In bash, what happens when a function whose body uses $1 and $2 is called with only one argument, and how do you make that function fail loudly instead?
answer
- no signature, no arity check
- the missing one is simply empty
- a strict-mode flag is a backstop, not the contract
- unset and empty are different states
basics
~20 sNothing happens: bash checks no arity, so $2 expands to the empty string and the function runs on with a blank value. Under set -u referencing $2 aborts as an unbound variable; otherwise test $# yourself or use ${1:?message}.
solid answer
~50 sBash functions have no signature and no arity checking. Call a two-argument function with one argument and `$2` simply expands to the empty string — the body carries on, which is how you get `rm -rf "$target/"` deleting the wrong thing or a `curl` to a URL with a missing path segment. Extra arguments are equally silent: they sit unused in `"$@"` and are still counted by `$#`. `set -u` helps, because referencing an unset positional parameter is an unbound-variable error that aborts a non-interactive shell, but the message names `$2` rather than what the caller did wrong. The explicit form is better: check `$#` at the top of the function and print a usage line to stderr with a non-zero status, or use `${1:?src required}` when you want the shell to abort immediately with a named message.
code
bash · 14 lines#!/usr/bin/env bash
set -u
copy_to() {
if (( $# != 2 )); then
printf 'usage: copy_to SRC DST\n' >&2
return 2
fi
printf 'copying %s -> %s\n' "$1" "$2"
}
copy_to /etc/hosts # usage on stderr, status 2
echo "status=$?"
copy_to /etc/hosts /tmp/h # runsgo deeper
Know that bash does not check how many arguments a function was given, and that a missing $2 is simply an empty string rather than an error.
Explain the mechanics you would add: a $# test at the top of the function, ${1:?message} for a required value, and what set -u changes about referencing an unset positional parameter.
Show the production judgment — the empty value that reached rm or cp, why set -u is a backstop rather than the contract, the difference between unset and empty, usage on stderr and a distinct non-zero status.
Own the convention across a shell codebase: every exported library function documents and enforces its arity, diagnostics go to stderr with reserved exit codes, and the difference between a caller error and a runtime failure is visible in the status a script returns to its scheduler.
## Bash functions have no signature ```bash copy_to() { cp -- "$1" "$2"; } copy_to /etc/hosts ``` Nothing about that call is an error to bash. `$1` is `/etc/hosts`, `$2` is unset and expands to the empty string, and `cp` is invoked as `cp -- /etc/hosts ''`. The failure surfaces one layer down, in a message from `cp`, or — much worse — does not surface at all when the empty value is harmless-looking but wrong: ```bash cleanup() { rm -rf -- "$1"/cache; } # called with nothing: rm -rf -- /cache ``` That class of bug is the reason interviewers ask this. There is no type system, no arity check and no warning; a function's calling contract exists only in its comments and in whatever you enforce yourself. The same looseness applies in the other direction. Call a two-argument function with five and the extra three are silently accepted: they are in `"$@"`, `$#` reports 5, and the body that only reads `$1` and `$2` ignores them. A typo that adds a stray word therefore does not fail either. ## What `set -u` does and does not catch With `set -u` (equivalently `set -o nounset`), expanding an unset variable is an error, and that includes positional parameters: ```bash #!/usr/bin/env bash set -u copy_to() { cp -- "$1" "$2"; } copy_to /etc/hosts # bash: $2: unbound variable -> script exits ``` This is a genuine safety net and a good reason to run scripts with `set -u`. Its limits are worth stating in an interview: - The message identifies the *parameter*, not the call. `"$2": unbound variable` tells a user nothing about which command they typed wrong. - It only fires when the body actually expands the missing parameter. A body that reads `$2` only in a branch that this run does not take will still proceed with the wrong contract. - It does not fire for an argument passed as an empty string — `copy_to /etc/hosts ""` sets `$2` to `''`, which is *set*. Missing and empty are different states. - It does nothing about extra arguments. ## Explicit guards The convention that survives contact with real users is a guard at the top of the function, a usage line on stderr, and a non-zero status: ```bash copy_to() { if (( $# != 2 )); then printf 'usage: copy_to SRC DST\n' >&2 return 2 fi cp -- "$1" "$2" } ``` Points to note. `(( $# != 2 ))` is an arithmetic test and reads better than `[ "$#" -ne 2 ]`, though both work. The usage message goes to stderr so it never contaminates a caller that is capturing the function's stdout. The status is non-zero and distinguishable from the wrapped command's own failures — many scripts reserve 2 for usage errors, following the convention of common CLI tools. For a single mandatory argument, the `${1:?word}` expansion is a compact alternative: ```bash deploy() { : "${1:?deploy: environment name required}"; echo "deploying to $1"; } ``` If `$1` is unset or empty the shell writes `bash: 1: deploy: environment name required` to stderr and, because the shell is non-interactive, exits. That is exactly the behaviour you want in a small script and exactly what you do not want in a library function that a caller expected to be able to test — it kills the whole script rather than returning a status. Choose it accordingly. ## Optional arguments and defaults A function that genuinely has optional parameters should say so with a default rather than leaving the empty string to propagate: ```bash retry() { attempts=${1:-3} shift || true ... } ``` `${1:-3}` supplies `3` when `$1` is unset *or* empty; `${1-3}` supplies it only when unset. Using the wrong one is a common source of "the empty string I deliberately passed was ignored". ## The judgment an interviewer is looking for At senior level the expected answer is not just "it expands to empty". It is: bash gives you no contract, so a function that other people call needs an explicit one — check `$#` for a fixed arity, use `${var:?}` for a required single value, distinguish unset from empty, print usage to stderr, return a distinct status, and run the script under `set -u` as a backstop rather than as the primary mechanism.
- Does set -u catch an argument that was passed as an empty string?No. `set -u` fires only for parameters that are *unset*; an explicitly passed `""` is set to an empty value and expands silently. If empty is invalid for your function, test it — `[[ -n $1 ]]` or `${1:?...}`, which treats unset and empty the same way.
- What happens when you call a bash function with more arguments than its body uses?They are accepted silently. The extras are in `"$@"`, `$#` counts them, and a body that reads only `$1` and `$2` ignores the rest. If a stray word must be an error, test `$#` for an exact count rather than a minimum.
- Why print a usage message to stderr rather than stdout?Because callers routinely capture a function's stdout, and a usage line written there would be swallowed into the captured value or corrupt downstream parsing. Diagnostics belong on stderr so they reach the operator regardless of how the function is being called.
- When is ${1:?message} the wrong choice inside a library function?When the caller is expected to handle the failure. In a non-interactive shell `${1:?...}` aborts the whole script, so a library function that should return a status to its caller must use an explicit `$#` check and `return` instead. Reserve the `:?` form for small scripts where dying immediately is the desired behaviour.
saying these in an interview costs you the question
- Says bash reports an error when arguments are missing
- Believes set -u catches an explicitly passed empty string
- Thinks extra arguments cause the call to fail
- Treats unset and empty positional parameters as the same state
- Prints usage messages to stdout inside a function