skip to content

Functions

Functions are how a script stops being a pile of commands: named blocks that take positional arguments, can declare local variables, and communicate through exit status and stdout. Interviewers use them to check whether you have internalized that bash has no return values — only statuses and output.

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

questions

19

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 bash, a variable assigned inside a function is still set after the function returns and can overwrite a caller's variable of the same name. Why does bash behave this way, and what makes a variable exist only for the duration of the function?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Bash variables are global by default, so an assignment inside a function writes into the one shell-wide namespace and survives the return. Declaring the name with local (or declare) inside the function creates a temporary copy that disappears when the function returns.

open as a page

In bash, a function ends with `return 42`. What can the caller actually do with that 42, and how would you get a non-numeric result such as a hostname out of a function instead?

level: juniorimportance: must knowfreq 68%

basics

~20 s

In bash, return only sets the function's exit status — a number 0-255 the caller reads in $? — never a value. To hand back real data, print it with echo or printf and capture it with result=$(func).

open as a page

In bash, what is the difference between loading a script with `source setup.sh` (or `. setup.sh`) and running it as `./setup.sh`, and why does only one of them change the variables in your current shell?

level: juniorimportance: must knowfreq 78%

basics

~20 s

source (and its POSIX synonym .) reads the file and runs its commands in the shell you are already in, so variables, functions and cd persist. ./setup.sh starts a separate shell that gets a copy of the environment and discards everything when it exits.

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

When a bash script needs to source a helper file that lives next to it, why is `${BASH_SOURCE[0]}` a better basis than `$0` for computing the file's own directory, and what does the usual `cd`-and-`pwd` wrapper add?

level: middleimportance: must knowfreq 55%

basics

~20 s

$0 is the name the outermost command was invoked with, so inside a sourced library it names the caller, not the library. ${BASH_SOURCE[0]} is always the file whose code is running. Wrapping it in cd and pwd turns a possibly relative path into an absolute one.

open as a page

A bash script runs under `set -e`. Inside a function, the line `local out=$(some-command)` lets the script continue even when some-command exits non-zero, while the same assignment written without `local` aborts. Why does the status disappear, and how do you write it so the failure is caught?

level: seniorimportance: must knowfreq 44%

basics

~20 s

local is a builtin command, so the line's exit status is local's own status — almost always 0 — and the command substitution's failure is thrown away. Split it into two lines: declare with local on one line, assign on the next, so the assignment carries the real status.

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 a bash script, an outer function declares `local retries=3` and then calls a helper function that reads `$retries` — and the helper sees the value 3 even though it never declared or received it. Which scoping rule does bash use to allow this, and how does it differ from the lexical scoping of C, Java or Python?

level: middleimportance: should knowfreq 46%

basics

~20 s

Bash uses dynamic scoping: a local variable is visible to the function that declared it and to everything it calls, for as long as that call is on the stack. Lexical scoping, by contrast, resolves names from where code is written, not from who called it.

open as a page

A bash function `get_version` runs `echo "looking up version..."` and then `echo "1.4.2"`, and callers use `v=$(get_version)`. What does `$v` actually contain, and how should a function that both logs and produces a value be written?

level: middleimportance: should knowfreq 55%

basics

~20 s

$v holds both lines — "looking up version..." and "1.4.2" — because a capture takes everything the function wrote to standard output. Keep stdout for the return value only and send every log line to standard error with >&2.

open as a page

A bash helper library defines `die() { echo "$1" >&2; exit 1; }`. A colleague sources the library into their interactive shell to try it out, calls `die`, and their terminal window closes. Explain what happened and how `return` behaves differently in a sourced file.

level: middleimportance: should knowfreq 42%

basics

~20 s

Sourced code runs in the caller's own shell, so exit terminates that shell — an interactive session closes, and a calling script stops mid-run. return only leaves the current function, or stops the sourcing early and hands a status back to source. Libraries should return, not exit.

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

A shared bash library defines `require_tool() { command -v "$1" >/dev/null || { echo "missing: $1" >&2; exit 1; }; }`. What goes wrong when another script sources that library and calls the function, and what should the function do instead?

level: seniorimportance: should knowfreq 42%

basics

~20 s

exit terminates the whole shell running the function, not just the function, so the caller dies at the call site and can never fall back or clean up. A library function should return 1 after writing the reason to standard error, leaving the decision to the caller.

open as a page

Your bash deploy script loads its settings with `source /etc/deploy.conf`. What does that actually do to the contents of that file, and what goes wrong when the file is writable by other users or is edited as if it were a plain .env file?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Sourcing a config file executes every line as bash in the running shell. Anyone who can write that file can run arbitrary commands with the script's privileges, and ordinary .env conventions such as unquoted values with spaces or inline comments do not mean what the author intended.

open as a page

You maintain a growing set of shared bash function libraries, and an incident traced back to a helper whose undeclared variable overwrote a caller's value mid-loop. Given that bash variables are global by default and its scoping is dynamic, what conventions would you mandate to stop function state from colliding, and what would tell you the codebase has outgrown bash?

level: principalimportance: should knowfreq 28%

basics

~20 s

Mandate that every function declares all its working variables local on entry, reserve a small set of prefixed read-only globals for configuration, pass data as explicit arguments, and enforce it with linting and review. Move off bash when the logic needs real data structures, error handling or testability.

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

Bash functions may call themselves. What limits how deep a recursive bash function can go, what does bash's FUNCNEST variable do, and how can such a function tell how deep it currently is?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

By default bash imposes no recursion limit, so a runaway recursive function grows the call stack until the shell exhausts memory and dies. Setting FUNCNEST to a positive number caps nesting depth, and the FUNCNAME array records the current call stack.

open as a page

In a bash project, two helper files each `source common.sh`, so common.sh is loaded twice in the same shell. What can go wrong, and what does an include guard look like in bash?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Bash has no import deduplication: every source re-executes the whole file. Re-running it can abort a strict script when readonly variables are reassigned, duplicate PATH entries, reset counters, replace traps and repeat expensive setup. A guard variable checked at the top with an early return prevents it.

open as a page

You are setting the conventions for a shared bash function library used by a team's deploy scripts. How should functions hand results back to callers, and what are the tradeoffs between capturing stdout, using globals, and nameref out-parameters?

level: principalimportance: nice to knowfreq 26%

basics

~20 s

Standardise on three channels: exit status for success or failure, standard output for the value, standard error for humans. Use nameref out-parameters (bash 4.3+) or documented globals only where capturing output is impossible or too slow.

open as a page