@@ -21,10 +21,11 @@ def file_exists(file_path: str) -> bool:
2121 """Check if a file exists."""
2222 return os .path .exists (file_path )
2323
24+
2425def extract_resource_addresses_from_content (content : str ) -> List [str ]:
2526 """Extract resource and module addresses from Terraform content."""
2627 addresses = []
27-
28+
2829 # Match resource blocks
2930 resource_pattern = r'resource\s+"([^"]+)"\s+"([^"]+)"'
3031 resources = re .finditer (resource_pattern , content )
@@ -36,35 +37,39 @@ def extract_resource_addresses_from_content(content: str) -> List[str]:
3637 modules = re .finditer (module_pattern , content )
3738 for match in modules :
3839 addresses .append (f"module.{ match .group (1 )} " )
39-
40+
4041 return addresses
4142
43+
4244def extract_resource_addresses_from_file (file_path : str ) -> List [str ]:
4345 """Extract resource and module addresses from a Terraform file."""
4446 addresses = []
45-
47+
4648 # First check if file exists
4749 if not file_exists (file_path ):
4850 print (f"Error: File { file_path } does not exist" , file = sys .stderr )
4951 sys .exit (1 )
50-
52+
5153 try :
5254 with open (file_path , "r" ) as f :
5355 content = f .read ()
54-
56+
5557 addresses = extract_resource_addresses_from_content (content )
5658
5759 except Exception as e :
5860 print (f"Warning: Could not process file { file_path } : { str (e )} " , file = sys .stderr )
5961
6062 if not addresses :
61- print (f"Warning: No resources or modules found in file { file_path } " , file = sys .stderr )
62-
63+ print (
64+ f"Warning: No resources or modules found in file { file_path } " ,
65+ file = sys .stderr ,
66+ )
67+
6368 return addresses
6469
6570
6671def is_resource_match (
67- resource_addr : str , filter_addrs : List [str ], file_addrs : List [str ]
72+ resource_addr : str , filter_addrs : List [str ] = [] , file_addrs : List [str ] = [ ]
6873) -> bool :
6974 """Check if resource address matches the filter conditions.
7075
@@ -106,25 +111,26 @@ def matches_address_list(addr: str, addr_list: List[str]) -> bool:
106111 if base_addr_match :
107112 # Get the base address and any remaining part after the index
108113 base_addr = base_addr_match .group (1 )
109- remaining = base_addr_match .group (3 )
110114
111115 # Check if the base address matches the filter
112116 if base_addr == filter_addr :
113117 return True
114118
115119 # Handle nested modules with indices
116- if filter_addr .startswith ("module." ) and base_addr .startswith (f"{ filter_addr } ." ):
120+ if filter_addr .startswith ("module." ) and base_addr .startswith (
121+ f"{ filter_addr } ."
122+ ):
117123 return True
118124
119125 # Standard module prefix match (for non-indexed modules)
120126 if filter_addr .startswith ("module." ) and addr .startswith (f"{ filter_addr } ." ):
121127 return True
122-
128+
123129 # For file addresses, we need to check if the resource type and name match
124130 # even if the full address is different due to modules
125131 resource_type , resource_name = extract_resource_type_and_name (addr )
126132 filter_type , filter_name = extract_resource_type_and_name (filter_addr )
127-
133+
128134 if resource_type and resource_name and filter_type and filter_name :
129135 if resource_type == filter_type and resource_name == filter_name :
130136 return True
@@ -152,23 +158,25 @@ def matches_address_list(addr: str, addr_list: List[str]) -> bool:
152158def filter_resources (
153159 state : Dict [str , Any ], addresses : List [str ] = [], files : List [str ] = []
154160) -> List [Dict [str , Any ]]:
155- """Extract matching AWS managed resources from state."""
161+ """Extract matching AWS resources from Terraform state."""
156162 # Extract addresses from files if provided
157163 file_addresses = []
158164 if files :
159165 for file_path in files :
160166 extracted = extract_resource_addresses_from_file (file_path )
161167 file_addresses .extend (extracted )
162-
168+
163169 if not file_addresses :
164- print ("Warning: No resources or modules found in any of the specified files." , file = sys .stderr )
170+ print (
171+ "Warning: No resources or modules found in any of the specified files." ,
172+ file = sys .stderr ,
173+ )
165174 print ("No resources will be included in the output." , file = sys .stderr )
166175 return []
167176
168177 resources = []
169178 modules_to_process = [state ["values" ]["root_module" ]]
170179
171- # Iterative approach instead of recursion
172180 while modules_to_process :
173181 module = modules_to_process .pop ()
174182
@@ -185,15 +193,18 @@ def filter_resources(
185193 resources .append (resource )
186194
187195 if not resources and (addresses or files ):
188- print ("Warning: No resources found matching the specified filters." , file = sys .stderr )
196+ print (
197+ "Warning: No resources found matching the specified filters." ,
198+ file = sys .stderr ,
199+ )
189200
190201 return resources
191202
192203
193204def generate_import_block (
194205 resource : Dict [str , Any ], schema_classes : Dict [str , type ]
195206) -> str :
196- """Generate import block for a resource."""
207+ """Generate Terraform import block for a resource."""
197208 matching_class = schema_classes .get (resource ["type" ])
198209 documentation = f"https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/{ resource ['type' ].replace ('aws_' , '' )} #import"
199210 import_id = f'"" # TODO: { documentation } '
@@ -213,7 +224,7 @@ def generate_import_block(
213224
214225
215226def generate_removed_block (resource_addr : str , destroy : bool = False ) -> str :
216- """Generate removed block for a resource or module."""
227+ """Generate Terraform removed block for a resource or module."""
217228 destroy_line = "\n destroy = true" if destroy else "\n destroy = false"
218229
219230 # Strip instance keys from the resource address for the 'from' attribute
@@ -227,23 +238,52 @@ def generate_removed_block(resource_addr: str, destroy: bool = False) -> str:
227238}}"""
228239
229240
241+ def generate_blocks_for_command (
242+ resources : List [Dict [str , Any ]], command : str , destroy : bool = False
243+ ) -> List [str ]:
244+ """Generate Terraform code blocks based on command."""
245+ blocks = []
246+
247+ if command == "remove" :
248+ # For removed blocks, deduplicate based on the base address
249+ base_addresses = set ()
250+
251+ for resource in resources :
252+ # Strip instance keys to get the base address
253+ base_addr = re .sub (
254+ r'(\[\d+\]|\[".+?"\]|\[\'.+?\'\])' , "" , resource ["address" ]
255+ )
256+
257+ # Only add a removed block if we haven't seen this base address before
258+ if base_addr not in base_addresses :
259+ base_addresses .add (base_addr )
260+ blocks .append (generate_removed_block (resource ["address" ], destroy ))
261+ elif command == "import" :
262+ # For import blocks, we need the full resource data
263+ schema_classes = get_aws_resource_import_id_generators ()
264+ blocks = [generate_import_block (r , schema_classes ) for r in resources ]
265+ else :
266+ raise ValueError (f"Invalid command '{ command } '" )
267+ return blocks
268+
269+
230270def parse_args () -> argparse .Namespace :
231271 """Parse command line arguments."""
232272 parser = argparse .ArgumentParser (
233273 description = "Terraform blocks utility for generating and managing Terraform blocks" ,
234- epilog = "Example usage: terraform show -json | tfblocks import [resource_addresses]"
274+ epilog = "Example usage: terraform show -json | tfblocks import [resource_addresses]" ,
235275 )
236276
237- # Global options
238277 parser .add_argument (
239278 "--no-color" , action = "store_true" , help = "Disable colored output"
240279 )
241280
242- # Create subcommands
243- subparsers = parser .add_subparsers (dest = "command" , help = "Command to execute" , required = True )
281+ subparsers = parser .add_subparsers (
282+ dest = "command" , help = "Command to execute" , required = True
283+ )
244284
245285 # Common filter arguments function
246- def add_filter_args (cmd_parser ):
286+ def add_filter_args (cmd_parser : argparse . ArgumentParser ):
247287 cmd_parser .add_argument (
248288 "addresses" ,
249289 nargs = "*" ,
@@ -253,11 +293,9 @@ def add_filter_args(cmd_parser):
253293 "--files" , "-f" , nargs = "+" , help = "Optional Terraform files to filter by"
254294 )
255295
256- # Import command
257296 import_parser = subparsers .add_parser ("import" , help = "Generate import blocks" )
258297 add_filter_args (import_parser )
259298
260- # Remove command
261299 remove_parser = subparsers .add_parser ("remove" , help = "Generate removed blocks" )
262300 add_filter_args (remove_parser )
263301 remove_parser .add_argument (
@@ -266,73 +304,44 @@ def add_filter_args(cmd_parser):
266304 help = "Set destroy = true in removed blocks (default is false)" ,
267305 )
268306
269- # List command
270- list_parser = subparsers .add_parser ("list" , help = "List resource addresses" )
307+ list_parser = subparsers .add_parser (
308+ "list" , help = "List addresses delimited by newlines"
309+ )
271310 add_filter_args (list_parser )
272311
273- # No default command - require explicit command
274312 args = parser .parse_args ()
275313 return args
276314
277315
278- def generate_blocks_for_command (
279- resources : List [Dict [str , Any ]], command : str , destroy : bool = False
280- ) -> List [str ]:
281- """Generate appropriate blocks based on command."""
282- blocks = []
283-
284- if command == "remove" :
285- # For removed blocks, deduplicate based on the base address
286- base_addresses = set ()
287-
288- for resource in resources :
289- # Strip instance keys to get the base address
290- base_addr = re .sub (
291- r'(\[\d+\]|\[".+?"\]|\[\'.+?\'\])' , "" , resource ["address" ]
292- )
293-
294- # Only add a removed block if we haven't seen this base address before
295- if base_addr not in base_addresses :
296- base_addresses .add (base_addr )
297- blocks .append (generate_removed_block (resource ["address" ], destroy ))
298- else : # import command
299- # For import blocks, we need the full resource data
300- schema_classes = get_aws_resource_import_id_generators ()
301- blocks = [generate_import_block (r , schema_classes ) for r in resources ]
302-
303- return blocks
304-
305-
306316def main ():
307317 args = parse_args ()
308-
309318 state = json .load (sys .stdin )
310-
311319 if state .get ("format_version" ) != "1.0" :
312320 print (
313321 "Error: Unsupported state file format version. Expected version '1.0'." ,
314322 file = sys .stderr ,
315323 )
316324 sys .exit (1 )
317-
318325 resources = filter_resources (state , args .addresses , args .files )
319326
320327 if args .command == "list" :
321- # Just output the addresses
322328 for resource in resources :
323329 print (resource ["address" ])
324330 return
325331
326- # Generate blocks based on command
327332 blocks = generate_blocks_for_command (
328333 resources , args .command , getattr (args , "destroy" , False )
329334 )
330335
331- # Output results
332336 if args .no_color :
333337 print ("\n \n " .join (blocks ))
334338 else :
335- print ("\033 [92m" + "\n \n " .join (blocks ) + "\033 [0m" )
339+ if args .command == "import" :
340+ print ("\033 [92m" + "\n \n " .join (blocks ) + "\033 [0m" )
341+ elif args .command == "remove" :
342+ print ("\033 [91m" + "\n \n " .join (blocks ) + "\033 [0m" )
343+ else :
344+ print ("\033 [92m" + "\n \n " .join (blocks ) + "\033 [0m" )
336345
337346
338347if __name__ == "__main__" :
0 commit comments