Rollup merge of #143850 - fmease:comptest-simp-docck-handling, r=jieyouxu

Compiletest: Simplify {Html,Json}DocCk directive handling

So much more maintainable and extensible.

r? ````@jieyouxu```` as discussed
This commit is contained in:
Jakub Beránek 2025-07-14 11:04:54 +02:00 committed by GitHub
commit 2ec48e0489
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 294 additions and 359 deletions

View file

@ -55,6 +55,9 @@ Similar to shell commands,
directives can extend across multiple lines if their last char is `\`.
In this case, the start of the next line should be `//`, with no `@`.
Similar to compiletest directives, besides a space you can also use a colon `:` to separate
the directive name and the arguments, however a space is preferred for HtmlDocCk directives.
Use the special string `{{channel}}` in XPaths, `PATTERN` arguments and [snapshot files](#snapshot)
if you'd like to refer to the URL `https://doc.rust-lang.org/CHANNEL` where `CHANNEL` refers to the
current release channel (e.g, `stable` or `nightly`).

View file

@ -147,48 +147,17 @@ def concat_multi_lines(f):
print_err(lineno, line, "Trailing backslash at the end of the file")
def get_known_directive_names():
def filter_line(line):
line = line.strip()
return line.startswith('"') and (line.endswith('",') or line.endswith('"'))
# Equivalent to `src/tools/compiletest/src/header.rs` constant of the same name.
with open(
os.path.join(
# We go back to `src`.
os.path.dirname(os.path.dirname(__file__)),
"tools/compiletest/src/directive-list.rs",
),
"r",
encoding="utf8",
) as fd:
content = fd.read()
return [
line.strip().replace('",', "").replace('"', "")
for line in content.split("\n")
if filter_line(line)
]
# To prevent duplicating the list of commmands between `compiletest` and `htmldocck`, we put
# it into a common file which is included in rust code and parsed here.
# FIXME: This setup is temporary until we figure out how to improve this situation.
# See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
KNOWN_DIRECTIVE_NAMES = get_known_directive_names()
LINE_PATTERN = re.compile(
r"""
//@\s+
(?P<negated>!?)(?P<cmd>[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)
(?P<args>.*)$
(?P<negated>!?)(?P<cmd>.+?)
(?:[\s:](?P<args>.*))?$
""",
re.X | re.UNICODE,
)
DEPRECATED_LINE_PATTERN = re.compile(
r"""
//\s+@
""",
r"//\s+@",
re.X | re.UNICODE,
)
@ -209,12 +178,7 @@ def get_commands(template):
cmd = m.group("cmd")
negated = m.group("negated") == "!"
if not negated and cmd in KNOWN_DIRECTIVE_NAMES:
continue
args = m.group("args")
if args and not args[:1].isspace():
print_err(lineno, line, "Invalid template syntax")
continue
args = m.group("args") or ""
try:
args = shlex.split(args)
except UnicodeEncodeError:
@ -636,14 +600,11 @@ def check_command(c, cache):
else:
raise InvalidCheck("Invalid number of {} arguments".format(c.cmd))
elif c.cmd == "valid-html":
raise InvalidCheck("Unimplemented valid-html")
elif c.cmd == "valid-links":
raise InvalidCheck("Unimplemented valid-links")
else:
raise InvalidCheck("Unrecognized {}".format(c.cmd))
# Ignore unknown directives as they might be compiletest directives
# since they share the same `//@` prefix by convention. In any case,
# compiletest rejects unknown directives for us.
return
if ret == c.negated:
raise FailedCheck(cerr)

View file

@ -1,260 +0,0 @@
/// This was originally generated by collecting directives from ui tests and then extracting their
/// directive names. This is **not** an exhaustive list of all possible directives. Instead, this is
/// a best-effort approximation for diagnostics. Add new directives to this list when needed.
const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
// tidy-alphabetical-start
"add-core-stubs",
"assembly-output",
"aux-bin",
"aux-build",
"aux-codegen-backend",
"aux-crate",
"build-aux-docs",
"build-fail",
"build-pass",
"check-fail",
"check-pass",
"check-run-results",
"check-stdout",
"check-test-line-numbers-match",
"compile-flags",
"doc-flags",
"dont-check-compiler-stderr",
"dont-check-compiler-stdout",
"dont-check-failure-status",
"dont-require-annotations",
"edition",
"error-pattern",
"exact-llvm-major-version",
"exec-env",
"failure-status",
"filecheck-flags",
"forbid-output",
"force-host",
"ignore-16bit",
"ignore-32bit",
"ignore-64bit",
"ignore-aarch64",
"ignore-aarch64-pc-windows-msvc",
"ignore-aarch64-unknown-linux-gnu",
"ignore-aix",
"ignore-android",
"ignore-apple",
"ignore-arm",
"ignore-arm-unknown-linux-gnueabi",
"ignore-arm-unknown-linux-gnueabihf",
"ignore-arm-unknown-linux-musleabi",
"ignore-arm-unknown-linux-musleabihf",
"ignore-auxiliary",
"ignore-avr",
"ignore-beta",
"ignore-cdb",
"ignore-compare-mode-next-solver",
"ignore-compare-mode-polonius",
"ignore-coverage-map",
"ignore-coverage-run",
"ignore-cross-compile",
"ignore-eabi",
"ignore-elf",
"ignore-emscripten",
"ignore-endian-big",
"ignore-enzyme",
"ignore-freebsd",
"ignore-fuchsia",
"ignore-gdb",
"ignore-gdb-version",
"ignore-gnu",
"ignore-haiku",
"ignore-horizon",
"ignore-i686-pc-windows-gnu",
"ignore-i686-pc-windows-msvc",
"ignore-illumos",
"ignore-ios",
"ignore-linux",
"ignore-lldb",
"ignore-llvm-version",
"ignore-loongarch32",
"ignore-loongarch64",
"ignore-macabi",
"ignore-macos",
"ignore-msp430",
"ignore-msvc",
"ignore-musl",
"ignore-netbsd",
"ignore-nightly",
"ignore-none",
"ignore-nto",
"ignore-nvptx64",
"ignore-nvptx64-nvidia-cuda",
"ignore-openbsd",
"ignore-pass",
"ignore-powerpc",
"ignore-remote",
"ignore-riscv64",
"ignore-rustc-debug-assertions",
"ignore-rustc_abi-x86-sse2",
"ignore-s390x",
"ignore-sgx",
"ignore-sparc64",
"ignore-spirv",
"ignore-stable",
"ignore-stage1",
"ignore-stage2",
"ignore-std-debug-assertions",
"ignore-test",
"ignore-thumb",
"ignore-thumbv8m.base-none-eabi",
"ignore-thumbv8m.main-none-eabi",
"ignore-tvos",
"ignore-unix",
"ignore-unknown",
"ignore-uwp",
"ignore-visionos",
"ignore-vxworks",
"ignore-wasi",
"ignore-wasm",
"ignore-wasm32",
"ignore-wasm32-bare",
"ignore-wasm64",
"ignore-watchos",
"ignore-windows",
"ignore-windows-gnu",
"ignore-windows-msvc",
"ignore-x32",
"ignore-x86",
"ignore-x86_64",
"ignore-x86_64-apple-darwin",
"ignore-x86_64-pc-windows-gnu",
"ignore-x86_64-unknown-linux-gnu",
"incremental",
"known-bug",
"llvm-cov-flags",
"max-llvm-major-version",
"min-cdb-version",
"min-gdb-version",
"min-lldb-version",
"min-llvm-version",
"min-system-llvm-version",
"needs-asm-support",
"needs-crate-type",
"needs-deterministic-layouts",
"needs-dlltool",
"needs-dynamic-linking",
"needs-enzyme",
"needs-force-clang-based-tests",
"needs-git-hash",
"needs-llvm-components",
"needs-llvm-zstd",
"needs-profiler-runtime",
"needs-relocation-model-pic",
"needs-run-enabled",
"needs-rust-lld",
"needs-rustc-debug-assertions",
"needs-sanitizer-address",
"needs-sanitizer-cfi",
"needs-sanitizer-dataflow",
"needs-sanitizer-hwaddress",
"needs-sanitizer-kcfi",
"needs-sanitizer-leak",
"needs-sanitizer-memory",
"needs-sanitizer-memtag",
"needs-sanitizer-safestack",
"needs-sanitizer-shadow-call-stack",
"needs-sanitizer-support",
"needs-sanitizer-thread",
"needs-std-debug-assertions",
"needs-subprocess",
"needs-symlink",
"needs-target-has-atomic",
"needs-target-std",
"needs-threads",
"needs-unwind",
"needs-wasmtime",
"needs-xray",
"no-auto-check-cfg",
"no-prefer-dynamic",
"normalize-stderr",
"normalize-stderr-32bit",
"normalize-stderr-64bit",
"normalize-stdout",
"only-16bit",
"only-32bit",
"only-64bit",
"only-aarch64",
"only-aarch64-apple-darwin",
"only-aarch64-unknown-linux-gnu",
"only-apple",
"only-arm",
"only-avr",
"only-beta",
"only-bpf",
"only-cdb",
"only-dist",
"only-elf",
"only-emscripten",
"only-gnu",
"only-i686-pc-windows-gnu",
"only-i686-pc-windows-msvc",
"only-i686-unknown-linux-gnu",
"only-ios",
"only-linux",
"only-loongarch32",
"only-loongarch64",
"only-loongarch64-unknown-linux-gnu",
"only-macos",
"only-mips",
"only-mips64",
"only-msp430",
"only-msvc",
"only-nightly",
"only-nvptx64",
"only-powerpc",
"only-riscv64",
"only-rustc_abi-x86-sse2",
"only-s390x",
"only-sparc",
"only-sparc64",
"only-stable",
"only-thumb",
"only-tvos",
"only-unix",
"only-visionos",
"only-wasm32",
"only-wasm32-bare",
"only-wasm32-wasip1",
"only-watchos",
"only-windows",
"only-windows-gnu",
"only-windows-msvc",
"only-x86",
"only-x86_64",
"only-x86_64-apple-darwin",
"only-x86_64-fortanix-unknown-sgx",
"only-x86_64-pc-windows-gnu",
"only-x86_64-pc-windows-msvc",
"only-x86_64-unknown-linux-gnu",
"pp-exact",
"pretty-compare-only",
"pretty-mode",
"proc-macro",
"reference",
"regex-error-pattern",
"remap-src-base",
"revisions",
"run-fail",
"run-flags",
"run-pass",
"run-rustfix",
"rustc-env",
"rustfix-only-machine-applicable",
"should-fail",
"should-ice",
"stderr-per-bitwidth",
"test-mir-pass",
"unique-doc-out-dir",
"unset-exec-env",
"unset-rustc-env",
// Used by the tidy check `unknown_revision`.
"unused-revision-names",
// tidy-alphabetical-end
];

View file

@ -765,11 +765,266 @@ fn line_directive<'line>(
Some(DirectiveLine { line_number, revision, raw_directive })
}
// To prevent duplicating the list of directives between `compiletest`,`htmldocck` and `jsondocck`,
// we put it into a common file which is included in rust code and parsed here.
// FIXME: This setup is temporary until we figure out how to improve this situation.
// See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
include!("directive-list.rs");
/// This was originally generated by collecting directives from ui tests and then extracting their
/// directive names. This is **not** an exhaustive list of all possible directives. Instead, this is
/// a best-effort approximation for diagnostics. Add new directives to this list when needed.
const KNOWN_DIRECTIVE_NAMES: &[&str] = &[
// tidy-alphabetical-start
"add-core-stubs",
"assembly-output",
"aux-bin",
"aux-build",
"aux-codegen-backend",
"aux-crate",
"build-aux-docs",
"build-fail",
"build-pass",
"check-fail",
"check-pass",
"check-run-results",
"check-stdout",
"check-test-line-numbers-match",
"compile-flags",
"doc-flags",
"dont-check-compiler-stderr",
"dont-check-compiler-stdout",
"dont-check-failure-status",
"dont-require-annotations",
"edition",
"error-pattern",
"exact-llvm-major-version",
"exec-env",
"failure-status",
"filecheck-flags",
"forbid-output",
"force-host",
"ignore-16bit",
"ignore-32bit",
"ignore-64bit",
"ignore-aarch64",
"ignore-aarch64-pc-windows-msvc",
"ignore-aarch64-unknown-linux-gnu",
"ignore-aix",
"ignore-android",
"ignore-apple",
"ignore-arm",
"ignore-arm-unknown-linux-gnueabi",
"ignore-arm-unknown-linux-gnueabihf",
"ignore-arm-unknown-linux-musleabi",
"ignore-arm-unknown-linux-musleabihf",
"ignore-auxiliary",
"ignore-avr",
"ignore-beta",
"ignore-cdb",
"ignore-compare-mode-next-solver",
"ignore-compare-mode-polonius",
"ignore-coverage-map",
"ignore-coverage-run",
"ignore-cross-compile",
"ignore-eabi",
"ignore-elf",
"ignore-emscripten",
"ignore-endian-big",
"ignore-enzyme",
"ignore-freebsd",
"ignore-fuchsia",
"ignore-gdb",
"ignore-gdb-version",
"ignore-gnu",
"ignore-haiku",
"ignore-horizon",
"ignore-i686-pc-windows-gnu",
"ignore-i686-pc-windows-msvc",
"ignore-illumos",
"ignore-ios",
"ignore-linux",
"ignore-lldb",
"ignore-llvm-version",
"ignore-loongarch32",
"ignore-loongarch64",
"ignore-macabi",
"ignore-macos",
"ignore-msp430",
"ignore-msvc",
"ignore-musl",
"ignore-netbsd",
"ignore-nightly",
"ignore-none",
"ignore-nto",
"ignore-nvptx64",
"ignore-nvptx64-nvidia-cuda",
"ignore-openbsd",
"ignore-pass",
"ignore-powerpc",
"ignore-remote",
"ignore-riscv64",
"ignore-rustc-debug-assertions",
"ignore-rustc_abi-x86-sse2",
"ignore-s390x",
"ignore-sgx",
"ignore-sparc64",
"ignore-spirv",
"ignore-stable",
"ignore-stage1",
"ignore-stage2",
"ignore-std-debug-assertions",
"ignore-test",
"ignore-thumb",
"ignore-thumbv8m.base-none-eabi",
"ignore-thumbv8m.main-none-eabi",
"ignore-tvos",
"ignore-unix",
"ignore-unknown",
"ignore-uwp",
"ignore-visionos",
"ignore-vxworks",
"ignore-wasi",
"ignore-wasm",
"ignore-wasm32",
"ignore-wasm32-bare",
"ignore-wasm64",
"ignore-watchos",
"ignore-windows",
"ignore-windows-gnu",
"ignore-windows-msvc",
"ignore-x32",
"ignore-x86",
"ignore-x86_64",
"ignore-x86_64-apple-darwin",
"ignore-x86_64-pc-windows-gnu",
"ignore-x86_64-unknown-linux-gnu",
"incremental",
"known-bug",
"llvm-cov-flags",
"max-llvm-major-version",
"min-cdb-version",
"min-gdb-version",
"min-lldb-version",
"min-llvm-version",
"min-system-llvm-version",
"needs-asm-support",
"needs-crate-type",
"needs-deterministic-layouts",
"needs-dlltool",
"needs-dynamic-linking",
"needs-enzyme",
"needs-force-clang-based-tests",
"needs-git-hash",
"needs-llvm-components",
"needs-llvm-zstd",
"needs-profiler-runtime",
"needs-relocation-model-pic",
"needs-run-enabled",
"needs-rust-lld",
"needs-rustc-debug-assertions",
"needs-sanitizer-address",
"needs-sanitizer-cfi",
"needs-sanitizer-dataflow",
"needs-sanitizer-hwaddress",
"needs-sanitizer-kcfi",
"needs-sanitizer-leak",
"needs-sanitizer-memory",
"needs-sanitizer-memtag",
"needs-sanitizer-safestack",
"needs-sanitizer-shadow-call-stack",
"needs-sanitizer-support",
"needs-sanitizer-thread",
"needs-std-debug-assertions",
"needs-subprocess",
"needs-symlink",
"needs-target-has-atomic",
"needs-target-std",
"needs-threads",
"needs-unwind",
"needs-wasmtime",
"needs-xray",
"no-auto-check-cfg",
"no-prefer-dynamic",
"normalize-stderr",
"normalize-stderr-32bit",
"normalize-stderr-64bit",
"normalize-stdout",
"only-16bit",
"only-32bit",
"only-64bit",
"only-aarch64",
"only-aarch64-apple-darwin",
"only-aarch64-unknown-linux-gnu",
"only-apple",
"only-arm",
"only-avr",
"only-beta",
"only-bpf",
"only-cdb",
"only-dist",
"only-elf",
"only-emscripten",
"only-gnu",
"only-i686-pc-windows-gnu",
"only-i686-pc-windows-msvc",
"only-i686-unknown-linux-gnu",
"only-ios",
"only-linux",
"only-loongarch32",
"only-loongarch64",
"only-loongarch64-unknown-linux-gnu",
"only-macos",
"only-mips",
"only-mips64",
"only-msp430",
"only-msvc",
"only-nightly",
"only-nvptx64",
"only-powerpc",
"only-riscv64",
"only-rustc_abi-x86-sse2",
"only-s390x",
"only-sparc",
"only-sparc64",
"only-stable",
"only-thumb",
"only-tvos",
"only-unix",
"only-visionos",
"only-wasm32",
"only-wasm32-bare",
"only-wasm32-wasip1",
"only-watchos",
"only-windows",
"only-windows-gnu",
"only-windows-msvc",
"only-x86",
"only-x86_64",
"only-x86_64-apple-darwin",
"only-x86_64-fortanix-unknown-sgx",
"only-x86_64-pc-windows-gnu",
"only-x86_64-pc-windows-msvc",
"only-x86_64-unknown-linux-gnu",
"pp-exact",
"pretty-compare-only",
"pretty-mode",
"proc-macro",
"reference",
"regex-error-pattern",
"remap-src-base",
"revisions",
"run-fail",
"run-flags",
"run-pass",
"run-rustfix",
"rustc-env",
"rustfix-only-machine-applicable",
"should-fail",
"should-ice",
"stderr-per-bitwidth",
"test-mir-pass",
"unique-doc-out-dir",
"unset-exec-env",
"unset-rustc-env",
// Used by the tidy check `unknown_revision`.
"unused-revision-names",
// tidy-alphabetical-end
];
const KNOWN_HTMLDOCCK_DIRECTIVE_NAMES: &[&str] = &[
"count",
@ -834,35 +1089,26 @@ pub(crate) struct CheckDirectiveResult<'ln> {
pub(crate) fn check_directive<'a>(
directive_ln: &'a str,
mode: TestMode,
original_line: &str,
) -> CheckDirectiveResult<'a> {
let (directive_name, post) = directive_ln.split_once([':', ' ']).unwrap_or((directive_ln, ""));
let is_known_directive = KNOWN_DIRECTIVE_NAMES.contains(&directive_name)
|| match mode {
TestMode::Rustdoc => KNOWN_HTMLDOCCK_DIRECTIVE_NAMES.contains(&directive_name),
TestMode::RustdocJson => KNOWN_JSONDOCCK_DIRECTIVE_NAMES.contains(&directive_name),
_ => false,
};
let trailing = post.trim().split_once(' ').map(|(pre, _)| pre).unwrap_or(post);
let is_known = |s: &str| {
KNOWN_DIRECTIVE_NAMES.contains(&s)
|| match mode {
TestMode::Rustdoc | TestMode::RustdocJson => {
original_line.starts_with("//@")
&& match mode {
TestMode::Rustdoc => KNOWN_HTMLDOCCK_DIRECTIVE_NAMES,
TestMode::RustdocJson => KNOWN_JSONDOCCK_DIRECTIVE_NAMES,
_ => unreachable!(),
}
.contains(&s)
}
_ => false,
}
};
let trailing_directive = {
// 1. is the directive name followed by a space? (to exclude `:`)
matches!(directive_ln.get(directive_name.len()..), Some(s) if s.starts_with(' '))
directive_ln.get(directive_name.len()..).is_some_and(|s| s.starts_with(' '))
// 2. is what is after that directive also a directive (ex: "only-x86 only-arm")
&& is_known(trailing)
&& KNOWN_DIRECTIVE_NAMES.contains(&trailing)
}
.then_some(trailing);
CheckDirectiveResult { is_known_directive: is_known(&directive_name), trailing_directive }
CheckDirectiveResult { is_known_directive, trailing_directive }
}
const COMPILETEST_DIRECTIVE_PREFIX: &str = "//@";
@ -914,9 +1160,9 @@ fn iter_directives(
};
// Perform unknown directive check on Rust files.
if testfile.extension().map(|e| e == "rs").unwrap_or(false) {
if testfile.extension() == Some("rs") {
let CheckDirectiveResult { is_known_directive, trailing_directive } =
check_directive(directive_line.raw_directive, mode, ln);
check_directive(directive_line.raw_directive, mode);
if !is_known_directive {
*poisoned = true;
@ -936,7 +1182,7 @@ fn iter_directives(
"{testfile}:{line_number}: detected trailing compiletest test directive `{}`",
trailing_directive,
);
help!("put the trailing directive in it's own line: `//@ {}`", trailing_directive);
help!("put the trailing directive in its own line: `//@ {}`", trailing_directive);
return;
}

View file

@ -4,7 +4,7 @@ use super::{TestCx, remove_and_create_dir_all};
impl TestCx<'_> {
pub(super) fn run_rustdoc_test(&self) {
assert!(self.revision.is_none(), "revisions not relevant here");
assert!(self.revision.is_none(), "revisions not supported in this test suite");
let out_dir = self.output_base_dir();
remove_and_create_dir_all(&out_dir).unwrap_or_else(|e| {

View file

@ -6,7 +6,7 @@ impl TestCx<'_> {
pub(super) fn run_rustdoc_json_test(&self) {
//FIXME: Add bless option.
assert!(self.revision.is_none(), "revisions not relevant here");
assert!(self.revision.is_none(), "revisions not supported in this test suite");
let out_dir = self.output_base_dir();
remove_and_create_dir_all(&out_dir).unwrap_or_else(|e| {

View file

@ -105,13 +105,10 @@ impl DirectiveKind {
[_path, value] => Self::HasNotValue { value: value.clone() },
_ => panic!("`//@ !has` must have 2 or 3 arguments, but got {args:?}"),
},
// Ignore compiletest directives, like //@ edition
(_, false) if KNOWN_DIRECTIVE_NAMES.contains(&directive_name) => {
return None;
}
_ => {
panic!("Invalid directive `//@ {}{directive_name}`", if negated { "!" } else { "" })
}
// Ignore unknown directives as they might be compiletest directives
// since they share the same `//@` prefix by convention. In any case,
// compiletest rejects unknown directives for us.
_ => return None,
};
Some((kind, &args[0]))
@ -216,10 +213,6 @@ fn get_one<'a>(matches: &[&'a Value]) -> Result<&'a Value, String> {
}
}
// FIXME: This setup is temporary until we figure out how to improve this situation.
// See <https://github.com/rust-lang/rust/issues/125813#issuecomment-2141953780>.
include!(concat!(env!("CARGO_MANIFEST_DIR"), "/../compiletest/src/directive-list.rs"));
fn string_to_value<'a>(s: &str, cache: &'a Cache) -> Cow<'a, Value> {
if s.starts_with("$") {
Cow::Borrowed(&cache.variables.get(&s[1..]).unwrap_or_else(|| {

View file

@ -47,8 +47,8 @@ static LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
^\s*
//@\s+
(?P<negated>!?)
(?P<directive>[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)
(?P<args>.*)$
(?P<directive>.+?)
(?:[\s:](?P<args>.*))?$
"#,
)
.ignore_whitespace(true)
@ -58,15 +58,7 @@ static LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
});
static DEPRECATED_LINE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
RegexBuilder::new(
r#"
//\s+@
"#,
)
.ignore_whitespace(true)
.unicode(true)
.build()
.unwrap()
RegexBuilder::new(r"//\s+@").ignore_whitespace(true).unicode(true).build().unwrap()
});
fn print_err(msg: &str, lineno: usize) {
@ -94,7 +86,7 @@ fn get_directives(template: &str) -> Result<Vec<Directive>, ()> {
let negated = &cap["negated"] == "!";
let args_str = &cap["args"];
let args_str = cap.name("args").map(|m| m.as_str()).unwrap_or_default();
let Some(args) = shlex::split(args_str) else {
print_err(&format!("Invalid arguments to shlex::split: `{args_str}`",), lineno);
errors = true;