| |

Bash Scripting 14 ๐Ÿง Functions โ€” Writing reusable blocks and localized variables

โฑ๏ธ Estimated read: 12 min | |

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

FormExample
POSIXname() { ...; }
Bashfunction name { ...; }
Callname arg1 arg2

Scope

KeywordEffect
local var=valueFunction-scoped variable
local -a arrFunction-scoped indexed array
local -A mapFunction-scoped associative array
local -r varFunction-scoped read-only
(none)Global variable

Return Mechanisms

MechanismUse Case
return NExit status (0โ€“255)
echo "value"String result via command substitution
$? after callRead the exit status
if function; thenUse the status as a condition

Argument Access

VariableMeaning
$1, $2Positional arguments
$#Number of arguments
"$@"All arguments, boundaries preserved
shiftRemove 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

PitfallWhy It HappensFix
Variable leaks to globalNo localDeclare with local
Function returns a number instead of textUsed return "string"Use echo and capture
$? is always 0 after locallocal masks the exit statusSeparate declaration and assignment
Function not foundCalled before declarationDeclare functions first
Arguments with spaces splitUsed $* or unquoted $@Use "$@"
Array passed by valueUsed "${arr[@]}" in the callUse 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

ItemValue
Declarationname() { ...; }
Callname arg1 arg2
Arguments$1, $2, "$@", $#
Local variablelocal var=value
Local arraylocal -a arr
Local associativelocal -A map
Return statusreturn N (0โ€“255)
Return stringecho + command substitution
Status read$? after the call
Array passlocal -n ref=$1 (nameref)

Key takeaways:

  • A function returns an exit status, not a value. The return command 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 local keyword prevents variable leaks. Without it, every variable the function assigns becomes global and collides with the caller’s variables. The local declaration scopes the variable to the function and its children.
  • local masks the exit status of a command substitution. The local result=$(command) form always returns the status of local, 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=$1 declaration 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.


Was this helpful?


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!