Files
sausage/userland/brush/source/brush-shell/tests/cases/compat/arguments.yaml
T
2026-11-11 22:04:16 +00:00

660 lines
20 KiB
YAML

name: "Argument handling tests"
common_test_files:
- path: "script.sh"
contents: |
echo \"$0\" \"$1\" \"$2\" \"$@\"
# Announces that it ran, so cases can prove a script was *not* executed.
- path: "announce.sh"
contents: |
echo "RAN-SCRIPT count=[$#] one=[$1] two=[$2]"
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
case $- in *s*) echo "stdin-flag";; *) echo "no-stdin-flag";; esac
# Consumes stdin, to prove leftover stdin reaches the script as data.
- path: "reader.sh"
contents: |
while IFS= read -r line; do echo "read=[$line]"; done
# Only sourced by an *interactive* shell, and only via `--rcfile`. Both effects it has
# -- the variable and the alias -- are things a non-interactive shell does not get.
- path: "testrc"
contents: |
# Re-assert the harness's prompt: cases that load this file drop `--norc`, which
# lets a system-wide /etc/bash.bashrc (Debian/Ubuntu) clobber the inherited PS1.
PS1='test$ '
RC_MARKER=loaded
alias aliascheck='echo ALIAS-EXPANDED'
# Reports whether the rc file reached it, from inside a named script.
- path: "rccheck.sh"
contents: |
echo "RC[$RC_MARKER]"
cases:
- name: "-c mode arguments without --"
args:
- "-c"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- 1
- "-2"
- "3"
- name: "-c mode arguments with --"
args:
- "-c"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- "--"
- 1
- 2
- 3
- name: "-c mode and arguments with +O"
args:
- "+O"
- "nullglob"
- "-c"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- "--"
- 1
- 2
- 3
- name: "-o enables an option disabled with +o"
known_failure: true # TODO(args): every +o is applied after every -o, so the last one doesn't win
args: ["+o", "errexit", "-o", "errexit", "-c", 'echo "$-"']
- name: "+O disables a shopt option enabled with -O"
known_failure: true # TODO(args): every -O is applied after every +O, so the last one doesn't win
args: ["-O", "extglob", "+O", "extglob", "-c", "shopt extglob"]
- name: "-c mode -- torture"
args:
- "-c"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- --
- --
- -&-1
- --!
- "\"-2\""
- "''--''"
- 3--*
- name: "-c modeonly one --"
args:
- "-c"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- --
- name: "script arguments without --"
args:
- script.sh
- -1
- -2
- -3
- name: "script arguments with --"
args:
- script.sh
- --
- --1
- -2
- 3
- name: "script -- torture"
args:
- script.sh
- --
- "--"
- --
- -!-1*
- "\"-2\""
- --
- 3--
- name: "script only one --"
args:
- script.sh
- --
- name: "-c with -- separator and command"
args:
- "-c"
- "--"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- "1"
- "-2"
- "3"
- name: "-c with -- separator only"
ignore_stderr: true
args:
- "-c"
- "--"
- name: "-c with -- separator where command is --"
ignore_stderr: true
args:
- "-c"
- "--"
- "--"
- "echo"
- "hi"
- name: "-c with option before command"
args:
- "-c"
- "-u"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- "1"
- "-2"
- "3"
- name: "-c with value-taking option before command"
args:
- "-c"
- "-o"
- "errexit"
- 'echo \"$0\" \"$1\" \"$@\"'
- "1"
- "2"
- name: "-c with option before command and no command"
ignore_stderr: true
args:
- "-c"
- "-u"
- name: "-c with option then -- separator"
args:
- "-c"
- "-u"
- "--"
- 'echo \"$0\" \"$1\" \"$@\"'
- "1"
- "2"
- name: "-ec with -- separator and command"
args:
- "-ec"
- "--"
- 'echo \"$0\" \"$1\" \"$2\" \"$@\"'
- "1"
- "-2"
- "3"
# `-c` is a flag, not a value-taking option: the command string is the first
# non-option argument, so flags may be grouped with it.
- name: "-c grouped with a later flag"
args:
- "-cf"
- 'echo * \"$0\" \"$1\"'
- "name"
- "1"
- name: "-c with no command string"
ignore_stderr: true
args:
- "-c"
- name: "-c with an empty command string"
args:
- "-c"
- ""
- "name"
- "1"
# Only the first `--` is the option terminator; a later one is an ordinary positional.
- name: "-c with -- separator and a later --"
args:
- "-c"
- "--"
- 'echo \"$0\" \"$1\" \"$@\"'
- "--"
- "more"
# `--` after a boolean flag ends option parsing, so a following `-c` is a script path,
# not a flag.
- name: "-c after -- is a script path, not a flag"
ignore_stderr: true
args:
- "-e"
- "--"
- "-c"
- "echo hi"
- name: "-o with -- is not an option terminator"
ignore_stderr: true
args:
- "-o"
- "--"
# bash does not honor an attached value for `-o`: it takes the *next* word as the
# option name, so this fails with "--: invalid option name" (exit 2). brush's parser
# instead accepts "c" as the attached value, then treats `echo` as a script path.
- name: "-oc with -- takes its option name from the next word"
known_failure: true
ignore_stderr: true
args:
- "-oc"
- "--"
- "echo"
#
# Where the shell takes its commands from, and what the leftover words become.
#
# Three sources are possible -- a `-c` string, a named script, or standard input --
# and exactly one of `-c`/`-s`/a leading non-option word picks it. Getting that
# classification wrong silently changes both which commands run and what `$1`... are,
# so every combination below is pinned against bash.
#
- name: "no args reads commands from stdin"
stdin: |
echo "count=[$#] all=[$@]"
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
case $- in *s*) echo "stdin-flag";; *) echo "no-stdin-flag";; esac
- name: "-s with no positional args"
args: ["-s"]
stdin: |
echo "count=[$#] all=[$@]"
case $- in *s*) echo "stdin-flag";; *) echo "no-stdin-flag";; esac
- name: "-s with one positional arg"
args: ["-s", "myarg"]
stdin: |
echo "count=[$#] one=[$1] all=[$@]"
- name: "-s with multiple positional args"
args: ["-s", "a", "b", "c"]
stdin: |
echo "count=[$#] one=[$1] two=[$2] three=[$3] all=[$@] star=[$*]"
# A `--` in option position ends option parsing and is dropped, as in bash. Only the
# first one is the terminator; a second `--` is an ordinary positional.
- name: "-s with -- separator followed by literal --"
args: ["-s", "--", "--", "a"]
stdin: |
echo "count=[$#] one=[$1] two=[$2]"
- name: "-s -e with -- separator"
args: ["-s", "-e", "--", "a"]
stdin: |
echo "count=[$#] one=[$1]"
- name: "-s with hyphen-leading positional args"
args: ["-s", "a", "-2", "c"]
stdin: |
echo "count=[$#] one=[$1] two=[$2] three=[$3] all=[$@]"
# `-s` claims the script slot, so a script named among the trailing words is a
# positional parameter and must NOT be executed.
- name: "-s does not run a script named among its positional args"
args: ["-s", "announce.sh"]
stdin: |
echo "count=[$#] one=[$1]"
# ...and $0 stays the shell's own name rather than being taken from the words.
- name: "-s leaves $0 alone rather than consuming the first positional"
args: ["-s", "myarg"]
stdin: |
case "$0" in myarg) echo "BAD: \$0 took the positional";; *) echo "OK: \$0 untouched";; esac
echo "one=[$1]"
- name: "-s reports the stdin flag in \\$-"
args: ["-s", "myarg"]
stdin: |
case $- in *s*) echo "stdin-flag";; *) echo "no-stdin-flag";; esac
# A shell reading commands from a pipe is not interactive, positional args or not.
- name: "-s with positional args on a non-terminal is not interactive"
args: ["-s", "myarg"]
stdin: |
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
echo "one=[$1]"
# ...but -i forces it, and must not disturb the positional parameters.
- name: "-i -s with positional args is interactive"
ignore_stderr: true # bash echoes prompts and job-control warnings on a non-tty `-i` shell
args: ["-i", "-s", "myarg"]
stdin: |
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
echo "count=[$#] one=[$1]"
- name: "-si with positional args"
ignore_stderr: true
args: ["-si", "myarg"]
stdin: |
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
echo "count=[$#] one=[$1]"
- name: "-is with positional args"
ignore_stderr: true
args: ["-is", "myarg"]
stdin: |
case $- in *i*) echo "interactive";; *) echo "not-interactive";; esac
echo "count=[$#] one=[$1]"
#
# The tty-attached half of the matrix, and the one the piped cases above cannot reach:
# with `-s`, commands still come from stdin, so a terminal-attached shell is
# *interactive* -- trailing positional args notwithstanding. The `#expect:` directives
# are the assertion in these (stdout is too pty-dependent to diff).
#
# Being interactive is not cosmetic. It is what makes the shell source its rc file and
# expand aliases, so those are checked here too rather than taken on faith; the
# non-pty cases that follow pin the matching negatives.
#
- name: "-s with positional args is interactive on a terminal"
pty: true
incompatible_configs: ["sh"]
ignore_stdout: true # pty transcripts differ in echo and escape-sequence details
ignore_whitespace: true
args: ["-s", "myarg"]
# N.B. The markers must be *computed*, or the terminal's echo of the typed command
# line satisfies the expectation on its own and the case can never fail.
stdin: |
#expect-prompt
echo "FLAGS[${-//[!i]/}] ARG[$1]"
#send:Enter
#expect:FLAGS[i] ARG[myarg]
#expect-prompt
#send:Ctrl+D
- name: "-s with positional args loads the rc file on a terminal"
pty: true
incompatible_configs: ["sh"]
removed_default_args: ["--norc"] # `--norc` otherwise overrides `--rcfile`
ignore_stdout: true
ignore_whitespace: true
args: ["--rcfile", "testrc", "-s", "myarg"]
stdin: |
#expect-prompt
echo "RC[$RC_MARKER] ARG[$1]"
#send:Enter
#expect:RC[loaded] ARG[myarg]
#expect-prompt
aliascheck
#send:Enter
#expect:ALIAS-EXPANDED
#expect-prompt
#send:Ctrl+D
# The negatives, which need no terminal: the same `--rcfile` must be ignored whenever
# the shell is not interactive, so the rc variable stays unset and the alias undefined.
- name: "the rc file is not loaded when -s reads from a pipe"
removed_default_args: ["--norc"]
ignore_stderr: true # only the "command not found" wording differs
args: ["--rcfile", "testrc", "-s", "myarg"]
stdin: |
echo "RC[$RC_MARKER] ARG[$1]"
aliascheck
- name: "the rc file is not loaded for a -c command"
removed_default_args: ["--norc"]
args: ["--rcfile", "testrc", "-c", 'echo "RC[$RC_MARKER]"']
- name: "the rc file is not loaded for a named script"
removed_default_args: ["--norc"]
args: ["--rcfile", "testrc", "rccheck.sh"]
#
# A named script wins over stdin: the script runs, and whatever is left on stdin is
# data for the script, never commands for the shell.
#
- name: "a named script runs and stdin is not read as commands"
args: ["announce.sh", "a", "b"]
stdin: |
echo STDIN-SHOULD-NOT-RUN
- name: "-i with a named script still runs the script"
ignore_stderr: true
args: ["-i", "announce.sh"]
stdin: |
echo STDIN-SHOULD-NOT-RUN
- name: "a script sees stdin as data, not commands"
args: ["reader.sh"]
stdin: |
first-line
second-line
#
# `-c` wins over everything, including `-s` and a named script.
#
- name: "-c runs its command and stdin is not read as commands"
args: ["-c", "echo from-c"]
stdin: |
echo STDIN-SHOULD-NOT-RUN
- name: "-s followed by -c runs the -c command"
args: ["-s", "-c", "echo from-c"]
stdin: |
echo STDIN-SHOULD-NOT-RUN
- name: "-c with -i is interactive but still runs the -c command"
ignore_stderr: true
args: ["-i", "-c", 'echo from-c; case $- in *i*) echo interactive;; *) echo not-interactive;; esac']
stdin: |
echo STDIN-SHOULD-NOT-RUN
#
# `--` and `-` as end-of-options markers. brush consumes `--` but presently keeps `-`
# as an ordinary positional word, so the `-` cases are pinned as known failures --
# they document the gap and will flag the day it gets fixed.
#
- name: "-s with -- before positional args"
args: ["-s", "--", "myarg"]
stdin: |
echo "count=[$#] one=[$1] all=[$@]"
- name: "-s with a lone --"
args: ["-s", "--"]
stdin: |
echo "count=[$#] one=[$1] all=[$@]"
- name: "-s with a lone -"
known_failure: true
args: ["-s", "-"]
stdin: |
echo "count=[$#] one=[$1] all=[$@]"
- name: "-- before a script path"
args: ["--", "announce.sh"]
# Once a script is named, a later `--` is that script's argument, not the shell's.
- name: "-- before a script path with a later --"
args: ["--", "script.sh", "--", "x"]
- name: "- before a script path"
known_failure: true
ignore_stderr: true
args: ["-", "announce.sh"]
# After the `-c` command string, every remaining word is a positional parameter --
# even one that looks like an option.
- name: "-c with an option-like word as $0"
args: ["-c", 'echo "0=[$0] 1=[$1]"', "-s"]
- name: "-c with -i as $0"
ignore_stderr: true
args: ["-c", 'echo "0=[$0] 1=[$1]"', "-i"]
- name: "+o disables an option enabled with -o"
args: ["-o", "errexit", "+o", "errexit", "-c", 'false; echo "after false: $-"']
# Repeating a boolean option is harmless in bash.
- name: "repeated -s"
ignore_stderr: true
args: ["-s", "-s"]
stdin: |
echo "count=[$#] all=[$@]"
#
# The shell's own flags. Long options come first in each case: bash reads none after a
# short one.
#
- name: "--posix turns on the posix option"
args: ["--posix", "-c", "shopt -oq posix"]
- name: "--noediting runs a -c command"
args: ["--noediting", "-c", "echo hi"]
- name: "repeated --norc and --noprofile"
args: ["--norc", "--noprofile", "--norc", "-c", "echo hi"]
- name: "--init-file names the rc file of an interactive shell"
removed_default_args: ["--norc"]
ignore_stderr: true # bash warns that it has no job control
args: ["--init-file", "testrc", "-i", "-c", 'echo "RC[$RC_MARKER]"']
- name: "--rcfile names the rc file of an interactive -c shell"
removed_default_args: ["--norc"]
ignore_stderr: true # bash warns that it has no job control
args: ["--rcfile", "testrc", "-i", "-c", 'echo "RC[$RC_MARKER]"']
- name: "--rcfile with no file name"
ignore_stderr: true
args: ["--rcfile"]
- name: "--rcfile does not take an attached file name"
known_failure: true # brush accepts `--rcfile=FILE` and runs the command; bash rejects it (exit 2)
ignore_stderr: true
args: ["--rcfile=testrc", "-c", "echo hi"]
- name: "an abbreviated long option is rejected"
ignore_stderr: true
args: ["--nor", "-c", "echo hi"]
- name: "a value attached to a long switch is rejected"
ignore_stderr: true
args: ["--norc=1", "-c", "echo hi"]
- name: "an unknown long option is rejected"
ignore_stderr: true
args: ["--bogus", "-c", "echo hi"]
- name: "an unknown short option is rejected"
ignore_stderr: true
args: ["-Z", "-c", "echo hi"]
- name: "--version with an unknown long option is rejected"
ignore_stderr: true
args: ["--version", "--bogus"]
- name: "--version after the -c command is a positional"
args: ["-c", 'echo "0=[$0]"', "--version"]
- name: "--debug takes no value"
known_failure: true # brush's --debug takes an event class, so this fails with "`--debug` needs a value" (exit 2)
args: ["--debug", "-c", "echo hi"]
- name: "-o with an unknown option name"
known_failure: true # brush ignores the unknown name and runs the command; bash fails (exit 2)
ignore_stderr: true
args: ["-o", "nosuch", "-c", "echo after"]
- name: "-O with an unknown option name"
known_failure: true # brush ignores the unknown name and runs the command; bash fails (exit 2)
ignore_stderr: true
args: ["-O", "nosuch", "-c", "echo after"]
- name: "-o with no option name lists the options"
known_failure: true # brush fails with "`-o` needs a value" (exit 2)
args: ["-o"]
- name: "-O with no option name lists the shopt options"
known_failure: true # brush fails with "`-O` needs a value" (exit 2)
args: ["-O"]
- name: "-o followed by -c"
ignore_stderr: true
args: ["-o", "-c", "echo hi"]
- name: "-O followed by -c"
ignore_stderr: true
args: ["-O", "-c", "echo hi"]
- name: "-O takes its option name from the next word"
known_failure: true # brush takes the attached `nullglob` as the option name; bash takes none attached (exit 2)
ignore_stderr: true
args: ["-Onullglob", "-c", "shopt nullglob"]
- name: "+O disables extglob"
known_failure: true # brush turns extglob on as the shell starts, after the command line is applied
args: ["+O", "extglob", "-c", "shopt extglob"]
- name: "+e disables errexit enabled with -e"
known_failure: true # brush accepts only `+o` and `+O`, so `+e` is an unknown flag (exit 2)
args: ["-e", "+e", "-c", "false; echo after"]
- name: "-v prints input as it is read"
known_failure: true # brush accepts -v but prints nothing
args: ["-v", "-c", "echo hi"]
- name: "--verbose prints input as it is read"
known_failure: true # brush accepts --verbose but prints nothing
args: ["--verbose", "-c", "echo hi"]
# Single-letter `set` options that bash also takes on its command line.
- name: "-a on the command line"
known_failure: true # brush rejects -a as an unknown flag (exit 2)
args: ["-a", "-c", 'echo "$-"']
- name: "-b on the command line"
known_failure: true # brush rejects -b as an unknown flag (exit 2)
args: ["-b", "-c", 'echo "$-"']
- name: "-h on the command line"
known_failure: true # brush rejects -h as an unknown flag (exit 2)
args: ["-h", "-c", 'echo "$-"']
- name: "-k on the command line"
known_failure: true # brush rejects -k as an unknown flag (exit 2)
args: ["-k", "-c", 'echo "$-"']
- name: "-m on the command line"
known_failure: true # brush rejects -m as an unknown flag (exit 2)
ignore_stderr: true # bash warns that it has no job control
args: ["-m", "-c", 'echo "$-"']
- name: "-p on the command line"
known_failure: true # brush rejects -p as an unknown flag (exit 2)
args: ["-p", "-c", 'echo "$-"']
- name: "-r on the command line"
known_failure: true # brush rejects -r as an unknown flag (exit 2)
args: ["-r", "-c", 'echo "$-"']
- name: "-B on the command line"
known_failure: true # brush rejects -B as an unknown flag (exit 2)
args: ["-B", "-c", 'echo "$-"']
- name: "-D on the command line"
known_failure: true # brush rejects -D as an unknown flag (exit 2)
args: ["-D", "-c", 'echo $"hi"']
- name: "-E on the command line"
known_failure: true # brush rejects -E as an unknown flag (exit 2)
args: ["-E", "-c", 'echo "$-"']
- name: "-H on the command line"
known_failure: true # brush rejects -H as an unknown flag (exit 2)
args: ["-H", "-c", 'echo "$-"']
- name: "-P on the command line"
known_failure: true # brush rejects -P as an unknown flag (exit 2)
args: ["-P", "-c", 'echo "$-"']
- name: "-T on the command line"
known_failure: true # brush rejects -T as an unknown flag (exit 2)
args: ["-T", "-c", 'echo "$-"']
- name: "a child shell inherits open file descriptors"
known_failure: true # without --inherit-fd, brush gives its shell only 0-2, so `>&3` fails with "bad file descriptor"
incompatible_platforms: ["wasi", "windows"] # spawns a child shell; fd 3 is a Unix notion
stdin: |
"$BASH" -c 'echo via3 >&3' 3>&1