UNPKG

@struktoai/mirage-core

Version:

A Unified Virtual Filesystem For AI Agents

815 lines • 32.9 kB
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2026 @ Strukto.AI All Rights Reserved. ========= import { GNU_LONG_OPTIONS } from "./long_options.js"; import { Argument } from "./types.js"; // The two options every registered command answers, as GNU coreutils does. // They live here rather than beside the wrapper that injects them because // they are grammar: `commands/config.ts` appends them to a spec before the // parser reads the line, and a CLI node appends --help the same way. Shared // SINGLETONS, so `o === HELP_OPTION` identifies one through any copy of a // spec, which is what tells a builtin's grammar apart from a registered // command that borrowed its name. export const HELP_OPTION = new Argument('--help', { action: 'store_true', help: 'Show this help and exit', }); export const VERSION_OPTION = new Argument('--version', { action: 'store_true', help: 'Show version information and exit', }); // The operand slots most builtin specs declare, one shared object each: an // Argument is frozen, and only the two options above are ever told apart by // identity. export const PATHS_OPERAND = new Argument('paths', { metavar: '', type: 'path', nargs: '*' }); export const TEXTS_OPERAND = new Argument('texts', { metavar: '', nargs: '*' }); export const PATH_OPERAND = new Argument('path', { metavar: '', type: 'path', nargs: '?' }); export const PATH2_OPERAND = new Argument('path2', { metavar: '', type: 'path', nargs: '?' }); export const TEXT_OPERAND = new Argument('text', { metavar: '', nargs: '?' }); export const REMAINDER_OPERAND = new Argument('texts', { metavar: '', nargs: 'REMAINDER' }); // Stand-in name for a required operand whose slot declares none, so a // refusal that has to name the slot always has a word for it. Bare like // every operand name: the brackets are the renderer's. export const ARG_PLACEHOLDER = 'ARG'; // The path options whose lone `-` is stdout rather than a file, keyed by // command, valued by the option's canonical spelling: `wget -O -` and // `curl -D -`. Such a value stays `-`, unresolved and outside routing. // Explicit `./-` still names a file (curl 8.14.1 writes `-D ./-` there). export const STDOUT_DASH_OPTIONS = new Map([ ['wget', '-O'], ['curl', '--dump-header'], ]); // CPython and node read the script from stdin for a lone `-`, including // after `--`. Explicit `./-` still names a file (CPython 3.12, node 22). export const STDIN_SCRIPT_COMMANDS = new Set([ 'python', 'python3', 'js', 'node', ]); // The commands that read standard input for a lone `-` operand, so the word // names no path and routes nowhere: `split - /data/x` runs on /data from any // working directory. Everywhere else (`touch -`, `rev -`) it is a file in the // working directory, and explicit `./-` always is. export const STDIN_DASH_COMMANDS = new Set([ 'awk', 'b2sum', 'base64', 'cat', 'cmp', 'comm', 'csplit', 'cut', 'diff', 'expand', 'fmt', 'fold', 'grep', 'gunzip', 'gzip', 'head', 'join', 'md5sum', 'nl', 'od', 'paste', 'rg', 'sed', 'sha1sum', 'sha224sum', 'sha256sum', 'sha384sum', 'sha512sum', 'sort', 'split', 'tac', 'tail', 'unexpand', 'uniq', 'wc', 'xxd', 'zcat', ]); // How many leading operands may read standard input, for a command whose // later operands name outputs: split reads FILE and writes PREFIX, so // `split f -` writes `-aa` to the working directory (GNU coreutils 9.7). export const STDIN_DASH_LEADING = new Map([['split', 1]]); // The name an operand goes by on a flag bag's tape. The tape records the // operands in scan order among the option occurrences, so a program can tell // which options were typed before each one. An operand has no dest, and no // option's dest is empty. export const OPERAND = ''; // The programs whose option loop files each operand the moment it reads it, // so the rest slot's textWhen options turn textual only the operands typed // after the first of them, and the operands before it keep the declared kind. // jq's main.c walks argv once: a word before any --args or --jsonargs is an // input file, and one after either is a positional value (jq 1.8.2 reads // `jq -c '[., $ARGS.positional]' f.json --args a` as f.json plus ["a"]). tar // reads the whole line first: GNU tar 1.35 lists `d/m` for // `tar -f a.tar d/m -t` as it does for `tar -f a.tar -t d/m`. Measured, not // derived, like the other per-program rules. join's getopt loop runs // RETURN_IN_ORDER too, so `join a b c -a 3` refuses the extra operand before // the bad file number, and a missing-operand line names the line's last word // rather than its last operand: `join a.txt -t ,` is missing an operand after // ',' (coreutils 9.7). uniq's loop does as well: `uniq a b c -f x` refuses the // extra operand, `uniq -f x a b c` the count. Info-ZIP's unzip reads its words // in order too: -x takes every operand after it, up to a -d (UnZip 6.00). // find reads its link options only before the first start point, so `find -L // x` follows and `find x -L` does not (findutils 4.10). export const IN_ORDER_OPERANDS = new Set([ 'find', 'jq', 'join', 'uniq', 'unzip', ]); // The option words an IN_ORDER_OPERANDS program's own loop reads by their // spelling, so the tape keeps each one where it was typed, as [SPELLED, word] // just before the occurrence it spells. GNU join reads a `-j1` or `-j2` typed // as a word of its own as the obsolete `-j1 FIELD` when a third operand needs // the room (`join -j1 2 a b`), while `-j 1` and `-ij1` are always both files' // field 1, and it reads every operand after `--` as a file (coreutils 9.7). export const SPELLED_WORDS = Object.freeze({ join: new Set(['-j1', '-j2', '--']), }); // The programs whose option loop reads a dash-led word as options only when a // letter follows the dash, and any other one as an operand where it sits: // jq's isoptish() is a dash followed by a second dash or isalpha(). Measured // on jq 1.8.2: `jq -n '-1'` runs the program -1, `jq . -1` reads a file named // -1, `--jsonargs -1 -.5` reads [-1,-0.5] and `--args - -. '- x'` reads three // strings, while `-x` is still refused and `-nan` is -n, -a and -n. The letter // is an ASCII one: `-é` is an operand in the POSIX and the C.UTF-8 locale // alike. export const LETTER_OPTIONS = new Set(['jq']); // The dash-led words such a program reads as options. A `--` word takes the // long-option branch before this is asked. export const DASH_LETTER = /^-[A-Za-z]/; // The programs that read a dash followed by a digit or a point as a negative // number before each getopt call, and stop scanning options there: the word // and every word after it are operands. Measured on coreutils 9.7: `seq -1.5 // 1` counts from -1.5, `seq -.5 .5` from -0.5, and `seq -1 -w 1` refuses -w // as LAST, while `seq -inf` is still the invalid option -i. export const NEGATIVE_NUMBER_OPERANDS = new Set(['seq']); // The dash-led words such a program takes for a negative number. export const NEGATIVE_NUMBER = /^-[.0-9]/; // The programs that compare a long option's whole word against their own // table, as strcmp does, `=` included, so `--name=value` is an unknown option, // value and all, rather than the option and its value. Measured on jq 1.8.2: // `--indent=3` and `--slurp=1` are each refused as `jq: Unknown option` with // the word as typed. jq takes no abbreviation either, which is argparse's // allowAbbrev, so its spec declares that instead. export const WHOLE_WORD_LONG_OPTIONS = new Set(['jq']); // The programs whose handler runs the option loop: the parser leaves each // option it refuses on the tape where it met it, as [REFUSED, word], and // nothing ahead of the handler refuses the line or answers --help for it. // jq's main.c walks argv once and stops at the first word it cannot take, so // a bad --jsonargs value, --indent width or --slurpfile typed first is the // one reported, and --help and --version answer where they stand. Measured // on jq 1.8.2: `jq -n . --jsonargs '{' --bogus` reports the JSON, // `jq -n . --bogus --jsonargs '{'` reports --bogus, and `jq --help --bogus` // prints the help. export const OWN_OPTION_LOOP = new Set(['jq']); // The name a refused option goes by on the tape of an OWN_OPTION_LOOP // program, whose value is the option word as the program names it: the whole // word for a long option (`--indent=3`), the dash and the first letter it // does not know for a cluster (`-x`), and the option itself when the line // ends before its value (`--arg`). No option of such a program is spelled // `-?`, so no dest is `?`. export const REFUSED = '?'; // The name a SPELLED_WORDS word goes by on the tape. No option is spelled // `-=`, so no dest is `=`. export const SPELLED = '='; const AMBIGUOUS_NAMES = Object.freeze({ l: 'args_l', O: 'args_O', I: 'args_I', '1': 'args_1', }); // Numeric shorthand token like `-5` (head/tail count), never a flag // cluster or a path. /** * Map a flag name to its dispatcher kwarg name. * * Mirrors Python's `flag_kwarg_name`. The dispatcher spells flags without * their dashes and with dashes turned into underscores, so this is the one * place that translation lives. */ export function flagKwargName(flag) { const clean = flag.replace(/^-+/, '').replaceAll('-', '_'); return AMBIGUOUS_NAMES[clean] ?? clean; } export const NUMERIC_SHORT = /^-\d+$/; // GNU echo is not getopt, so its option surface is a word shape, not a // CommandSpec: options are LEADING words matching this pattern only. export const ECHO_OPTION = /^-[neE]+$/; // The programs with NO long-option parser at all: their answer to a // dash-leading word they do not recognize is to print it as an operand rather // than to refuse it, and they never expand an abbreviation. bash's `echo` // builtin reads only a leading `-neE` cluster and prints every other word // verbatim, `--` included (`echo -- --zzz` prints `-- --zzz`); Info-ZIP unzip // has no `--long` grammar either, scanning the word's letters instead, so // `unzip --nothelp` prints help for the `h` the word happens to contain. // unzip's own refusal -- exit 10 with the whole usage block, and `--` not an // end-of-options marker -- is a separate change; it sits here because of the // two answers the parser has today, the lenient one is the closer. // // MEASURED against coreutils 9.4, bash 5.2.21 and Info-ZIP 6.00, and // deliberately NOT derived. #1107 proposed deriving it -- require that "the // command declare no long options" -- and no predicate over the declarations // can work: `sleep`, `pwd`, `bc`, `history` and `printf` declare zero long // options in mirage's specs and all five REPORT an option they do not know, // while `echo` declares zero and treats it as an operand, so `expr` and // `sleep` are indistinguishable that way. The `@command` decorator also // injects `--help`/`--version` into every registered spec, so no spec declares // zero by the time the parser reads it. Ten of the thirteen commands the old // rest-operand-kind predicate reached (basename, dirname, csplit, numfmt, // sleep, pwd, bc, history, bash, printf) are strict, which is why this is an // exception list and not a rule. export const NO_LONG_OPTIONS = new Set(['echo', 'unzip']); // The programs whose long options are parsed ONLY when the line carries // exactly one argument: gnulib's `parse_long_options`, whose guard is // literally `argc == 2`. Measured on coreutils 9.4: `expr --help` exits 0 with // help, `expr --help x` exits 2 with // `expr: syntax error: unexpected argument 'x'`, and `expr -- --help` is argc // 3, so it prints `--help`. Inside the window getopt_long's name-prefix // matching applies (`--h`, `--hel` and `--versio` all resolve) and a word that // prefixes nothing falls through to an operand with no diagnostic at all, // because parse_long_options sets `opterr = 0`: `expr --hex` prints `--hex`, // and so does `expr --help=x`. This is a narrower rule than NO_LONG_OPTIONS // and not the same one -- echo has no long options in any position, expr has // them in exactly one -- so the two are spelled separately rather than // collapsed. export const SOLE_ARGUMENT_LONG_OPTIONS = new Set(['expr']); // The two programs that answer a standard option only once the WHOLE option // scan has succeeded, rather than at the position the word sits in. GNU grep's // getopt loop sets `show_version` / `show_help` and keeps scanning, printing // after the loop, so a refusal anywhere on the line outranks the answer; // ripgrep's clap parse is whole-line for the same reason. Measured on GNU grep // 3.11 and ripgrep 14.1.1, and for BOTH options: `grep --version --bogus`, // `grep --help --bogus` and `rg --version --bogus` all report the option and // exit 2, where `cat --version --bogus` prints the version and // `cat --help --bogus` the help page, both exit 0, because coreutils calls // `version_etc` or `usage` and exits INSIDE the loop. export const STANDARD_AFTER_SCAN = new Set(['grep', 'rg']); // The short options a builtin answers as it answers --help or --version // (grep 3.11, ripgrep 14.1.1). export const SHORT_STANDARD = { grep: { '-V': '--version' }, rg: { '-h': '--help', '-V': '--version' }, }; // The options a builtin answers like --version with a line of its own: // ripgrep's --pcre2-version, the line its --version ends with (14.1.1). export const EXTRA_STANDARD = { rg: ['--pcre2-version'], }; // The STANDARD_AFTER_SCAN programs that answer the standard option they read // last: ripgrep keeps one special mode and each one replaces the last, so // `rg --version --help` prints the help page and `rg -hV` the version. grep // tests show_version before show_help, so `grep --help -V` prints the // version (grep 3.11, ripgrep 14.1.1). export const STANDARD_LAST_WINS = new Set(['rg']); // The programs whose getopt string lists the ten digits as options, the // obsolete `-NUM` count spelled one letter at a time: the digits of one word // build a number and a later word replaces it, wherever they sit in a // cluster. Measured on coreutils 9.7: `split -d10` is `-d` and ten lines, as // are `-10d` and `-dx10`, and `split -12 -5` is five. head and tail list the // digits too but refuse one past the first word, so they keep only the // whole-word `-NUM` that numericShorthand reads. grep 3.11 reads them the // same way into its -C (`grep -1n` and `-n1`). export const DIGIT_OPTIONS = new Set(['grep', 'split']); // The long spellings that are one option under two names, keyed by // "<program> <synonym>" to the spelling it duplicates: glibc's several // long_options entries sharing one `val`, so a prefix of both resolves rather // than being ambiguous. Every other pair of declared longs is two options, // and a prefix of both is ambiguous, which is what getopt_long answers for // `ls --re` and `uname --k`. Measured on coreutils 9.7 and grep 3.11: // `grep --col` is --color and `date --u` is --utc. Python keys the same table // by (program, synonym) pairs. export const LONG_SYNONYMS = new Map([ ['grep --colour', '--color'], ['date --universal', '--utc'], ['rg --passthrough', '--passthru'], ]); // The whole long-option table of a program whose getopt_long resolves an // abbreviation against more options than mirage declares for it, so a partial // spec cannot say whether a prefix is ambiguous: `tar --fil` is `--file` or // `--files-from`, the second one mirage never declared. Each group is one // option, its primary spelling first and its aliases after it (argp's // OPTION_ALIAS entries, which share the primary's key and so never make a // prefix of both ambiguous), in the order getopt_long reads the table, which // is the order it lists possibilities in. A prefix names an option this table // resolves; one mirage does not declare stays unrecognized. // // GNU tar 1.35: argp hands getopt_long tar.c's options, then names.c's, then // argp's own --help, --usage, --program-name and --HANG, then --version. // Mirrors Python's TAR_LONG_OPTIONS. export const TAR_LONG_OPTIONS = [ ['--list'], ['--extract', '--get'], ['--create'], ['--diff', '--compare'], ['--append'], ['--update'], ['--catenate', '--concatenate'], ['--delete'], ['--test-label'], ['--sparse'], ['--hole-detection'], ['--sparse-version'], ['--incremental'], ['--listed-incremental'], ['--level'], ['--ignore-failed-read'], ['--occurrence'], ['--seek'], ['--no-seek'], ['--no-check-device'], ['--check-device'], ['--verify'], ['--remove-files'], ['--keep-old-files'], ['--skip-old-files'], ['--keep-newer-files'], ['--overwrite'], ['--unlink-first'], ['--recursive-unlink'], ['--no-overwrite-dir'], ['--overwrite-dir'], ['--keep-directory-symlink'], ['--one-top-level'], ['--to-stdout'], ['--to-command'], ['--ignore-command-error'], ['--no-ignore-command-error'], ['--owner'], ['--group'], ['--owner-map'], ['--group-map'], ['--mtime'], ['--clamp-mtime'], ['--mode'], ['--atime-preserve'], ['--touch'], ['--same-owner'], ['--no-same-owner'], ['--numeric-owner'], ['--preserve-permissions', '--same-permissions'], ['--no-same-permissions'], ['--preserve-order', '--same-order'], ['--delay-directory-restore'], ['--no-delay-directory-restore'], ['--sort'], ['--xattrs'], ['--no-xattrs'], ['--xattrs-include'], ['--xattrs-exclude'], ['--selinux'], ['--no-selinux'], ['--acls'], ['--no-acls'], ['--file'], ['--force-local'], ['--rmt-command'], ['--rsh-command'], ['--multi-volume'], ['--tape-length'], ['--info-script', '--new-volume-script'], ['--volno-file'], ['--blocking-factor'], ['--record-size'], ['--ignore-zeros'], ['--read-full-records'], ['--format'], ['--old-archive', '--portability'], ['--posix'], ['--pax-option'], ['--label'], ['--auto-compress'], ['--no-auto-compress'], ['--use-compress-program'], ['--bzip2'], ['--gzip', '--gunzip', '--ungzip'], ['--compress', '--uncompress'], ['--lzip'], ['--lzma'], ['--lzop'], ['--xz'], ['--zstd'], ['--one-file-system'], ['--absolute-names'], ['--dereference'], ['--hard-dereference'], ['--starting-file'], ['--newer', '--after-date'], ['--newer-mtime'], ['--backup'], ['--suffix'], ['--strip-components'], ['--transform', '--xform'], ['--checkpoint'], ['--checkpoint-action'], ['--check-links'], ['--totals'], ['--utc'], ['--full-time'], ['--index-file'], ['--block-number'], ['--show-defaults'], ['--show-snapshot-field-ranges'], ['--show-omitted-dirs'], ['--show-transformed-names', '--show-stored-names'], ['--quoting-style'], ['--quote-chars'], ['--no-quote-chars'], ['--interactive', '--confirmation'], ['--verbose'], ['--warning'], ['--restrict'], ['--add-file'], ['--directory'], ['--files-from'], ['--null'], ['--no-null'], ['--unquote'], ['--no-unquote'], ['--verbatim-files-from'], ['--no-verbatim-files-from'], ['--exclude'], ['--exclude-from'], ['--exclude-caches'], ['--exclude-caches-under'], ['--exclude-caches-all'], ['--exclude-tag'], ['--exclude-ignore'], ['--exclude-ignore-recursive'], ['--exclude-tag-under'], ['--exclude-tag-all'], ['--exclude-vcs'], ['--exclude-vcs-ignores'], ['--exclude-backups'], ['--recursion'], ['--no-recursion'], ['--anchored'], ['--no-anchored'], ['--ignore-case'], ['--no-ignore-case'], ['--wildcards'], ['--no-wildcards'], ['--wildcards-match-slash'], ['--no-wildcards-match-slash'], ['--help'], ['--usage'], ['--program-name'], ['--HANG'], ['--version'], ]; export const LONG_OPTION_TABLES = { ...GNU_LONG_OPTIONS, tar: TAR_LONG_OPTIONS, }; // The programs whose short value options drop one `=` from an attached value, // the way lexopt (ripgrep's parser), clap and argparse read `-x=VALUE`. GNU // getopt keeps it (`head -n=5` is refused as `=5`), which is every other // builtin. Measured on ripgrep 14.1.1: `rg -g=*.py`, `rg -m=1`, `rg -A=1` and // `rg -e=hello` read `*.py`, `1`, `1` and `hello`. export const EQUALS_SHORT_VALUES = new Set(['rg']); // ripgrep's own flag table, every long name, negation and alias in the order // of its FLAGS array (crates/core/flags/defs.rs, 14.1.1). ripgrep takes no // abbreviation of any of them (`rg --pcr` is `unrecognized flag --pcr`) and // suggests the ones whose name shares at least 40% of its 3-grams with an // unknown flag, in this order (flags/parse.rs, `suggest`). export const RG_FLAG_NAMES = [ 'regexp', 'file', 'after-context', 'before-context', 'binary', 'no-binary', 'block-buffered', 'no-block-buffered', 'byte-offset', 'no-byte-offset', 'case-sensitive', 'color', 'colors', 'column', 'no-column', 'context', 'context-separator', 'no-context-separator', 'count', 'count-matches', 'crlf', 'no-crlf', 'debug', 'dfa-size-limit', 'encoding', 'no-encoding', 'engine', 'field-context-separator', 'field-match-separator', 'files', 'files-with-matches', 'files-without-match', 'fixed-strings', 'no-fixed-strings', 'follow', 'no-follow', 'generate', 'glob', 'glob-case-insensitive', 'no-glob-case-insensitive', 'heading', 'no-heading', 'help', 'hidden', 'no-hidden', 'hostname-bin', 'hyperlink-format', 'iglob', 'ignore-case', 'ignore-file', 'ignore-file-case-insensitive', 'no-ignore-file-case-insensitive', 'include-zero', 'no-include-zero', 'invert-match', 'no-invert-match', 'json', 'no-json', 'line-buffered', 'no-line-buffered', 'line-number', 'no-line-number', 'line-regexp', 'max-columns', 'max-columns-preview', 'no-max-columns-preview', 'max-count', 'max-depth', 'maxdepth', 'max-filesize', 'mmap', 'no-mmap', 'multiline', 'no-multiline', 'multiline-dotall', 'no-multiline-dotall', 'no-config', 'no-ignore', 'ignore', 'no-ignore-dot', 'ignore-dot', 'no-ignore-exclude', 'ignore-exclude', 'no-ignore-files', 'ignore-files', 'no-ignore-global', 'ignore-global', 'no-ignore-messages', 'ignore-messages', 'no-ignore-parent', 'ignore-parent', 'no-ignore-vcs', 'ignore-vcs', 'no-messages', 'messages', 'no-require-git', 'require-git', 'no-unicode', 'unicode', 'null', 'null-data', 'one-file-system', 'no-one-file-system', 'only-matching', 'path-separator', 'passthru', 'passthrough', 'pcre2', 'no-pcre2', 'pcre2-version', 'pre', 'no-pre', 'pre-glob', 'pretty', 'quiet', 'regex-size-limit', 'replace', 'search-zip', 'no-search-zip', 'smart-case', 'sort', 'sortr', 'stats', 'no-stats', 'stop-on-nonmatch', 'text', 'no-text', 'threads', 'trace', 'trim', 'no-trim', 'type', 'type-not', 'type-add', 'type-clear', 'type-list', 'unrestricted', 'version', 'vimgrep', 'with-filename', 'no-filename', 'word-regexp', 'auto-hybrid-regex', 'no-auto-hybrid-regex', 'no-pcre2-unicode', 'pcre2-unicode', 'sort-files', 'no-sort-files', ]; // The one program whose standard options outrank every option refusal, // wherever the word sits. zgrep is a shell script that reads the line in its // own loop before it ever builds a grep command, and that loop answers both // itself. Measured on gzip 1.13: `zgrep --bogus --version f.gz` and // `zgrep --bogus --help f.gz` each print zgrep's own output and exit 0, while // `zgrep --bogus f.gz` reaches grep and is refused with exit 2. export const STANDARD_BEFORE_SCAN = new Set(['zgrep']); // The grep options zgrep refuses before grep sees them, since it reads one // stream per file and walks no directory: `zgrep -r` says // `zgrep: -r: option not supported` and exits 2, naming the letter in a // cluster (`-nr`) and a long one as typed, value included. A long one is // refused from the shortest prefix that names it alone among grep's options, // each mapped here to that length; a shorter one is grep's ambiguous option. // Measured on gzip 1.13. export const ZGREP_UNSUPPORTED_SHORT = new Set(['r', 'R', 'd']); export const ZGREP_UNSUPPORTED_LONG = new Map([ ['recursive', 3], ['directories', 2], ['include', 3], ['exclude', 3], ['exclude-from', 3], ['exclude-dir', 3], ]); // The spec-declared `choices` sets that GNU matches as word choices, so an // unambiguous prefix of a candidate resolves to it and the bag is rewritten // to the canonical word. Every other declared set compares the whole word, // which is argparse's own rule for `choices` and so the right default for // the grammar this spec layer is modelled on: a mount author's custom // `--mode` with choices ('read', 'remove') refuses `rem`, and an installed // CLI's node refuses `--state=o`, as clap and git do. // // It is an opt-in table because prefix matching is the rare case, not the // common one: the GNU commands that really do take such a word -- // `--backup`, `ls --sort`, `ls --time`, `sort --check`, `cp --update`, // `tail --follow`, `wc --total`, `uniq`, `cut` -- call `matchWord` from the // command with their own candidate list, and never reach the parser's // `Argument.choices` at all. The entries below are the whole of what does. // Measured on coreutils 9.7: `tee --output-error=exit-n` resolves to // `exit-nopipe` while `=w` and `=e` are ambiguous, `numfmt --to=s` resolves // to `si` and `--to=ie` is ambiguous between `iec` and `iec-i`, `date -Is` // resolves to `seconds` and `date --rfc-3339=` is ambiguous. // // Written as "<command> <canonical long spelling>" because that is how the // measurement reads, but it NAMES the builtin `Argument` objects rather // than keying on the two strings: the parser resolves each entry once and // then asks whether the option declaring a set IS one of them. A name is not // identity, and a mount may register its own `tee` (commands/registry.ts) // whose `--output-error` would otherwise inherit GNU's prefix rule from a // spelling collision alone. Identity is also the only signal that survives // registration, which hands the parser an enriched COPY of the spec // (config.ts appends --help/--version), so `spec === BUILTIN_SPECS[name]` is // false for every builtin by the time a line is parsed while every declared // Argument is still the same object. A command name never contains a space, so // the joined key is unambiguous; python spells the same table as a set of // pairs. export const WORD_CHOICE_OPTIONS = new Set([ 'tee --output-error', 'numfmt --to', 'numfmt --from', 'numfmt --round', 'numfmt --invalid', 'date --iso-8601', 'date --rfc-3339', ]); // Value shape accepted by an int-typed option: optional sign plus digits, // the portable core of Python int() and argparse (no whitespace, no // underscores, so both languages accept exactly the same strings). export const INT_VALUE = /^[+-]?\d+$/; export const FLOAT_VALUE = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/; // GNU usage-error exit codes, pinned against debian coreutils/grep/diffutils // and hostname 3.25 (plus ripgrep and jq upstream docs). Everything else // exits 1. // Commands whose `Try '--help'` hint line is prefixed with the command // name (GNU diffutils style: `diff: Try 'diff --help' ...`). export const USAGE_HINT_PREFIX = new Set(['diff', 'cmp', 'patch']); // An old-style cluster letter left without its argument exits 2, not // USAGE_EXIT's 64: tar reads the cluster itself and raises its own fatal // error, while 64 (EX_USAGE) is what argp returns for a letter it does // not know. Pinned on GNU tar 1.35: `tar xzf` is 2, `tar -Q` is 64. export const OLD_OPTION_EXIT = 2; // The exit code of a command refused on one operand before it ran (an // admission policy's operand-scoped Deny): 1 for the GNU tools, which // report an operand they cannot act on and exit 1, and tar's own fatal // code, since tar reports an operand it cannot open and exits 2 (GNU tar // 1.35, `Exiting with failure status due to previous errors`). export const OPERAND_EXIT = Object.freeze({ tar: 2, }); export const USAGE_EXIT = Object.freeze({ grep: 2, egrep: 2, fgrep: 2, zgrep: 2, rg: 2, ls: 2, sort: 2, diff: 2, cmp: 2, awk: 2, jq: 2, curl: 2, patch: 2, tar: 64, timeout: 125, hostname: 255, python: 2, python3: 2, }); // The exit code a command answers when it cannot read an operand. GNU's // code belongs to the COMMAND, not to the errno: `sort nope` and `sort // dir` are both 2, `cat` is 1 for both. Absent means 1, which is what // the executor's catch-all already did on its own. Pinned on // debian:stable-slim (coreutils 9.7, GNU sed 4.9, gzip 1.13, jq 1.7, // binutils 2.44, util-linux 2.41.5, bsdmainutils 12.1.8, xxd from // vim-common). The python twin is READ_FAIL_EXIT in // commands/spec/constants.py. export const READ_FAIL_EXIT = Object.freeze({ sort: 2, awk: 2, jq: 2, xxd: 2, grep: 2, egrep: 2, fgrep: 2, rg: 2, cmp: 2, diff: 2, sed: 2, zgrep: 2, unzip: 9, }); // The commands whose code DOES depend on the errno, so the table above // cannot express them on its own. sed opens the directory successfully and // fails on the read, which is its own class (4), while a missing file fails at // open (2). The gzip family reports a directory as a warning (2) and a missing // file as an error (1). zgrep opens its operands itself, as `gzip -cdfq` // does, and a pattern file it cannot read is exit 2 however it failed. // Mirrors Python's READ_FAIL_EXIT_ISDIR. export const READ_FAIL_EXIT_ISDIR = Object.freeze({ sed: 4, gzip: 2, gunzip: 2, zcat: 2, }); // The interpreter commands answer option errors in CPython's words, not // GNU's: python3 is not a GNU tool, and its refusal names the // source-selecting options a reader needs. export const PYTHON_NAMES = new Set(['python', 'python3']); // The programs that do NOT parse with getopt_long, and so answer an option // they will not take by naming the whole typed token as unknown rather than by // naming the option. Each one is measured: `curl --silent=2` is // `curl: option --silent=2: is unknown`, `python3 --version=2` is // `Unknown option: --version=2`, `jq --tab=2` is `jq: Unknown option --tab=2`, // and find reads the word as a predicate. Every other command here is a GNU // tool whose getopt_long words the refusal the other way, so the set is the // exception list and not the rule. export const NOT_GETOPT_LONG = new Set(['curl', 'jq', 'find', ...PYTHON_NAMES]); // Pinned on CPython 3.12.13, including two quirks worth keeping: the // hint always spells the program `python` (never `python3`, whichever // way it was invoked), and it quotes with a backquote/quote pair. export const PYTHON_USAGE = 'usage: {name} [option] ... [-c cmd | -m mod | file | -] [arg] ...\n' + "Try `python -h' for more information.\n"; // How many distinct readings one spec keeps before it starts over. A line // reads each command's words from classification, admission, routing and // dispatch, a few dozen times with two or three distinct inputs, so a // handful covers a line. export const PARSES_KEPT = 16; // The most characters a command's words may hold, in all, for a spec to keep // its reading of them. A longer line (a `jq --argjson` document, a // heredoc-sized printf) is read afresh each time: keeping it would hold its // words until the table starts over, and reading them is cheap beside what // the command then does with them. With PARSES_KEPT this bounds what one spec // holds to about 16K characters. export const PARSE_KEPT_CHARS = 1024; //# sourceMappingURL=constants.js.map