skip to content

Defining Functions and Arguments

A bash function receives its arguments exactly the way a script does — positionally, through $1, $# and "$@" — so forwarding them onward means quoting "$@" precisely. A common follow-up is that a function can shadow a real command, and that `command` is how you bypass that.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

5

Inside a bash function, what do $1, $# and $0 refer to, and why must the tenth argument be written as ${10}?

level: juniorimportance: must knowfreq 70%

answer

  1. arguments arrive positionally, never named
  2. the script's list is saved and restored
  3. one special parameter refuses to change
  4. single digit only, then braces

basics

~20 s

A bash function receives its own positional parameters: $1, $2 … are the function's arguments and $# is their count, shadowing the script's for the duration of the call. $0 is the exception — it still names the script. Write ${10}, because $10 parses as $1 followed by 0.

solid answer

~50 s

Bash functions have no named parameters. When you call one, bash swaps in a fresh set of positional parameters, so inside the body `$1` is the function's first argument, `$#` is how many arguments the function got, and `"$@"` is the whole list. The script's own arguments are saved and restored when the function returns, so a function cannot see them unless you pass or save them. `$0` is deliberately not part of that set — it is fixed when the shell starts and keeps naming the script, which is why usage messages can print `$0` from anywhere. From the tenth argument on you need braces: `$10` is parsed as `${1}` followed by a literal `0`, so you write `${10}`. In practice you rarely index that far — iterate with `for a in "$@"` or consume with `shift`.

code

bash · 12 lines
bash
#!/usr/bin/env bash
show() {
  echo "count=$#"
  echo "first=$1"
  echo "tenth=${10}"
  echo "dollar0=$0"
  shift 2
  echo "after shift: count=$# first=$1"
}

show a b c d e f g h i j
echo "script still sees $# argument(s)"

go deeper

for a junior

Be ready to say plainly that bash functions take arguments positionally as $1, $2 and so on, that $# counts them, and that you loop over them with "$@".

for a middle

Explain that bash saves and restores the caller's positional parameters around a call, that $0 is excluded from that swap, and that $10 parses as $1 followed by a literal 0.

for a senior

Show the consequences in real scripts: a helper cannot see the script's command line unless you pass it, usage text prints $0 so it stays correct at any depth, and functions must validate $# because bash checks no arity.

for a principal

Own the interface question — a bash function taking many positional arguments is a design smell; argue for few positional arguments, explicit environment or an options object built by the caller, and a documented calling contract in a shared library.

## Bash functions have no parameter list A bash function is declared with an empty pair of parentheses — they are punctuation, not a signature: ```bash greet() { echo "hello, $1" } greet world ``` You never write parameter names between the parentheses. Arguments arrive exactly the way a script's arguments arrive: **positionally**, through the special parameters `$1`, `$2`, …, plus `$#` (how many), `$@` and `$*` (all of them). There are two spellings of a definition. `name() { ...; }` is the POSIX form and works in every shell. `function name { ...; }` is a ksh/bash extension; bash treats it the same way, but it is not portable, so style guides and ShellCheck prefer the parenthesis form. Either way the body is a compound command, and the closing brace needs a `;` or a newline in front of it — `{ echo hi }` is a syntax error, `{ echo hi; }` is not. A definition is executed, not merely parsed: the function exists only after the shell has run the line that defines it. That is why library files are sourced near the top and why many scripts end with a single `main "$@"` call after all definitions. ## Calling swaps the positional parameters When a function is called, bash saves the current positional parameters and installs the function's arguments in their place. Inside the body: - `$1` … `$9` are the function's own arguments; - `$#` is the number the function received — it never counts the function name or the script name; - `"$@"` is the full list, one word per argument; - `shift` consumes the function's arguments only, leaving the caller's untouched. When the function returns, the previous set comes back. This is why a helper cannot peek at the script's command line by accident — a genuine safety property, but also a surprise for people expecting Python-style closures over `sys.argv`. If a helper truly needs the script's original arguments, capture them at the top level and pass them explicitly: ```bash script_args=("$@") # at top level main() { printf 'script got %d args\n' "${#script_args[@]}"; } main "$@" ``` ## $0 is the exception `$0` is set once when the shell starts — to the path the script was invoked with — and function calls do not touch it. Inside a function, `$0` is still `./deploy.sh`, not the function's name. The function's own name lives in `${FUNCNAME[0]}`. Confusing the two is a classic interview slip, and it matters in practice because usage messages usually print `$0` and should keep printing the script name no matter how deep the call is. ## Why ${10} needs braces The shell parses `$` followed by a *single* digit for positional parameters. So `$10` is `${1}` immediately followed by the character `0`: call a function with ten arguments and `echo "$10"` prints the first argument with a `0` glued onto it — quietly wrong, never an error. From ten on, brace it: `${10}`, `${11}`, `${12}`. In real scripts this almost never comes up, because a function that takes ten positional arguments is already a design smell. The idiomatic handling is to iterate or consume: ```bash log_all() { for line in "$@"; do printf '%s\n' "$line"; done; } drop_first() { shift; printf '[%s]' "$@"; echo; } ``` A bare `for line; do ... done` with no `in` list iterates `"$@"` — the same thing, written shorter. ## Arity is not checked Bash never validates how many arguments a function got. Extra ones simply sit in `"$@"` unused; missing ones expand to the empty string. That is why defensive scripts test `$#` explicitly, or use `${1:?message}` to abort when a required argument was not supplied. The interviewer's follow-up is usually exactly that: "so what happens if I call it with nothing?"

  • Does a shift inside a function affect the script's positional parameters?
    No. The function has its own copy of the positional parameters, so `shift` consumes only the function's arguments. When the function returns, the caller's list is restored exactly as it was. That is what makes `shift`-driven helpers safe to call repeatedly from the same script.
  • What is the difference between `name() { ...; }` and `function name { ...; }` in bash?
    In bash they behave the same. The parenthesis form is POSIX and runs in dash, ash and other shells; the `function` keyword is a ksh/bash extension and is not portable, so ShellCheck and most style guides prefer the parenthesis form. Mixing them — `function name() {}` — is the least portable spelling of all.
  • Inside a function, how do you get the name of the function you are in?
    Use `${FUNCNAME[0]}`. `$0` is not it — `$0` stays fixed at the script's invocation path for the whole run, which is exactly what you want in a usage message but not what you want when logging which helper failed.

Calling a bash function is like handing someone a numbered stack of index cards: they read card 1, card 2, and count how many they got — but they never see the stack you were holding, which you get back when they hand yours over.

saying these in an interview costs you the question

  • Says $0 becomes the function name inside a function
  • Thinks $1 inside a function is the script's first argument
  • Believes $10 expands to the tenth argument
  • Says $# includes the script or function name in its count
  • Claims shift inside a function consumes the caller's arguments

context

open as a page

In a bash wrapper function that passes its arguments on to another command, what is the difference between forwarding them as "$@", as "$*", and as unquoted $@?

level: middleimportance: must knowfreq 72%

basics

~20 s

"$@" expands to one word per argument, preserving spaces and empty arguments — it is the only correct way to forward. "$*" joins everything into a single word separated by IFS's first character. Unquoted $@ re-splits each argument on whitespace and globs the pieces.

open as a page

In a bash script someone defines `ls() { ls -l "$@"; }` so listings are always long-format, and every call to ls then recurses until the shell dies. Why does that happen, and how do you call the real ls from inside the wrapper?

level: middleimportance: should knowfreq 45%

basics

~20 s

Bash resolves a plain command name to a shell function before it ever searches PATH, so ls inside the body finds the function again and it calls itself forever. Prefix the inner call with command — command ls -l "$@" — which skips function lookup.

open as a page

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?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Nothing 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}.

open as a page

Why does an alias defined near the top of a bash script usually fail to work when it is used further down in the same script, and what should you write instead?

level: middleimportance: nice to knowfreq 38%

basics

~20 s

Bash expands aliases only in interactive shells unless shopt -s expand_aliases is set, and expansion happens while a line is parsed, so an alias may not be visible to code the shell has already read. Use a function, which is resolved when it runs.

open as a page