@@ -137,7 +137,9 @@ def load_config(self) -> List[Dict[str, str]]:
137137 resolved_pattern = template .strip ().format (** self .variables )
138138 resolved_rules .append ({
139139 "name" : name ,
140- "pattern" : resolved_pattern
140+ "pattern" : resolved_pattern ,
141+ "dependency" : self .dependency ,
142+ "version" : self .version
141143 })
142144 except KeyError as e :
143145 print (f"Warning: Missing variable for interpolation in rule { name } : { e } " , file = sys .stderr )
@@ -178,7 +180,9 @@ def scan_file(file_path: str, compiled_rules: List[Dict[str, re.Pattern]]) -> Li
178180 "rule_name" : rule ["name" ],
179181 "line_number" : line_num ,
180182 "matched_string" : match .group (0 ).strip (),
181- "context_line" : line .strip ()
183+ "context_line" : line .strip (),
184+ "dependency" : rule .get ("dependency" , "" ),
185+ "version" : rule .get ("version" , "" )
182186 })
183187 except IOError as e :
184188 print (f"Warning: Could not read file { file_path } : { e } " , file = sys .stderr )
@@ -456,11 +460,12 @@ def read_package_file(file_path: str) -> List[str]:
456460
457461def scan_repository (
458462 root_path : str ,
459- rules : List [Dict [str , str ]],
463+ rules : List [Dict [str , Any ]],
460464 target_packages : List [str ] = None ,
461465 ignore_dirs : List [str ] = None ,
462- version_string : str = None
463- ) -> List [Dict [str , str ]]:
466+ version_string : str = None ,
467+ targets : List [Tuple [str , str ]] = None
468+ ) -> List [Dict [str , Any ]]:
464469 """
465470 Scans the repository directory tree applying resolved regex patterns to files.
466471
@@ -478,21 +483,30 @@ def scan_repository(
478483 performs a full recursive scan of the repository.
479484 ignore_dirs: Optional list of directory names or glob-like files to ignore (case-insensitive).
480485 version_string: Optional target version string (e.g. "3.7") to scan for in filenames.
486+ targets: Optional list of (dependency, version) tuples.
481487
482488 Returns:
483- A list of dictionaries detailing each match: 'file_path', 'repo_path',
484- 'package_name', 'rule_name', 'line_number', 'matched_string', 'context_line'.
489+ A list of dictionaries detailing each match.
485490 """
486491 ignore_lower = {i .lower () for i in ignore_dirs } if ignore_dirs else set ()
487492 results = []
488493
494+ filename_targets = []
495+ if targets :
496+ filename_targets = targets
497+ elif version_string :
498+ dep = rules [0 ].get ("dependency" ) if rules else None
499+ filename_targets = [(dep , version_string )]
500+
489501 # Compile patterns once here
490502 compiled_rules = []
491503 for rule in rules :
492504 try :
493505 compiled_rules .append ({
494506 "name" : rule ["name" ],
495- "pattern" : re .compile (rule ["pattern" ], re .IGNORECASE )
507+ "pattern" : re .compile (rule ["pattern" ], re .IGNORECASE ),
508+ "dependency" : rule .get ("dependency" , "" ),
509+ "version" : rule .get ("version" , "" )
496510 })
497511 except re .error as e :
498512 print (f"Error compiling regex for rule { rule ['name' ]} : { e } " , file = sys .stderr )
@@ -510,7 +524,6 @@ def scan_repository(
510524 files = [f for f in files if f .lower () not in ignore_lower ]
511525
512526 rel_root = os .path .relpath (root , root_path )
513- parts = rel_root .split (os .sep )
514527
515528 # Layout-agnostic generic subdirectory filtering
516529 if target_packages :
@@ -533,13 +546,16 @@ def scan_repository(
533546 matches = scan_file (file_path , compiled_rules )
534547
535548 # Add filename match if applicable
536- if version_string and version_string in file :
537- matches .append ({
538- "rule_name" : "filename_match" ,
539- "line_number" : 0 ,
540- "matched_string" : version_string ,
541- "context_line" : f"Filename contains { version_string } "
542- })
549+ for dep , ver in filename_targets :
550+ if ver and ver in file :
551+ matches .append ({
552+ "rule_name" : "filename_match" ,
553+ "line_number" : 0 ,
554+ "matched_string" : ver ,
555+ "context_line" : f"Filename contains { ver } " ,
556+ "dependency" : dep or "" ,
557+ "version" : ver
558+ })
543559
544560 # Compute display path and package name
545561 rel_file_path = os .path .relpath (file_path , root_path )
@@ -567,6 +583,46 @@ def scan_repository(
567583 return results
568584
569585
586+ def parse_targets (targets_input : str ) -> List [Tuple [str , str ]]:
587+ """
588+ Parses a targets input (file path or inline YAML/JSON string) into a list of (dependency, version) tuples.
589+ """
590+ raw_targets = {}
591+ content = targets_input
592+
593+ # Check if the input is a file path
594+ if os .path .exists (targets_input ):
595+ try :
596+ with open (targets_input , 'r' , encoding = 'utf-8' ) as f :
597+ content = f .read ()
598+ except PermissionError :
599+ print (f"Error: Permission denied reading targets file: { targets_input } " , file = sys .stderr )
600+ sys .exit (1 )
601+ except Exception as e :
602+ print (f"Error reading targets file { targets_input } : { e } " , file = sys .stderr )
603+ sys .exit (1 )
604+
605+ try :
606+ raw_targets = yaml .safe_load (content )
607+ except Exception as e :
608+ print (f"Error parsing targets YAML/JSON content: { e } " , file = sys .stderr )
609+ sys .exit (1 )
610+
611+ if not isinstance (raw_targets , dict ):
612+ print ("Error: Targets input must resolve to a JSON object or YAML mapping" , file = sys .stderr )
613+ sys .exit (1 )
614+
615+ targets = []
616+ for dep , versions in raw_targets .items ():
617+ if isinstance (versions , list ):
618+ for v in versions :
619+ targets .append ((str (dep ), str (v )))
620+ else :
621+ targets .append ((str (dep ), str (versions )))
622+
623+ return targets
624+
625+
570626def main ():
571627 script_dir = os .path .dirname (os .path .abspath (__file__ ))
572628 default_config = os .path .join (script_dir , "regex_config.yaml" )
@@ -577,16 +633,19 @@ def main():
577633
578634 parser .add_argument (
579635 "-d" , "--dependency" ,
580- required = True ,
581636 help = "Name of the dependency (e.g., python, protobuf)"
582637 )
583638
584639 parser .add_argument (
585640 "-v" , "--version" ,
586- required = True ,
587641 help = "Specific version to search for (e.g., 3.7, 4.25.8)"
588642 )
589643
644+ parser .add_argument (
645+ "--targets" ,
646+ help = "Path to a YAML/JSON targets file, or an inline YAML/JSON string (e.g. 'python: [3.8, 3.9]')"
647+ )
648+
590649 parser .add_argument (
591650 "-p" , "--path" ,
592651 default = "." ,
@@ -650,6 +709,25 @@ def main():
650709
651710 args = parser .parse_args ()
652711
712+ # Validation of required inputs
713+ has_single_target = bool (args .dependency and args .version )
714+ has_targets_list = bool (args .targets )
715+
716+ if not (has_single_target or has_targets_list ):
717+ parser .error ("Must specify either (-d/--dependency AND -v/--version) OR (--targets)" )
718+ if has_single_target and has_targets_list :
719+ parser .error ("Cannot specify both single target (-d/-v) and targets list (--targets)" )
720+
721+ targets = []
722+ if has_targets_list :
723+ targets = parse_targets (args .targets )
724+ else :
725+ targets = [(args .dependency , args .version )]
726+
727+ if not targets :
728+ print ("Error: No targets resolved to scan." , file = sys .stderr )
729+ sys .exit (1 )
730+
653731 # Resolve target packages if filtering is requested
654732 target_packages = []
655733 if args .package :
@@ -661,9 +739,14 @@ def main():
661739 elif args .package_file :
662740 target_packages = read_package_file (args .package_file )
663741
664- print (f"Starting scan for dependency: { args .dependency } version: { args .version } " )
742+ if has_targets_list :
743+ print ("Starting scan for multiple targets:" )
744+ for dep , ver in targets :
745+ print (f" - { dep } : { ver } " )
746+ else :
747+ print (f"Starting scan for dependency: { args .dependency } version: { args .version } " )
665748 print (f"Root path: { args .path } " )
666- print (f "Targets to scan:" )
749+ print ("Targets to scan:" )
667750 if target_packages :
668751 for pkg in target_packages :
669752 print (f" - { os .path .join (args .path , pkg )} " )
@@ -672,8 +755,10 @@ def main():
672755 print (f"Using config: { args .config } " )
673756
674757 # Load and resolve rules
675- config_manager = ConfigManager (args .config , args .dependency , args .version )
676- rules = config_manager .load_config ()
758+ rules = []
759+ for dep , ver in targets :
760+ config_manager = ConfigManager (args .config , dep , ver )
761+ rules .extend (config_manager .load_config ())
677762
678763
679764
@@ -686,7 +771,14 @@ def main():
686771 print (f"Loaded { len (ignore_dirs )} ignore patterns from { ignore_file_path } " )
687772
688773 # Scan repository
689- all_matches = scan_repository (args .path , rules , target_packages , ignore_dirs , version_string = args .version )
774+ all_matches = scan_repository (
775+ args .path ,
776+ rules ,
777+ target_packages ,
778+ ignore_dirs ,
779+ version_string = (None if has_targets_list else args .version ),
780+ targets = targets
781+ )
690782
691783 print (f"\n Found { len (all_matches )} matches." )
692784 display_matches = all_matches if args .stdout else all_matches [:10 ]
@@ -708,7 +800,14 @@ def main():
708800 script_dir = os .path .dirname (os .path .abspath (__file__ ))
709801 results_dir = os .path .join (script_dir , "results" )
710802 os .makedirs (results_dir , exist_ok = True )
711- output_path = os .path .join (results_dir , f"{ args .dependency } -{ args .version } -{ timestamp } .csv" )
803+ if has_targets_list :
804+ if os .path .exists (args .targets ):
805+ base_name = os .path .splitext (os .path .basename (args .targets ))[0 ]
806+ else :
807+ base_name = "targets"
808+ output_path = os .path .join (results_dir , f"{ base_name } -{ timestamp } .csv" )
809+ else :
810+ output_path = os .path .join (results_dir , f"{ args .dependency } -{ args .version } -{ timestamp } .csv" )
712811
713812 write_csv_report (output_path , all_matches )
714813
0 commit comments