@@ -9,7 +9,7 @@ priv suberror CliError {
99priv struct CliOptions {
1010 rules_root : String?
1111 rule_file : String?
12- patterns : Array [String ]
12+ patterns : Array [@rule_model. RulePatternSpec ]
1313 scan_root : String
1414 exclude_dirs : Array [String ]
1515 verbose : Bool
@@ -59,6 +59,11 @@ let scan_command : @argparse.Command = @argparse.Command(
5959 action=@argparse.OptionAction ::Append ,
6060 about="Anonymous structural pattern to match.",
6161 ),
62+ @argparse.OptionArg (
63+ "guard",
64+ action=@argparse.OptionAction ::Append ,
65+ about="YAML guard map for the preceding anonymous pattern.",
66+ ),
6267 @argparse.OptionArg (
6368 "exclude-dir",
6469 action=@argparse.OptionAction ::Append ,
@@ -218,6 +223,7 @@ fn dump_cli_command(
218223fn scan_cli_options(
219224 scan_matches : @argparse.Matches ,
220225 env : Map [String , String ],
226+ argv : Array [String ],
221227) -> CliOptions raise {
222228 let verbose = cli_flag_enabled(scan_matches.flags, "verbose")
223229 let enable_builtin_rules = cli_flag_enabled(
@@ -227,7 +233,7 @@ fn scan_cli_options(
227233 let no_color = no_color_enabled(env)
228234 let rules_root = last_cli_value(scan_matches.values, "rules")
229235 let rule_file = last_cli_value(scan_matches.values, "rule")
230- let patterns = all_cli_values(scan_matches.values, "pattern" )
236+ let patterns = parse_scan_pattern_specs(argv )
231237 let exclude_dirs = normalize_scan_exclude_dirs(
232238 all_cli_values(scan_matches.values, "exclude-dir"),
233239 )
@@ -258,6 +264,115 @@ fn scan_cli_options(
258264 }
259265}
260266
267+ ///|
268+ fn parse_scan_pattern_specs(
269+ argv : Array [String ],
270+ ) -> Array [@rule_model.RulePatternSpec ] raise {
271+ let patterns : Array [@rule_model.RulePatternSpec ] = []
272+ let has_guard : Array [Bool ] = []
273+ let mut index = 1
274+ while index < argv.length() {
275+ let arg = argv[index]
276+ if arg == "--pattern" {
277+ if index + 1 >= argv.length() {
278+ raise CliError ::Usage (
279+ message="missing value for --pattern",
280+ exit_code=2,
281+ )
282+ }
283+ patterns.push({ shape: argv[index + 1], guards: {} })
284+ has_guard.push(false )
285+ index += 2
286+ } else if arg.has_prefix("--pattern=") {
287+ patterns.push({
288+ shape: arg["--pattern=".length():].to_owned(),
289+ guards: {},
290+ })
291+ has_guard.push(false )
292+ index += 1
293+ } else if arg == "--guard" {
294+ if index + 1 >= argv.length() {
295+ raise CliError ::Usage (message="missing value for --guard", exit_code=2)
296+ }
297+ attach_scan_guard(
298+ patterns,
299+ has_guard,
300+ parse_cli_guard_map(argv[index + 1]),
301+ )
302+ index += 2
303+ } else if arg.has_prefix("--guard=") {
304+ attach_scan_guard(
305+ patterns,
306+ has_guard,
307+ parse_cli_guard_map(arg["--guard=".length():].to_owned()),
308+ )
309+ index += 1
310+ } else {
311+ index += 1
312+ }
313+ }
314+ patterns
315+ }
316+
317+ ///|
318+ fn attach_scan_guard(
319+ patterns : Array [@rule_model.RulePatternSpec ],
320+ has_guard : Array [Bool ],
321+ guards : Map [String , String ],
322+ ) -> Unit raise {
323+ let mut index = patterns.length()
324+ while index > 0 {
325+ index -= 1
326+ if !has_guard[index] {
327+ let shape = patterns[index].shape
328+ patterns[index] = { shape, guards }
329+ has_guard[index] = true
330+ return
331+ }
332+ }
333+ let message = if patterns.is_empty() {
334+ "--guard requires a preceding --pattern"
335+ } else {
336+ "duplicate --guard for anonymous --pattern"
337+ }
338+ raise CliError ::Usage (message~, exit_code=2)
339+ }
340+
341+ ///|
342+ fn parse_cli_guard_map(source : String ) -> Map [String , String ] raise {
343+ let docs = @yaml.Yaml ::load_from_string(source) catch {
344+ err =>
345+ raise CliError ::Usage (message="invalid --guard YAML : \{err}", exit_code=2)
346+ }
347+ if docs.length() != 1 {
348+ raise CliError ::Usage (
349+ message="--guard must contain exactly one YAML document",
350+ exit_code=2,
351+ )
352+ }
353+ guard docs[0] is Map (map) else {
354+ raise CliError ::Usage (message="--guard must be a YAML mapping", exit_code=2)
355+ }
356+ let guards : Map [String , String ] = {}
357+ for key, value in map {
358+ if !key.has_prefix("$") || key.length() <= 1 {
359+ raise CliError ::Usage (
360+ message="guard key \{key} must be a $-prefixed metavar name",
361+ exit_code=2,
362+ )
363+ }
364+ match value {
365+ String (regex_source) => guards[key] = regex_source
366+ _ =>
367+ raise CliError ::Usage (
368+ message="--guard.\{key} must be a string",
369+ exit_code=2,
370+ )
371+ }
372+ }
373+ guards
374+ }
375+
261376///|
262377fn normalize_scan_exclude_dir_args(argv : Array [String ]) -> Array [String ] raise {
263378 if argv.length() == 0 || argv[0] != "scan" {
@@ -318,7 +433,7 @@ fn parse_cli_command(
318433 }
319434 match matches.subcommand {
320435 Some (("scan", scan_matches)) =>
321- ("scan", Some (scan_cli_options(scan_matches, env)), None )
436+ ("scan", Some (scan_cli_options(scan_matches, env, normalized_argv )), None )
322437 Some (("docs", docs_matches)) => docs_cli_command(docs_matches)
323438 Some (("dump", dump_matches)) => dump_cli_command(dump_matches)
324439 _ => raise CliError ::Usage (message="missing required command", exit_code=2)
0 commit comments