Bash Scripting 14 ๐ง Functions โ Writing reusable blocks and localized variables
A function is a named block of commands that can be called repeatedly. In Bash, a function is not a separate program; it is a set of commands stored in the current shell’s memory and executed when the function name is invoked. The previous chapters covered loops and arrays; this chapter covers the construct that packages logic into a reusable unit, the local keyword that keeps variables from leaking into the global scope, and the return mechanism that communicates a function’s result to its caller.
The function syntax is simple: a name, parentheses, and a body enclosed in braces. The parentheses are empty; they are part of the syntax and do not take arguments. The function receives its arguments through the positional parameters $1, $2, and $@, just like a script. The return command sets the function’s exit status, and the local keyword declares variables that are visible only inside the function. The distinction between return (exit status) and echo (output) is the source of the most common confusion.
Key point: A function returns an exit status, not a value. The return command accepts a number from 0 to 255, and the caller reads it with $?. To return a string or a computed value, the function prints it to stdout and the caller captures it with command substitution. The local keyword keeps a variable from leaking into the global scope, but it does not prevent the function from reading global variables. Without local, every variable the function assigns becomes a global variable.
Why functions require the local keyword
The variable-leak problem.
Bash variables are global by default. A variable assigned inside a function is visible outside it, and a variable assigned outside is visible inside it. A function that uses a loop counter named i will overwrite the caller’s i if the caller uses the same name. The local keyword declares a variable that is scoped to the function and its child functions. Without it, the function’s variables collide with the caller’s, and the collisions are difficult to trace because the function’s body is often in a different file.
The return-versus-echo problem.
The return command sets the exit status, which is a number from 0 to 255. A function that computes a string cannot return it with return; the string would be truncated to a number or rejected. The correct pattern is to print the result to stdout and let the caller capture it with $(function_name). The exit status is reserved for success or failure, and the caller checks it with if function_name; then ... or function_name || echo "failed" .
The argument problem.
A function does not have named parameters. The arguments are available as $1, $2, and $@, and the count is $#. The function can shift the arguments, but the caller’s positional parameters are not affected because the function has its own positional-parameter scope. The function can also modify the global positional parameters only if it is called with . or source, which is rarely the intent.
a. Declaring and calling functions
A function is declared with the name() syntax or the function name syntax. The body is a compound command enclosed in braces.
#!/usr/bin/env bash
# Declare a function
greet() {
echo "Hello, $1"
}
# Call it with an argument
greet "world" # Hello, world
# The function name is a command
if declare -f greet > /dev/null; then
echo "greet is defined"
fi
The greet() declaration defines the function. The greet "world" call passes the argument, which the function reads as $1. The declare -f greet command checks whether the function is defined, which is useful in a script that sources a library and wants to verify that the expected functions are present.
The function keyword is an alternative declaration form.
function greet {
echo "Hello, $1"
}
Both forms are equivalent. The name() form is POSIX-compliant, and the function form is a Bash extension. The name() form is the portable choice.
Functions must be declared before they are called. A function that is called before its declaration produces a “command not found” error. The standard pattern is to declare all functions at the top of the script and call them at the bottom.
b. Local variables and scope
The local keyword declares a variable that is visible only inside the function and its child functions. A variable declared without local is global.
#!/usr/bin/env bash
global_var="global"
demo_scope() {
local local_var="local"
global_var="modified by function"
echo "Inside: local_var=$local_var, global_var=$global_var"
}
demo_scope
echo "Outside: global_var=$global_var"
echo "Outside: local_var=${local_var:-unset}"
The demo_scope function declares local_var with local, so it is not visible outside the function. The global_var is assigned without local, so the assignment modifies the global variable. After the call, the global variable shows the modified value, and the local variable is unset . The ${local_var:-unset} expansion prints “unset” because the variable does not exist in the global scope.
The local keyword can assign and declare in one statement.
process_file() {
local file="$1"
local count=0
local -a lines
# ...
}
The local file="$1" form declares the variable and assigns the argument. The local -a lines form declares an array. The local -A map form declares an associative array. The local -r form declares a read-only variable .
The local keyword can also be used with command substitution, but the assignment and the declaration must be separate for the exit status to be preserved.
# โ INCORRECT: the local declaration masks the exit status
get_value() {
local result=$(some_command) # $? is the status of local, not some_command
return $?
}
# โ
CORRECT: separate declaration and assignment
get_value() {
local result
result=$(some_command) # $? is the status of some_command
return $?
}
The local result=$(some_command) form always returns the exit status of the local builtin, which is 0, even if some_command failed. The separate declaration and assignment preserves the command’s exit status .
c. Return values and the return status
The return command sets the function’s exit status. The caller reads it with $? or uses the function directly in a conditional.
#!/usr/bin/env bash
# Return an exit status
is_file() {
if [ -f "$1" ]; then
return 0 # Success
else
return 1 # Failure
fi
}
# Use the exit status in a conditional
if is_file /etc/passwd; then
echo "File exists"
fi
# Check the status explicitly
is_file /nonexistent
echo "Status: $?" # 1
The is_file function returns 0 if the file exists and 1 if it does not. The caller uses the function directly in an if statement, and the exit status is the condition. The $? variable holds the status after the call .
To return a string, the function prints it to stdout and the caller captures it with command substitution.
# Return a string via stdout
get_username() {
whoami
}
username=$(get_username)
echo "User: $username"
The whoami command prints the username, and the command substitution captures it. The function’s exit status is the status of the last command, which is whoami‘s status. If the caller needs both the output and the status, it can capture the output and check $? afterward.
# Capture output and check status
output=$(get_username)
status=$?
if [ "$status" -eq 0 ]; then
echo "User: $output"
fi
The output=$(get_username) command captures the stdout, and the $? immediately after holds the function’s exit status. The two are separate, and the script can use both .
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: BASIC FUNCTION
# ============================================
greet() {
echo "Hello, $1"
}
greet "world"
# ============================================
# PART 2: LOCAL VARIABLES
# ============================================
counter=100
increment() {
local counter=0
counter=$((counter + 1))
echo "Inside: $counter"
}
increment
echo "Outside: $counter" # 100, not 1
# ============================================
# PART 3: RETURNING A STATUS
# ============================================
is_root() {
[ "$(id -u)" -eq 0 ]
}
if is_root; then
echo "Running as root"
else
echo "Not root"
fi
# ============================================
# PART 4: RETURNING A STRING
# ============================================
get_hostname() {
hostname
}
host=$(get_hostname)
echo "Host: $host"
# ============================================
# PART 5: MULTIPLE ARGUMENTS
# ============================================
create_user() {
local username="$1"
local role="$2"
echo "Creating user $username with role $role"
}
create_user "alice" "admin"
# ============================================
# PART 6: ITERATING OVER ARGUMENTS
# ============================================
process_all() {
for item in "$@"; do
echo "Processing: $item"
done
}
process_all "one" "two three" "four"
# ============================================
# PART 7: LOCAL ARRAYS
# ============================================
collect_files() {
local -a files=()
for f in "$@"; do
[ -f "$f" ] && files+=("$f")
done
printf '%s\n' "${files[@]}"
}
collect_files /etc/passwd /nonexistent /etc/hosts
# ============================================
# PART 8: EXIT STATUS PRESERVATION
# ============================================
check_command() {
local output
output=$(some_command_that_fails 2>/dev/null)
local status=$?
echo "Output: $output"
return $status
}
# ============================================
# PART 9: FUNCTION WITH NAMEREF
# ============================================
fill_array() {
local -n target=$1
local value=$2
target+=("$value")
}
declare -a my_list=()
fill_array my_list "new item"
echo "${my_list[0]}"
# ============================================
# PART 10: RECURSIVE FUNCTION
# ============================================
factorial() {
local n=$1
if [ "$n" -le 1 ]; then
echo 1
else
local prev
prev=$(factorial $((n - 1)))
echo $((n * prev))
fi
}
echo "5! = $(factorial 5)"
The ten parts demonstrate the function surface: the basic call, the local scope, the return status, the return string, multiple arguments, argument iteration, local arrays, exit-status preservation, nameref passing, and recursion. The critical distinction is between Parts 3 and 4: the status function uses return, and the string function uses echo and command substitution.
Quick Reference
Function Syntax
| Form | Example |
|---|---|
| POSIX | name() { ...; } |
| Bash | function name { ...; } |
| Call | name arg1 arg2 |
Scope
| Keyword | Effect |
|---|---|
local var=value | Function-scoped variable |
local -a arr | Function-scoped indexed array |
local -A map | Function-scoped associative array |
local -r var | Function-scoped read-only |
| (none) | Global variable |
Return Mechanisms
| Mechanism | Use Case |
|---|---|
return N | Exit status (0โ255) |
echo "value" | String result via command substitution |
$? after call | Read the exit status |
if function; then | Use the status as a condition |
Argument Access
| Variable | Meaning |
|---|---|
$1, $2 | Positional arguments |
$# | Number of arguments |
"$@" | All arguments, boundaries preserved |
shift | Remove the first argument |
Best Practices
โ Do This:
# Declare local variables
my_func() { local var="value"; } # โ
# Use return for status
is_file() { [ -f "$1" ]; return $?; } # โ
# Use echo for string results
get_name() { echo "Alice"; }; name=$(get_name) # โ
# Separate local declaration and assignment
local output; output=$(command); local status=$? # โ
# Iterate over "$@" for arguments
for arg in "$@"; do echo "$arg"; done # โ
# Use nameref to pass arrays
func() { local -n arr=$1; arr+=("x"); } # โ
โ Don’t Do This:
# Forget local
my_func() { var="value"; } # Leaks to global scope # โ
# Use return for a string
get_name() { return "Alice"; } # Returns a number, not text # โ
# Combine local and command substitution
local result=$(failing_command) # $? is always 0 # โ
# Call a function before declaring it
my_func; my_func() { echo "hi"; } # Command not found # โ
# Use $* instead of "$@"
for arg in $*; do ...; done # Splits arguments with spaces # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Variable leaks to global | No local | Declare with local |
| Function returns a number instead of text | Used return "string" | Use echo and capture |
$? is always 0 after local | local masks the exit status | Separate declaration and assignment |
| Function not found | Called before declaration | Declare functions first |
| Arguments with spaces split | Used $* or unquoted $@ | Use "$@" |
| Array passed by value | Used "${arr[@]}" in the call | Use a nameref |
Real-World Examples
1. Basic function
greet() { echo "Hello, $1"; }; greet world
2. Local variable
f() { local x=1; echo "$x"; }; f
3. Return status
is_dir() { [ -d "$1" ]; }; is_dir /tmp && echo "yes"
4. Return string
get_date() { date +%F; }; today=$(get_date)
5. Multiple arguments
add() { echo $(($1 + $2)); }; add 2 3
6. Iterate arguments
each() { for x in "$@"; do echo "$x"; done; }; each a b c
7. Local array
f() { local -a a=(1 2 3); echo "${a[@]}"; }; f
8. Exit status preservation
f() { local out; out=$(cmd); local s=$?; return $s; }
9. Nameref
fill() { local -n a=$1; a+=(new); }; arr=(); fill arr
10. Recursion
fact() { local n=$1; [ "$n" -le 1 ] && echo 1 || echo $((n * $(fact $((n-1))))); }
Visual
Function Scope
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ local KEEPS VARIABLES INSIDE THE FUNCTION โ
โ โ
โ counter=100 โ
โ โ
โ increment() { โ
โ local counter=0 โ
โ counter=$((counter + 1)) โ
โ echo "Inside: $counter" โ 1 โ
โ } โ
โ โ
โ increment โ
โ echo "Outside: $counter" โ 100 โ
โ โ
โ The local variable shadows the global one. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Return vs Echo
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ TWO DIFFERENT RETURN MECHANISMS โ
โ โ
โ return N: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Sets the exit status (0โ255) โ โ
โ โ Used for success/failure โ โ
โ โ Read with $? or in a conditional โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ echo "value": โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Prints to stdout โ โ
โ โ Used for string results โ โ
โ โ Captured with $(function) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ return is for status. echo is for data. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The local + Command Substitution Trap
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ local result=$(cmd) MASKS THE EXIT STATUS โ
โ โ
โ โ Incorrect: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ local result=$(failing_command) โ โ
โ โ return $? # $? is the status of local (0) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ
Correct: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ local result โ โ
โ โ result=$(failing_command) โ โ
โ โ return $? # $? is the status of failing_command โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Declaration | name() { ...; } |
| Call | name arg1 arg2 |
| Arguments | $1, $2, "$@", $# |
| Local variable | local var=value |
| Local array | local -a arr |
| Local associative | local -A map |
| Return status | return N (0โ255) |
| Return string | echo + command substitution |
| Status read | $? after the call |
| Array pass | local -n ref=$1 (nameref) |
Key takeaways:
- A function returns an exit status, not a value. The
returncommand sets a number from 0 to 255, and the caller reads it with$?or uses the function in a conditional. To return a string, the function prints it to stdout and the caller captures it with$(function). - The
localkeyword prevents variable leaks. Without it, every variable the function assigns becomes global and collides with the caller’s variables. Thelocaldeclaration scopes the variable to the function and its children. localmasks the exit status of a command substitution. Thelocal result=$(command)form always returns the status oflocal, which is 0. The separate declaration and assignment preserves the command’s status.- Use
"$@"to iterate over arguments. The quoted form preserves the boundaries between arguments, so an argument with spaces is one iteration. The unquoted$*and$@split on whitespace. - Functions must be declared before they are called. A call to an undeclared function produces a “command not found” error. The standard pattern is to declare all functions at the top and call them at the bottom.
- Namerefs allow functions to modify arrays. The
local -n ref=$1declaration creates a reference to the caller’s array, and the function can append to it or modify its elements without copying the entire structure.
Remember: A function is a named block of commands with its own argument scope and its own local variables. The return command sets the exit status, and the echo command produces the string result that the caller captures. The local keyword keeps the function’s variables from leaking into the caller’s scope. The arguments are $1, $2, and "$@". The function must be declared before it is called. The result is a reusable block that does not collide with the rest of the script.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!