| |

LFCS 47 🐧 Shell Functions

⏱️ Estimated read: 11 min | |

A shell function is a named block of commands that the shell executes in the current process, as if you had typed each line yourself. Unlike a script, which runs in a new process with its own environment, a function shares the shell’s variables, its working directory, and its file descriptors . This is the distinction that makes functions useful: they can modify the calling shell’s state, and they do so without the overhead of forking a new process. For the LFCS exam, functions fall under the shell scripting objective—they are the tool for organizing reusable logic inside a script or an interactive session, and they are the recommended alternative to aliases whenever the logic requires arguments or conditionals .

The syntax has a few sharp edges. The curly braces must be separated from the body by whitespace, and the body must end with a semicolon or a newline before the closing brace . The function keyword is optional, but if it is used, the parentheses are optional too. The exit status of a function is the exit status of the last command executed, unless a return statement sets a specific code. Arguments are passed exactly like script arguments—$1, $2, $#—but they are local to the function and restored when it returns .

Key point: A shell function is defined as name() { commands; }, runs in the current shell context without creating a new process, receives arguments through $1, $2, and returns an exit status via return n or the last command’s status; it is the correct tool for argument-taking shortcuts, where an alias cannot work.


Why functions exist and how they differ from aliases and scripts

The argument problem. An alias is a literal text substitution. It cannot reference $1, cannot branch on the number of arguments, and cannot validate input . A function receives arguments as positional parameters and can use them anywhere in its body. This is why the Bash manual recommends functions over aliases for almost every purpose .

The process problem. A script runs in a separate process. If a script changes a directory with cd, the parent shell’s working directory is unaffected. If a script sets a variable, the parent shell does not see it. A function runs in the current shell context, so cd inside a function changes the caller’s directory, and variable assignments persist after the function returns . This is the property that makes functions useful for environment manipulation.

The local variable problem. Because functions share the shell’s variable namespace, a function that sets count=0 will overwrite the caller’s count unless the variable is declared with local. The local builtin restricts the variable’s scope to the function and its children, and the value is restored when the function returns . This is the closest thing to a stack frame that shell scripting has.

The recursion problem. Functions can call themselves. The Bash manual states that there is no limit on recursive calls by default, but the FUNCNEST variable can set a maximum nesting depth . Recursion is useful for directory tree traversal and similar tasks, but it must have a terminating condition or the shell will exhaust its stack.


a. Defining, calling, and returning from a function

A function is defined by writing its name, parentheses, and a brace-enclosed body. The definition takes effect immediately; it does not need to be “executed” separately.

# Define a function that prints a greeting
# The braces must be separated from the body by whitespace
greet() {
    # $1 is the first argument passed to the function
    echo "Hello, $1"
}

# Call the function with an argument
greet "Alice"
# Output: Hello, Alice

# The function's exit status is the status of the last command
# Here, echo returns 0, so the function returns 0
echo $?

The return builtin ends the function and sets its exit status. Without return, the status is the status of the last command . A numeric argument to return becomes the function’s exit status, which is how a function signals success or failure to a conditional.

# Define a function that checks if a file exists
check_file() {
    # If the file exists, return success (0)
    if [ -f "$1" ]; then
        return 0
    else
        # If not, return failure (1)
        return 1
    fi
}

# Use the function in a conditional
if check_file "/etc/passwd"; then
    echo "File exists"
fi

The local builtin declares a variable that is scoped to the function. Without local, the assignment would affect the caller’s variable of the same name .

# Define a function with a local variable
process_items() {
    # local restricts this variable to the function
    local count=0
    # Loop over the arguments
    for item in "$@"; do
        count=$((count + 1))
    done
    # Print the count; this does not affect any caller variable
    echo "Processed $count items"
}

process_items a b c
# Output: Processed 3 items

b. Arguments and the "$@" convention

A function receives its arguments as positional parameters. $1 is the first argument, $2 the second, and $# the count. The special parameter "$@" expands to all arguments as separate words, which is the correct way to pass them through to another command without losing spaces or globbing .

# Define a function that wraps ls with common flags
# "$@" preserves all arguments exactly as the caller passed them
ll() {
    ls -alF "$@"
}

# Call it with a directory and an additional flag
ll /tmp
# Runs: ls -alF /tmp

Without the quotes around $@, arguments containing spaces are split into multiple words. With "$@", each argument remains a single word, which is what the caller intended.

The function’s positional parameters are local to the function. When the function returns, the caller’s $1, $2, and $# are restored to their previous values . This means a function can safely call another function without corrupting the caller’s argument list.


c. Functions in scripts and interactive shells

A function defined in a script is available only within that script. A function defined in ~/.bashrc is available in every interactive shell that reads that file, and in any script that sources .bashrc (or inherits the function through export -f).

# ~/.bashrc

# A function to create a directory and enter it
# An alias cannot do this because it cannot reference $1
mkcd() {
    mkdir -p "$1" && cd "$1"
}

The mkcd function is the classic example of why functions exist. An alias like alias mkcd='mkdir -p' would create the directory but could not change into it, because the cd would run in a subshell context that the alias cannot express. The function runs both commands in the current shell, so the directory change persists .

Functions can be exported to child shells with export -f. This is useful when a script calls another script and expects the function to be available. The child shell inherits the function definition through the environment .

# Export the function so child bash processes inherit it
export -f mkcd

The unset -f command removes a function definition. The declare -f command prints a function’s definition, which is useful for inspecting what a function does before calling it .


Complete Example Session

The following session defines a function that validates an argument, uses a local variable, returns a status code, and is used in a conditional.

# ============================================
# PART 1: DEFINE A FUNCTION WITH VALIDATION
# ============================================
# The function checks that a file exists and is readable
check_readable() {
    # $1 is the path to check; $# is the argument count
    if [ "$#" -ne 1 ]; then
        # Print usage to stderr and return failure
        echo "Usage: check_readable <file>" >&2
        return 2
    fi

    # local variable to hold the path
    local path="$1"

    # Test if the file exists and is readable
    if [ -r "$path" ]; then
        # Return success
        return 0
    else
        # Return failure
        return 1
    fi
}

# ============================================
# PART 2: CALL AND INTERPRET THE STATUS
# ============================================
# Call with a known-readable file
if check_readable "/etc/hostname"; then
    echo "File is readable"
fi

# Call with a missing argument; returns 2
check_readable
echo "Exit status: $?"
# Output: Usage: check_readable <file>
# Output: Exit status: 2

# ============================================
# PART 3: LOCAL VARIABLE SCOPE
# ============================================
# Set a variable in the caller
path="/tmp"

# Define a function that uses a local of the same name
show_path() {
    local path="/etc"
    echo "Inside function: $path"
}

show_path
# Output: Inside function: /etc

# The caller's variable is unchanged
echo "Outside function: $path"
# Output: Outside function: /tmp

The three parts cover the function definition with argument validation, the status code interpretation, and the local variable scope. What the session demonstrates is that the function receives its arguments as positional parameters, returns a status that the caller can branch on, and declares a local variable that does not leak into the caller’s scope.


Quick Reference

Function Definition

FormExample
Standardname() { commands; }
With functionfunction name { commands; }
With parenthesesfunction name() { commands; }

Arguments and Return

ConstructMeaning
$1, $2Positional arguments
$#Argument count
"$@"All arguments as separate words
return nSet exit status to n
local varScope variable to function

Function vs Alias vs Script

AspectAliasFunctionScript
ArgumentsNoYesYes
LogicNoYesYes
Runs in current shellYesYesNo
Can change caller’s cwdNoYesNo

Best Practices

✅ Do This:

# Quote "$@" to preserve arguments
ll() { ls -alF "$@"; }

# Declare local variables with local
process() { local count=0; }

# Use return for status codes
check() { [ -f "$1" ] && return 0 || return 1; }

❌ Don’t Do This:

# Forget whitespace around braces
f() {echo hi}  # syntax error

# Omit local and overwrite caller variables
f() { path=/tmp; }

# Use exit inside a function in a script
f() { exit 1; }  # exits the entire script, not just the function

Common Pitfalls

PitfallWhy It HappensFix
Syntax error on definitionBraces not separated by whitespace{ echo; } with spaces
Caller variable overwrittenNo local declarationlocal var=value
Arguments lostUnquoted $@"$@"
Script exits prematurelyUsed exit instead of returnreturn n
Function not found in child shellNot exportedexport -f name

Real-World Examples

1. Basic function

greet() { echo "Hello, $1"; }

2. Function with local variable

count_lines() {
    local file="$1"
    wc -l < "$file"
}

3. Function with return status

is_root() {
    [ "$(id -u)" -eq 0 ]
}

4. Function using “$@”

ll() { ls -alF "$@"; }

5. mkcd function

mkcd() { mkdir -p "$1" && cd "$1"; }

6. Export function

export -f mkcd

7. Unset function

unset -f mkcd

8. Print function definition

declare -f mkcd

9. Recursive function

walk() {
    for item in "$1"/*; do
        [ -d "$item" ] && walk "$item"
    done
}

10. Function in a conditional

if is_root; then echo "root"; fi

Visual

Function Execution Context

┌──────────────────────────────────────────────┐
│  CURRENT SHELL                               │
│  ┌────────────────────────────────────────┐  │
│  │  f() { cd /tmp; }                      │  │
│  │  f                                     │  │
│  │  # cd affects the current shell        │  │
│  └────────────────────────────────────────┘  │
│                                              │
│  SCRIPT                                      │
│  ┌────────────────────────────────────────┐  │
│  │  #!/bin/bash                           │  │
│  │  cd /tmp                               │  │
│  │  # cd affects only the child shell     │  │
│  └────────────────────────────────────────┘  │
└──────────────────────────────────────────────┘

Argument Flow

┌──────────────────────────────────────────────┐
│  Caller: f one two three                     │
│         │                                    │
│         ▼                                    │
│  Function body                               │
│  $1 = one                                    │
│  $2 = two                                    │
│  $3 = three                                  │
│  $# = 3                                      │
│         │                                    │
│         ▼                                    │
│  On return, caller's $1, $2, $# restored     │
└──────────────────────────────────────────────┘

local Scope

┌──────────────────────────────────────────────┐
│  var=global                                  │
│         │                                    │
│         ▼                                    │
│  f() {                                       │
│    local var=local                           │
│    echo $var  # local                        │
│  }                                           │
│         │                                    │
│         ▼                                    │
│  f                                           │
│  echo $var  # global (unchanged)             │
└──────────────────────────────────────────────┘

Function vs Alias

┌──────────────────────────────────────────────┐
│  ALIAS: mkcd='mkdir -p'                      │
│  mkcd /tmp/new                               │
│  → mkdir -p /tmp/new                         │
│  → does not cd (cannot reference $1)         │
│                                              │
│  FUNCTION: mkcd() { mkdir -p "$1" && cd "$1"; } │
│  mkcd /tmp/new                               │
│  → mkdir -p /tmp/new && cd /tmp/new          │
│  → cwd changes                               │
└──────────────────────────────────────────────┘

Summary

ItemValue
Definition syntaxname() { commands; }
Execution contextCurrent shell, no new process
Arguments$1, $2, $#, "$@"
Return statusreturn n or last command’s status
Local variableslocal var
Persistence~/.bashrc for interactive shells
Export to childexport -f name
Removalunset -f name
Recursion limitFUNCNEST

Key takeaways:

  • Functions run in the current shell. Unlike a script, a function shares the caller’s variables, working directory, and file descriptors. This is why cd inside a function changes the caller’s directory .
  • Arguments are positional parameters. $1, $2, and $# work exactly as they do in a script, but they are restored when the function returns. "$@" is the correct way to forward all arguments without losing spaces .
  • local prevents variable leakage. Without it, a function’s variable assignments overwrite the caller’s variables of the same name. With it, the variable is scoped to the function and its children .
  • return sets the exit status. A numeric argument to return becomes the function’s status, which is how a function signals success or failure to a conditional. Using exit instead exits the entire script .
  • Functions are the correct tool for argument-taking shortcuts. The Bash manual recommends functions over aliases for almost every purpose because aliases cannot reference arguments or contain logic .
  • Persistence follows the same rules as aliases. Functions defined in ~/.bashrc are available in interactive shells. Functions in scripts are available only within that script unless exported with export -f .

Remember: A shell function is a named block of commands that executes in the current shell, receives arguments as positional parameters, returns a status code, and can scope its variables with local. It is the tool for organizing logic that an alias cannot express—anything that needs $1, a conditional, or a cd that affects the caller. The definition syntax has whitespace requirements that are easy to forget, and the difference between return and exit is the difference between leaving the function and leaving the entire script. For the LFCS exam, know how to define a function, pass arguments, declare a local variable, and return a status, and know why a function is preferable to an alias when the logic requires arguments.


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!