@@ -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 )
@@ -465,10 +469,11 @@ def read_package_file(file_path: str) -> List[str]:
465469
466470def scan_repository (
467471 root_path : str ,
468- rules : List [Dict [str , str ]],
472+ rules : List [Dict [str , Any ]],
469473 target_packages : List [str ] = None ,
470474 ignore_dirs : List [str ] = None ,
471- version_string : str = None
475+ version_string : str = None ,
476+ targets : List [Tuple [str , str ]] = None
472477) -> List [Dict [str , Any ]]:
473478 """
474479 Scans the repository directory tree applying resolved regex patterns to files.
@@ -487,21 +492,30 @@ def scan_repository(
487492 performs a full recursive scan of the repository.
488493 ignore_dirs: Optional list of directory names or glob-like files to ignore (case-insensitive).
489494 version_string: Optional target version string (e.g. "3.7") to scan for in filenames.
495+ targets: Optional list of (dependency, version) tuples.
490496
491497 Returns:
492- A list of dictionaries detailing each match: 'file_path', 'repo_path',
493- 'package_name', 'rule_name', 'line_number', 'matched_string', 'context_line'.
498+ A list of dictionaries detailing each match.
494499 """
495500 ignore_lower = {i .lower () for i in ignore_dirs } if ignore_dirs else set ()
496501 results = []
497502
503+ filename_targets = []
504+ if targets :
505+ filename_targets = targets
506+ elif version_string :
507+ dep = rules [0 ].get ("dependency" ) if rules else None
508+ filename_targets = [(dep , version_string )]
509+
498510 # Compile patterns once here
499511 compiled_rules = []
500512 for rule in rules :
501513 try :
502514 compiled_rules .append ({
503515 "name" : rule ["name" ],
504- "pattern" : re .compile (rule ["pattern" ], re .IGNORECASE )
516+ "pattern" : re .compile (rule ["pattern" ], re .IGNORECASE ),
517+ "dependency" : rule .get ("dependency" , "" ),
518+ "version" : rule .get ("version" , "" )
505519 })
506520 except re .error as e :
507521 print (f"Error compiling regex for rule { rule ['name' ]} : { e } " , file = sys .stderr )
@@ -541,13 +555,16 @@ def scan_repository(
541555 matches = scan_file (file_path , compiled_rules )
542556
543557 # Add filename match if applicable
544- if version_string and version_string in file :
545- matches .append ({
546- "rule_name" : "filename_match" ,
547- "line_number" : 0 ,
548- "matched_string" : version_string ,
549- "context_line" : f"Filename contains { version_string } "
550- })
558+ for dep , ver in filename_targets :
559+ if ver and ver in file :
560+ matches .append ({
561+ "rule_name" : "filename_match" ,
562+ "line_number" : 0 ,
563+ "matched_string" : ver ,
564+ "context_line" : f"Filename contains { ver } " ,
565+ "dependency" : dep or "" ,
566+ "version" : ver
567+ })
551568
552569 # Compute display path and package name
553570 rel_file_path = os .path .relpath (file_path , root_path )
@@ -576,6 +593,46 @@ def scan_repository(
576593 return results
577594
578595
596+ def parse_targets (targets_input : str ) -> List [Tuple [str , str ]]:
597+ """
598+ Parses a targets input (file path or inline YAML/JSON string) into a list of (dependency, version) tuples.
599+ """
600+ raw_targets = {}
601+ content = targets_input
602+
603+ # Check if the input is a file path
604+ if os .path .exists (targets_input ):
605+ try :
606+ with open (targets_input , 'r' , encoding = 'utf-8' ) as f :
607+ content = f .read ()
608+ except PermissionError :
609+ print (f"Error: Permission denied reading targets file: { targets_input } " , file = sys .stderr )
610+ sys .exit (1 )
611+ except Exception as e :
612+ print (f"Error reading targets file { targets_input } : { e } " , file = sys .stderr )
613+ sys .exit (1 )
614+
615+ try :
616+ raw_targets = yaml .safe_load (content )
617+ except Exception as e :
618+ print (f"Error parsing targets YAML/JSON content: { e } " , file = sys .stderr )
619+ sys .exit (1 )
620+
621+ if not isinstance (raw_targets , dict ):
622+ print ("Error: Targets input must resolve to a JSON object or YAML mapping" , file = sys .stderr )
623+ sys .exit (1 )
624+
625+ targets = []
626+ for dep , versions in raw_targets .items ():
627+ if isinstance (versions , list ):
628+ for v in versions :
629+ targets .append ((str (dep ), str (v )))
630+ else :
631+ targets .append ((str (dep ), str (versions )))
632+
633+ return targets
634+
635+
579636def main ():
580637 script_dir = os .path .dirname (os .path .abspath (__file__ ))
581638 default_config = os .path .join (script_dir , "regex_config.yaml" )
@@ -586,16 +643,19 @@ def main():
586643
587644 parser .add_argument (
588645 "-d" , "--dependency" ,
589- required = True ,
590646 help = "Name of the dependency (e.g., python, protobuf)"
591647 )
592648
593649 parser .add_argument (
594650 "-v" , "--version" ,
595- required = True ,
596651 help = "Specific version to search for (e.g., 3.7, 4.25.8)"
597652 )
598653
654+ parser .add_argument (
655+ "--targets" ,
656+ help = "Path to a YAML/JSON targets file, or an inline YAML/JSON string (e.g. 'python: [3.8, 3.9]')"
657+ )
658+
599659 parser .add_argument (
600660 "-p" , "--path" ,
601661 default = "." ,
@@ -659,6 +719,25 @@ def main():
659719
660720 args = parser .parse_args ()
661721
722+ # Validation of required inputs
723+ has_single_target = bool (args .dependency and args .version )
724+ has_targets_list = bool (args .targets )
725+
726+ if not (has_single_target or has_targets_list ):
727+ parser .error ("Must specify either (-d/--dependency AND -v/--version) OR (--targets)" )
728+ if has_single_target and has_targets_list :
729+ parser .error ("Cannot specify both single target (-d/-v) and targets list (--targets)" )
730+
731+ targets = []
732+ if has_targets_list :
733+ targets = parse_targets (args .targets )
734+ else :
735+ targets = [(args .dependency , args .version )]
736+
737+ if not targets :
738+ print ("Error: No targets resolved to scan." , file = sys .stderr )
739+ sys .exit (1 )
740+
662741 # Resolve target packages if filtering is requested
663742 target_packages = []
664743 if args .package :
@@ -670,7 +749,12 @@ def main():
670749 elif args .package_file :
671750 target_packages = read_package_file (args .package_file )
672751
673- print (f"Starting scan for dependency: { args .dependency } version: { args .version } " )
752+ if has_targets_list :
753+ print ("Starting scan for multiple targets:" )
754+ for dep , ver in targets :
755+ print (f" - { dep } : { ver } " )
756+ else :
757+ print (f"Starting scan for dependency: { args .dependency } version: { args .version } " )
674758 print (f"Root path: { args .path } " )
675759 print ("Targets to scan:" )
676760 if target_packages :
@@ -681,8 +765,10 @@ def main():
681765 print (f"Using config: { args .config } " )
682766
683767 # Load and resolve rules
684- config_manager = ConfigManager (args .config , args .dependency , args .version )
685- rules = config_manager .load_config ()
768+ rules = []
769+ for dep , ver in targets :
770+ config_manager = ConfigManager (args .config , dep , ver )
771+ rules .extend (config_manager .load_config ())
686772
687773
688774
@@ -695,7 +781,14 @@ def main():
695781 print (f"Loaded { len (ignore_dirs )} ignore patterns from { ignore_file_path } " )
696782
697783 # Scan repository
698- all_matches = scan_repository (args .path , rules , target_packages , ignore_dirs , version_string = args .version )
784+ all_matches = scan_repository (
785+ args .path ,
786+ rules ,
787+ target_packages ,
788+ ignore_dirs ,
789+ version_string = (None if has_targets_list else args .version ),
790+ targets = targets
791+ )
699792
700793 print (f"\n Found { len (all_matches )} matches." )
701794 display_matches = all_matches if args .stdout else all_matches [:10 ]
@@ -717,7 +810,14 @@ def main():
717810 script_dir = os .path .dirname (os .path .abspath (__file__ ))
718811 results_dir = os .path .join (script_dir , "results" )
719812 os .makedirs (results_dir , exist_ok = True )
720- output_path = os .path .join (results_dir , f"{ args .dependency } -{ args .version } -{ timestamp } .csv" )
813+ if has_targets_list :
814+ if os .path .exists (args .targets ):
815+ base_name = os .path .splitext (os .path .basename (args .targets ))[0 ]
816+ else :
817+ base_name = "targets"
818+ output_path = os .path .join (results_dir , f"{ base_name } -{ timestamp } .csv" )
819+ else :
820+ output_path = os .path .join (results_dir , f"{ args .dependency } -{ args .version } -{ timestamp } .csv" )
721821
722822 write_csv_report (output_path , all_matches )
723823
0 commit comments