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