LFCS 47 🐧 Shell Functions
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
| Form | Example |
|---|---|
| Standard | name() { commands; } |
With function | function name { commands; } |
| With parentheses | function name() { commands; } |
Arguments and Return
| Construct | Meaning |
|---|---|
$1, $2 | Positional arguments |
$# | Argument count |
"$@" | All arguments as separate words |
return n | Set exit status to n |
local var | Scope variable to function |
Function vs Alias vs Script
| Aspect | Alias | Function | Script |
|---|---|---|---|
| Arguments | No | Yes | Yes |
| Logic | No | Yes | Yes |
| Runs in current shell | Yes | Yes | No |
| Can change caller’s cwd | No | Yes | No |
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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Syntax error on definition | Braces not separated by whitespace | { echo; } with spaces |
| Caller variable overwritten | No local declaration | local var=value |
| Arguments lost | Unquoted $@ | "$@" |
| Script exits prematurely | Used exit instead of return | return n |
| Function not found in child shell | Not exported | export -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
| Item | Value |
|---|---|
| Definition syntax | name() { commands; } |
| Execution context | Current shell, no new process |
| Arguments | $1, $2, $#, "$@" |
| Return status | return n or last command’s status |
| Local variables | local var |
| Persistence | ~/.bashrc for interactive shells |
| Export to child | export -f name |
| Removal | unset -f name |
| Recursion limit | FUNCNEST |
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
cdinside 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 . localprevents 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 .returnsets the exit status. A numeric argument toreturnbecomes the function’s status, which is how a function signals success or failure to a conditional. Usingexitinstead 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
~/.bashrcare available in interactive shells. Functions in scripts are available only within that script unless exported withexport -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.
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!