Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 7 – Functions and script structure

Functions: arguments, local and return status

Define Bash functions with the POSIX and Bash forms, keep variables local with dynamic scoping, use return to set a status and write functions as conditions.

  • Beginner
  • 10 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 10 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define and call functions with arguments using both the POSIX and the Bash syntax
  • Use local to limit a variable to a function and explain dynamic scoping
  • Set a function's exit status with return and use the function as a condition

Before you start

What you’ll learn · Before you start

What you will learn

  • Define and call functions with arguments using both the POSIX and the Bash syntax
  • Use local to limit a variable to a function and explain dynamic scoping
  • Set a function's exit status with return and use the function as a condition

Before you start

On this page

A script that uploads a report to three servers can repeat the same 12 upload lines three times, 36 lines that must all change together, or define them once as a shell function: A named group of commands defined with name() { …; } or function name { …; }, called like a command. It runs in the current shell, shares its variables unless local is used, and returns a status with return. and call it three times with the server name as its argument. A fix to the upload then goes in one place, and every call picks it up.

A Bash function has two ways to be defined, its own arguments, local variables that stay out of the caller’s way and a status set with return, which lets it stand directly in an if. Handing data back, rather than a status, is the subject of the next lesson.

The two definition forms and the body’s compound-command rule

Bash has two forms for defining a function. The POSIX form, name() { commands; }, works in Bash, dash and every POSIX-compliant shell. The Bash form, function name { commands; }, works in Bash and ksh but not in dash. The session defines a greet function both ways and shows what dash does with the Bash form:

POSIX and Bash function forms, dash syntax error, and the body rules Python · forms_session.py
"""POSIX and Bash function forms, dash's syntax error, the two body rules, and a call before the definition."""
from session import run

run("""
# POSIX form in Bash
greet() { echo "hi"; }
greet

# Bash form in Bash
function greet2 { echo "hi"; }
greet2

# POSIX form in dash
dash -c 'greet() { echo "hi"; }; greet'

# Bash form in dash: syntax error
!dash -c 'function greet { echo "hi"; }'

# Body rules: a space after { and a ; before }
!bash -c 'greet() {echo "hi"; }'
!bash -c 'greet() { echo "hi" }'

# A call before the definition has run
!later
later() { echo "later is defined now"; }
later
""")

Output

# POSIX form in Bash
$ greet() { echo "hi"; }
$ greet
hi
# Bash form in Bash
$ function greet2 { echo "hi"; }
$ greet2
hi
# POSIX form in dash
$ dash -c 'greet() { echo "hi"; }; greet'
hi
# Bash form in dash: syntax error
$ dash -c 'function greet { echo "hi"; }'
dash: 1: Syntax error: "}" unexpected
[exit status 2]
# Body rules: a space after { and a ; before }
$ bash -c 'greet() {echo "hi"; }'
bash: -c: line 1: syntax error near unexpected token `{echo'
bash: -c: line 1: `greet() {echo "hi"; }'
[exit status 2]
$ bash -c 'greet() { echo "hi" }'
bash: -c: line 2: syntax error: unexpected end of file from `{' command on line 1
[exit status 2]
# A call before the definition has run
$ later
bash: later: command not found
[exit status 127]
$ later() { echo "later is defined now"; }
$ later
later is defined now

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 forms_session.py

The POSIX form printed “hi” in Bash and in dash. The Bash form worked in Bash too, but dash stopped at its closing brace with Syntax error: "}" unexpected and status 2; ShellCheck reports the function keyword as SC2113 in a #!/bin/sh script for this reason. The body is a compound command, a group in braces, and the braces are reserved words, so two rules hold in both forms: a space or a newline must follow {, and a ;, an & or a newline must come before }. The two bash -c lines broke one rule each. {echo is one word, not a brace, so Bash reported an unexpected token; without the ;, Bash read the } as one more argument to echo and reached the end of the input still looking for the closing brace.

Write the POSIX form unless the script needs Bash anyway

name() { …; } runs in every shell. The function keyword adds nothing in Bash, and it stops a script that runs under dash, or under a /bin/sh that is not Bash.

A function exists only once its definition has run. The last lines of the session called later before its definition: Bash answered “command not found” with status 127, as it does for a program that is not installed, and the same call worked once the definition had run. That is why a script defines its functions first and calls them below.

Checkpoint

Check: The two definition forms

  1. Question 1 of 2 Which function form works in dash?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: greet() { echo hi; }

    ShellCheck flags the function keyword as SC2113 in sh scripts for this reason.

  2. Question 2 of 2 greet runs before its definition has been reached. What status does Bash give?

    Type a number.

    Show the answer: Question 2 of 2

    Answer: 127

    A function must be defined before it is called. 127 is the status Bash uses for command not found.

Arguments inside a function are its own positional parameters

Inside a function the arguments are the function’s own positional parameter: An argument of a script or function, read by its position as $1, $2 and from the tenth on ${10}; $# counts them and "$@" passes them all on. More in “Positional and special parameters”: $1, $2 and so on, with $# counting them and "$@" holding them all. The caller’s arguments are not visible inside the function; they come back after the function returns. The special parameter $0, though, stays the script’s name, and FUNCNAME[0] gives the function’s name. The positional-and-special-parameters lesson covered "$@" and shift; the session shows how a function uses them:

Positional parameters inside and outside a function, and FUNCNAME Python · args_session.py
"""Positional parameters inside and outside a function, and FUNCNAME."""
from session import run

run("""
# Outside any function
echo "outside: \\$# = $#, \\$0 = $0"

# Inside a function
show() { echo "inside: \\$# = $#, \\$@ = $@, \\$0 = $0, FUNCNAME = ${FUNCNAME[0]}"; }
show a b c

# After the function returns
echo "after: \\$# = $#"

# Passing all arguments on with "$@"
wrapper() { echo "wrapper received $#, calling show"; show "$@"; }
wrapper a b c
""")

Output

# Outside any function
$ echo "outside: \$# = $#, \$0 = $0"
outside: $# = 0, $0 = bash
# Inside a function
$ show() { echo "inside: \$# = $#, \$@ = $@, \$0 = $0, FUNCNAME = ${FUNCNAME[0]}"; }
$ show a b c
inside: $# = 3, $@ = a b c, $0 = bash, FUNCNAME = show
# After the function returns
$ echo "after: \$# = $#"
after: $# = 0
# Passing all arguments on with "$@"
$ wrapper() { echo "wrapper received $#, calling show"; show "$@"; }
$ wrapper a b c
wrapper received 3, calling show
inside: $# = 3, $@ = a b c, $0 = bash, FUNCNAME = show

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 args_session.py

Inside show, $# was 3 and "$@" held a b c; outside, they were 0 and empty. $0 stayed bash in both places (the session is an interactive shell, whose $0 is bash; in a script both places give the script’s name), while FUNCNAME[0] gave show inside the function. The wrapper function passed all its arguments on to show with "$@", so show received the same three words the caller gave wrapper.

Key idea

Inside a function, $1 to $# and "$@" are the function’s own arguments; $0 stays the script’s name and FUNCNAME[0] gives the function’s name.

Checkpoint

Check: Arguments inside a function

  1. Question 1 of 2 show() { echo "$#"; }; show a b c. What does show print?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 3

    Arguments passed to a function become its own positional parameters for the duration of the call.

  2. Question 2 of 2 Inside a function, what does FUNCNAME[0] hold?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The function's own name

    The session's show function printed show for FUNCNAME[0], also when wrapper called it.

local limits a variable to the function and the functions it calls

Without local, a variable set inside a function is global: it overwrites any variable of the same name and stays after the function returns. With local, it becomes a local variable: A variable declared with local inside a function, visible only to that function and the functions it calls. It shadows any global of the same name until the function returns. that exists only in that function and the functions it calls, and any global of the same name reappears unchanged when the function ends. The session shows both cases and what happens when a called function looks for a local variable:

Global without local, local with local, and dynamic scoping Python · scope_session.py
"""Global without local, local with local, and dynamic scoping."""
from session import run

run("""
# Without local: global is overwritten
x=global
set_global() { x=inside; }
set_global
echo "after set_global: x=$x"

# With local: global is preserved
x=global
set_local() { local x=inside; echo "inside set_local: x=$x"; }
set_local
echo "after set_local: x=$x"

# Dynamic scoping: inner sees outer's local
x=global
inner() { echo "inner sees: x=$x"; }
outer() { local x=outer; inner; }
outer
echo "after outer: x=$x"
""")

Output

# Without local: global is overwritten
$ x=global
$ set_global() { x=inside; }
$ set_global
$ echo "after set_global: x=$x"
after set_global: x=inside
# With local: global is preserved
$ x=global
$ set_local() { local x=inside; echo "inside set_local: x=$x"; }
$ set_local
inside set_local: x=inside
$ echo "after set_local: x=$x"
after set_local: x=global
# Dynamic scoping: inner sees outer's local
$ x=global
$ inner() { echo "inner sees: x=$x"; }
$ outer() { local x=outer; inner; }
$ outer
inner sees: x=outer
$ echo "after outer: x=$x"
after outer: x=global

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope_session.py

In the first block, set_global set x without local, so it overwrote the global x and the change persisted. In the second block, set_local declared local x=inside, so the global stayed global after the call.

The third block shows dynamic scoping: A scoping rule in which a function sees the variables of its caller, not only its own locals. In Bash, a called function can read and change its caller's local variables unless it declares its own local.: outer declared local x=outer, then called inner, which printed x without declaring its own. Bash looked up the call chain, found outer’s local x and printed outer. Step through the calls to see which x each function sees:

Interactive · step by step Dynamic scoping through outer and inner Step through the calls to see which x each function sees.
Dynamic scoping through outer and inner, step 6 of 6. Showing global scope (x=global (visible again)), outer (local x=outer), inner (sees x=outer). In focus: global scope.callscallsglobal scopex=global (visible again)outerlocal x=outerinnersees x=outer

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Global scope starts. The script sets x=global at the top level, before any function call.
  2. outer is called. The script calls outer. Inside outer, local x=outer shadows the global x.
  3. outer calls inner. outer calls inner. inner has no local x, so it looks up the call chain.
  4. inner finds outer's x. Bash finds x=outer from outer, prints "outer" and returns. The global x=global is still shadowed.
  5. inner returns to outer. inner finishes; control returns to outer. outer's local x is still in scope.
  6. outer returns. outer finishes; its local x goes away. The global x=global is visible again.

When inner ran, x=outer from outer was in scope because outer called inner. The global x=global was shadowed. After outer returned, the global reappeared.

local is not POSIX, but dash supports it

POSIX does not require local, but dash has it and Debian Policy allows it in /bin/sh scripts: “local to create a scoped variable must be supported.”

Checkpoint

Check: local and dynamic scoping

  1. Question 1 of 2 x=global; f() { x=inside; }; f; echo "$x". What is printed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: inside

    The session's set_global showed the same behaviour: without local the change persists.

  2. Question 2 of 2 outer() { local x=outer; inner; }; inner() { echo "$x"; }; x=global; outer. What does inner print?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: outer

    The StepThrough showed inner seeing outer's local x=outer, not the global x=global.

return sets the status and ends the function

A function’s exit status: The number from 0 to 255 every command ends with, kept by the shell in $?. 0 means success; Bash uses 126 for found but not executable and 127 for not found. More in “Your first script: shebang, chmod and running it” is the status of the last command its body ran, unless the function calls return. return with no argument returns that same status; return N returns N, which must be 0 to 255. If N is outside that range, Bash keeps only the low eight bits, so return 256 gives 0 and return 300 gives 44. return never prints anything: it only sets a status and ends the function. The session shows all these rules:

Return status without return, with return, and out of range Python · return_session.py
"""Return status without return, with return, out of range, with no argument, and a word instead of a number."""
from session import run

run("""
# Status of the last command
last_command() { false; true; }
last_command
echo "last_command status: $?"

# Explicit return status
explicit_status() { return 42; }
!explicit_status
echo "explicit_status status: $?"

# Out of range: 300 mod 256 = 44
out_of_range() { return 300; }
!out_of_range
echo "out_of_range status: $?"

# 256 mod 256 = 0
wrap_zero() { return 256; }
wrap_zero
echo "wrap_zero status: $?"

# return with no argument passes on the last command's status
printf 'apple\\nbanana\\n' > list.txt
found() { grep -q "$1" list.txt || return; echo "found $1"; }
found apple
!found cherry

# return takes a number, never text
not_text() { return hello; }
!not_text
""")

Output

# Status of the last command
$ last_command() { false; true; }
$ last_command
$ echo "last_command status: $?"
last_command status: 0
# Explicit return status
$ explicit_status() { return 42; }
$ explicit_status
[exit status 42]
$ echo "explicit_status status: $?"
explicit_status status: 42
# Out of range: 300 mod 256 = 44
$ out_of_range() { return 300; }
$ out_of_range
[exit status 44]
$ echo "out_of_range status: $?"
out_of_range status: 44
# 256 mod 256 = 0
$ wrap_zero() { return 256; }
$ wrap_zero
$ echo "wrap_zero status: $?"
wrap_zero status: 0
# return with no argument passes on the last command's status
$ printf 'apple\nbanana\n' > list.txt
$ found() { grep -q "$1" list.txt || return; echo "found $1"; }
$ found apple
found apple
$ found cherry
[exit status 1]
# return takes a number, never text
$ not_text() { return hello; }
$ not_text
bash: return: hello: numeric argument required
[exit status 2]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 return_session.py

last_command ran false and then true, so its status was that of true, 0. explicit_status returned 42, which the caller read from $?. out_of_range returned 300 and the caller saw 44, the remainder of 300 divided by 256; wrap_zero returned 256 and the caller saw 0, a success. found shows return with no argument: for cherry, grep -q failed with status 1, and || return handed that 1 to the caller without printing anything. not_text tried to return a word, and Bash refused with “numeric argument required” and status 2.

return carries a status, never text

A function hands text back by printing it with echo or printf, and the caller captures it with $(...). The next lesson covers that channel and the others.

Checkpoint

Check: return and function status

  1. Question 1 of 2 f() { return 300; }; f; echo $?. What is printed?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 44

    Bash keeps only the low eight bits: 300 modulo 256 is 44.

  2. Question 2 of 2 f() { return 256; }; if f; then echo ok; else echo failed; fi. What is printed?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: ok

    The session's wrap_zero returned 256 and the caller saw 0, so an if treats such a function as a success.

A function as a condition and recursion with FUNCNEST

A function’s status makes it work as a condition. is_even uses arithmetic evaluation: (( $1 % 2 == 0 )) returns 0 when the remainder is zero, 1 otherwise. The session uses it directly in an if:

A function as a condition and recursion with FUNCNEST Python · condition_session.py
"""A function as a condition, a deliberate recursion, and FUNCNEST stopping a runaway one in a child script."""
from session import run, write

write("deep.sh", """FUNCNEST=5
deep() { echo "depth $1"; deep $(( $1 + 1 )); }
deep 1
echo "next line runs, status $?"
""")

run("""
# Function as a condition
is_even() { (( $1 % 2 == 0 )); }
if is_even 4; then echo "4 is even"; else echo "4 is odd"; fi
if is_even 7; then echo "7 is even"; else echo "7 is odd"; fi

# Recursion: factorial
fact() { if (( $1 <= 1 )); then echo 1; else echo $(( $1 * $(fact $(( $1 - 1 ))) )); fi; }
echo "fact 5 = $(fact 5)"

# FUNCNEST limits the depth (a child script, so the session goes on)
cat deep.sh
bash deep.sh
""")

Output

# Function as a condition
$ is_even() { (( $1 % 2 == 0 )); }
$ if is_even 4; then echo "4 is even"; else echo "4 is odd"; fi
4 is even
$ if is_even 7; then echo "7 is even"; else echo "7 is odd"; fi
7 is odd
# Recursion: factorial
$ fact() { if (( $1 <= 1 )); then echo 1; else echo $(( $1 * $(fact $(( $1 - 1 ))) )); fi; }
$ echo "fact 5 = $(fact 5)"
fact 5 = 120
# FUNCNEST limits the depth (a child script, so the session goes on)
$ cat deep.sh
FUNCNEST=5
deep() { echo "depth $1"; deep $(( $1 + 1 )); }
deep 1
echo "next line runs, status $?"
$ bash deep.sh
depth 1
depth 2
depth 3
depth 4
depth 5
deep.sh: line 2: deep: maximum function nesting level exceeded (5)
next line runs, status 1

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 condition_session.py

is_even 4 succeeded, so the if branch ran. is_even 7 failed, so the else branch ran.

Recursion works too: fact called itself with a smaller argument until the argument reached 1, and fact 5 gave 120. deep has no such stopping rule. With FUNCNEST=5, five nested calls ran and the sixth was refused with “maximum function nesting level exceeded (5)”: Bash abandoned the command that started the calls, deep 1, and the script went on with its next line, where $? was 1. Without FUNCNEST, Bash sets no limit of its own, and a runaway recursion runs until the process’s stack is used up and the shell crashes.

FUNCNEST was added in Bash 4.2

The NEWS file records FUNCNEST as a new feature of bash-4.2. Scripts that must run on older Bash or on dash cannot rely on it.

Checkpoint

Check: Functions as conditions and recursion

  1. Question 1 of 2 is_even() { (( $1 % 2 == 0 )); }; if is_even 5; then echo yes; else echo no; fi. What is printed?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: no

    The session's is_even 7 also failed, printing "7 is odd".

  2. Question 2 of 2 FUNCNEST=3; deep() { deep; }; deep. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Three nested calls succeed; the fourth is refused with "maximum function nesting level exceeded"

    The session's FUNCNEST=5 allowed five calls, then printed the exceeded message.

Practise: functions, local and return

Write two functions in your own shell and check them against these calls. is_leap_year YEAR prints nothing and only sets its status: 0 for a leap year of the Gregorian calendar and 1 for any other year, so is_leap_year 2024 and is_leap_year 2000 give 0, while is_leap_year 2023 and is_leap_year 1900 give 1, a century year counting only when 400 divides it. days_in_month YEAR MONTH prints the number of days and asks is_leap_year about February: days_in_month 2024 2 prints 29, days_in_month 2023 2 prints 28 and days_in_month 2023 4 prints 30, while days_in_month 2023 13 prints nothing and returns 1.

The questions below then move the rules onto cases the sessions did not run: a body without its semicolon, a function that writes into its caller’s local, a status handed on by a bare return, and a nesting limit reached through two different functions. Settle each answer from the rules above before you open its explanation:

Practice that adapts to you

Practise this lesson

12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 Why does greet() { echo hi } fail?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: The body must end with a semicolon, an &, or a newline before the closing brace

    The session's second bash -c line made this mistake: Bash read the } as an argument to echo and reached the end of the input still looking for the closing brace. Write greet() { echo hi; } or put the } on its own line.

  2. Question 2 of 12 A script starts with #!/bin/sh and defines function greet { echo hi; }. What does dash do?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: It stops with a syntax error at the brace

    ShellCheck flags this as SC2113 to warn that the script will not run under sh.

  3. Question 3 of 12 wrapper() { inner "$@"; }; inner() { echo "$#"; }; wrapper a "b c" d. What does inner print?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: 3

    The lesson's wrapper showed the same pattern: "$@" passes every argument on intact.

  4. Question 4 of 12 Inside a function called from a script named app.sh, what does $0 hold?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: app.sh, the script name

    $0 does not change inside a function. The session's interactive shell printed bash both outside and inside show; a script prints its own name in both places.

  5. Question 5 of 12 x=1; inc() { x=$((x + 1)); }; inc; inc; echo "$x". What is printed?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: 3

    Each call to inc adds 1 to the global x.

  6. Question 6 of 12 a() { local x=a; b; echo "$x"; }; b() { x=b; }; x=global; a. What does a print after calling b?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: b

    Dynamic scoping means b sees and writes into a's local x.

  7. Question 7 of 12 f() { true; false; return; }; f; echo $?. What is printed?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: 1

    A bare return hands on the status of the last command. The session's found function passed on grep's status 1 the same way.

  8. Question 8 of 12 f() { return "hello"; }; f. What happens?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: Bash prints "numeric argument required" and returns a non-zero status

    To hand data back, print it with echo or printf and capture it with $(...).

  9. Question 9 of 12 f() { return 512; }; f; echo $?. What number is printed?

    Type a number.

    Show the answer: Question 9 of 12

    Answer: 0

    Bash keeps only the low eight bits of the return value. 512 in binary is 1000000000, and its low eight bits are all zeros, so the caller sees 0. The same rule makes return 256 arrive as 0.

  10. Question 10 of 12 is_positive() { (( $1 > 0 )); }; is_positive -5; echo $?. What is printed?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: 1

    Using a function as a condition relies on its status: 0 for true, non-zero for false.

  11. Question 11 of 12 Without FUNCNEST, what limits a runaway recursion in Bash?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The system stack size

    Set FUNCNEST to a reasonable limit in scripts that use recursion.

  12. Question 12 of 12 FUNCNEST=1; f() { g; }; g() { echo deep; }; f. What happens?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: g is refused with "maximum function nesting level exceeded (1)" and nothing is printed

    The session's deep was refused at its sixth nested call; the same count applies when different functions call each other.

Interview questions

Warm-up (fresher to 2–5 years): what status does a function return when it has no return, and what range can return N use?

Without a return, a function returns the exit status of the last command its body ran, so a body that ends with a failed test returns 1. return N sets N and ends the function; Bash keeps only the low eight bits, so the caller sees 0 to 255: return 256 arrives as 0, a success, and return 300 as 44. return with no argument hands on the status of the last command, and a word such as return ok is an error with status 2. Text never travels through the status: a function prints it, and the caller captures it with $(...).

Two harder questions start from symptoms in real scripts: a file loop that handles the wrong files after a helper function gains a loop of its own, and a small wrapper added to a shared library after which every script dies at its first search. Name the likely cause of each before you open it:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A script walks 10 files with for (( i = 0; i < 10; i++ )) and calls check_file on each. After a teammate adds a loop to check_file, one branch handles only the first file and reports success, and another prints the fifth file again and again. What happened, and how do you fix it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    Someone adds grep() { grep --color=never "$@"; } to a library that every team script sources. From then on each script dies at its first grep with a segmentation fault, and one that sets FUNCNEST prints "maximum function nesting level exceeded". What is happening, and how do you change the wrapper?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Which way of defining a function works in every POSIX shell?

    name() { ...; }. The form function name { ...; } is not POSIX: Bash and ksh accept it, dash stops with a syntax error, and ShellCheck flags it as SC2113 in sh scripts.

  2. What do $1, "$@" and $0 mean inside a function?

    $1 to $# and "$@" are the function's own arguments. $0 stays the script's name, FUNCNAME[0] gives the function's name, and the caller's arguments come back after the function returns.

  3. What does local do, and what happens without it?

    local keeps a variable inside the function and the functions it calls. A variable changed without local is the global one, so the function overwrites the caller's value.

  4. Can a function read the local variables of the function that called it?

    Yes. Bash uses dynamic scoping: a called function sees, and can change, the locals of its caller.

  5. What status does a function return, with and without return?

    Without return, whatever status the final command of its body left. return N sets N and ends the function, but only 0 to 255 survive: return 256 arrives as 0 and return 300 as 44.

  6. When does a function used in if succeed?

    When it returns status 0. return prints nothing, so the caller tests the function directly, as in if is_valid "$x"; then.

  7. What does FUNCNEST do?

    It caps how deeply function calls may nest, from Bash 4.2. With FUNCNEST=5 the sixth nested call was refused: Bash gave up the whole command that began the recursion, $? was 1, and the next line of the script still ran. Without a cap, runaway recursion runs until the stack is used up and the shell crashes.

Key takeaways

  • The POSIX form name() { …; } works in Bash, dash and every POSIX shell; the form function name { …; } is not POSIX: Bash and ksh accept it, dash stops with a syntax error, and ShellCheck flags it as SC2113 in sh scripts.
  • Inside a function, $1 to $# and "$@" are the function’s own arguments; $0 stays the script’s name and FUNCNAME[0] gives the function’s name; the caller’s arguments come back after the function returns.
  • local keeps a variable inside the function; Bash uses dynamic scoping, so a called function sees the locals of its caller, and a change without local overwrites the global.
  • return N sets status 0–255 and ends the function; without it the function returns the status of its last command; return prints nothing, and a function used as a condition succeeds when its status is 0.
  • FUNCNEST (Bash 4.2) caps how deeply function calls may nest and abandons the command that goes deeper, with status 1; without it a runaway recursion runs until the stack is used up and the shell crashes.

Terms in this lesson

dynamic scoping
A scoping rule in which a function sees the variables of its caller, not only its own locals. In Bash, a called function can read and change its caller's local variables unless it declares its own local.
local variable
A variable declared with local inside a function, visible only to that function and the functions it calls. It shadows any global of the same name until the function returns.
shell function
A named group of commands defined with name() { …; } or function name { …; }, called like a command. It runs in the current shell, shares its variables unless local is used, and returns a status with return.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.