|

Linux CLI 57 🐧 shell scripts functions

⏱️ Estimated read: 13 min | |

#!/bin/bash

function greet() {
    echo "Hello, World!"
}

greet2() {
    echo "Hello, World!"
}

greet
greet2

greet3() {
    echo "Hello, $1! You are $2 years old."
}

greet3 "Kronos" 30

add() {
    local sum=$(( $1 + $2 ))
    echo $sum
}

result=$(add 5 3)
echo "The sum is: $result"

Functions let you encapsulate code into reusable blocks. Instead of repeating the same commands in several places, you define them once and call them by name. This makes your scripts cleaner, shorter, and easier to maintain.

Key point: Shell functions are not like functions in C or Python. They don’t have a return type, they don’t have a separate scope (unless you use local), and they return an exit status (0–255), not a value. To pass a value back, you echo it and capture it with $(...).


a – functions in shell scripts

Functions within shell scripts allow you to encapsulate code into reusable blocks. It makes your scripts cleaner and more maintainable.

Defining a function — two ways:

Method 1 — Using the function keyword:

function greet() {
    echo "Hello, World!"
}

Method 2 — Without the function keyword (POSIX):

greet2() {
    echo "Hello, World!"
}

Both work in bash. The second form is portable — it works in sh, dash, ksh, and zsh too.

Calling a function:

Just write its name — like any other command:

$ greet
[ Hello, World! ]

$ greet2
[ Hello, World! ]

A function can call other functions:

#!/bin/bash

say_hello() {
    echo "Hello"
}

say_world() {
    echo "World"
}

greet() {
    say_hello
    say_world
}

greet
[ Hello ]
[ World ]

Functions must be defined before they’re called:

#!/bin/bash

greet          # ❌ error: greet: command not found

greet() {
    echo "Hello"
}

Order matters. Bash reads the script top to bottom, so the function must be defined before the call.

Where to put functions:

#!/bin/bash

# Option 1: at the top
myfunc() {
    echo "Hello"
}

# Main script
myfunc

# Option 2: at the bottom (works because the shell parses first, then runs)
main() {
    myfunc
}
myfunc() {
    echo "Hello"
}
main

Most scripts put all functions at the top, then a main function, then call main at the bottom.

A more complete example:

#!/bin/bash

log() {
    echo "[$(date +%T)] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

cleanup() {
    echo "Cleaning up..."
    rm -f /tmp/myscript.*
}

main() {
    log "Starting"
    log "Doing work..."
    error "Something went wrong"
    cleanup
    log "Done"
}

main
$ ./script.sh
[ 10:00:00 ] Starting
[ 10:00:00 ] Doing work...
[ ERROR ] Something went wrong
Cleaning up...
[ 10:00:00 ] Done

Listing and unsetting functions:

$ declare -f              # list all functions
$ declare -f greet        # show one function
$ unset -f greet          # remove a function

b – Passing arguments to a function in shell scripts

You can pass arguments to a function just like you would with any other command. Arguments are values that are passed to the function.

Inside the function, arguments are accessed as $1, $2, etc.:

VariableMeaning
$1, $2, …First, second, … argument
$0Script name (not function name!)
$#Number of arguments
$@All arguments as separate words
$*All arguments as one word

Important: Inside a function, $0 is still the script name — not the function name. There’s no built-in way to get the function’s own name.

Example — a function with two arguments:

#!/bin/bash

greet() {
    echo "Hello, $1! You are $2 years old."
}

greet "Kronos" 30
[ Hello, Kronos! You are 30 years old. ]

Passing arguments with spaces:

greet "Alice Smith" 25
[ Hello, Alice Smith! You are 25 years old. ]

Quote arguments that contain spaces.

Arguments are positional — order matters:

describe() {
    echo "Name: $1"
    echo "Age: $2"
    echo "City: $3"
}

describe Alice 30 Paris
[ Name: Alice ]
[ Age: 30 ]
[ City: Paris ]

Using $# to count arguments:

count_args() {
    echo "You passed $# arguments"
}

count_args a b c d
[ You passed 4 arguments ]

Using $@ to loop over arguments:

print_all() {
    for arg in "$@"; do
        echo "Argument: $arg"
    done
}

print_all one two three
[ Argument: one ]
[ Argument: two ]
[ Argument: three ]

Providing defaults:

greet() {
    local name="${1:-Guest}"
    echo "Hello, $name!"
}

greet
[ Hello, Guest! ]

greet Alice
[ Hello, Alice! ]

Validating argument count:

add() {
    if [ $# -ne 2 ]; then
        echo "Usage: add <num1> <num2>" >&2
        return 1
    fi
    echo $(( $1 + $2 ))
}

add 5 3
[ 8 ]

add 5
[ Usage: add <num1> <num2> ]

Passing arguments from the script to a function:

#!/bin/bash

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

greet "$1"
$ ./script.sh Alice
[ Hello, Alice! ]

Referencing all script arguments inside a function:

#!/bin/bash

show() {
    echo "Function got: $@"
    echo "Script got: $@"
}

show one two three

Note: Inside a function, $@ refers to the function’s arguments, not the script’s. To access the script’s arguments inside a function, save them first:

SCRIPT_ARGS=("$@")
show() {
    echo "Function: $@"
    echo "Script: ${SCRIPT_ARGS[@]}"
}

c – Returning values from a function in shell scripts

In shell scripting, functions don’t return values like in some other programming languages. Instead, they can modify variables or print output that can be captured by the caller.

The most common way to return values — echo:

add() {
    local sum=$(( $1 + $2 ))
    echo $sum
}

result=$(add 5 3)
echo "The sum is: $result"
[ The sum is: 8 ]

The function echoes the result. The caller captures it with $(...) — command substitution.

Return with exit status (return):

Every function returns an exit status — a number between 0 and 255. 0 means success, anything else means failure.

is_even() {
    if [ $(( $1 % 2 )) -eq 0 ]; then
        return 0    # success — even
    else
        return 1    # failure — odd
    fi
}

if is_even 4; then
    echo "4 is even"
fi

if is_even 5; then
    echo "5 is even"
else
    echo "5 is odd"
fi
[ 4 is even ]
[ 5 is odd ]

Reading the return value with $?:

add() {
    return $(( $1 + $2 ))
}

add 5 3
echo "Status: $?"
[ Status: 8 ]

⚠️ Warning: return in bash only accepts values 0–255. return 300 wraps around to 44. For real values, use echo + $(...).

Returning multiple values:

Use echo with multiple lines, then read into an array:

get_name_and_age() {
    echo "Alice"
    echo "30"
}

readarray -t info < <(get_name_and_age)
echo "Name: ${info[0]}"
echo "Age: ${info[1]}"
[ Name: Alice ]
[ Age: 30 ]

Or use read with process substitution:

get_info() {
    echo "Alice 30"
}

read name age < <(get_info)
echo "$name is $age"
[ Alice is 30 ]

Using global variables (not recommended):

RESULT=""

add() {
    RESULT=$(( $1 + $2 ))
}

add 5 3
echo "Sum: $RESULT"
[ Sum: 5 + 3 = 8 ]

This works but is fragile — globals can be accidentally overwritten. Prefer echo + $(...) or return for status.

Using local for internal variables:

add() {
    local sum=$(( $1 + $2 ))
    echo $sum
}

add 5 3
echo "sum is: $sum"    # empty — sum was local
[ 8 ]
[ sum is: ]

local keeps variables inside the function. Without it, sum would leak into the global scope.

Comparing return approaches:

ApproachUse forExample
echo + $()Return a valueresult=$(add 5 3)
return NReturn status (0–255)if is_even 4; then ...
Global variableSimple flagsRESULT=...
readarrayReturn multiple valuesreadarray -t arr < <(fn)

Full example — returning both a value and a status:

#!/bin/bash

divide() {
    if [ "$2" -eq 0 ]; then
        echo "Error: division by zero" >&2
        return 1
    fi
    echo $(( $1 / $2 ))
    return 0
}

result=$(divide 10 2)
if [ $? -eq 0 ]; then
    echo "10 / 2 = $result"
fi

result=$(divide 10 0)
if [ $? -ne 0 ]; then
    echo "Division failed"
fi
[ 10 / 2 = 5 ]
[ Error: division by zero ]
[ Division failed ]

Complete Example Session

# ============================================
# PART 1: DEFINING FUNCTIONS
# ============================================

$ greet() { echo "Hello, World!"; }
$ greet
[ Hello, World! ]

$ function greet2() { echo "Hi!"; }
$ greet2
[ Hi! ]

# ============================================
# PART 2: FUNCTIONS WITH ARGUMENTS
# ============================================

$ greet3() { echo "Hello, $1! You are $2 years old."; }
$ greet3 "Kronos" 30
[ Hello, Kronos! You are 30 years old. ]

$ greet3 "Alice Smith" 25
[ Hello, Alice Smith! You are 25 years old. ]

# ============================================
# PART 3: COUNTING ARGUMENTS
# ============================================

$ count_args() { echo "You passed $# arguments"; }
$ count_args a b c d
[ You passed 4 arguments ]

# ============================================
# PART 4: LOOPING OVER ARGUMENTS
# ============================================

$ print_all() {
>     for arg in "$@"; do
>         echo "Argument: $arg"
>     done
> }
$ print_all one two three
[ Argument: one ]
[ Argument: two ]
[ Argument: three ]

# ============================================
# PART 5: RETURNING A VALUE WITH ECHO
# ============================================

$ add() {
>     local sum=$(( $1 + $2 ))
>     echo $sum
> }
$ result=$(add 5 3)
$ echo "The sum is: $result"
[ The sum is: 8 ]

# ============================================
# PART 6: RETURNING A STATUS
# ============================================

$ is_even() {
>     if [ $(( $1 % 2 )) -eq 0 ]; then
>         return 0
>     else
>         return 1
>     fi
> }
$ if is_even 4; then echo "even"; fi
[ even ]

$ if is_even 5; then echo "even"; else echo "odd"; fi
[ odd ]

# ============================================
# PART 7: LOCAL VARIABLES
# ============================================

$ myfunc() {
>     local secret="hidden"
>     echo "Inside: $secret"
> }
$ myfunc
[ Inside: hidden ]
$ echo "Outside: $secret"
[ Outside: ]

# ============================================
# PART 8: DEFAULTS
# ============================================

$ greet() {
>     local name="${1:-Guest}"
>     echo "Hello, $name!"
> }
$ greet
[ Hello, Guest! ]
$ greet Alice
[ Hello, Alice! ]

# ============================================
# PART 9: MULTIPLE RETURN VALUES
# ============================================

$ get_info() {
>     echo "Alice"
>     echo "30"
> }
$ readarray -t info < <(get_info)
$ echo "Name: ${info[0]}, Age: ${info[1]}"
[ Name: Alice, Age: 30 ]

# ============================================
# PART 10: A LOGGING FUNCTION
# ============================================

$ log() { echo "[$(date +%T)] $1"; }
$ log "Starting backup"
[ [10:00:00] Starting backup ]
$ log "Backup complete"
[ [10:00:00] Backup complete ]

# ============================================
# PART 11: A SCRIPT WITH MAIN
# ============================================

$ cat > script.sh << 'EOF'
#!/bin/bash

log() {
    echo "[$(date +%T)] $1"
}

error() {
    echo "[ERROR] $1" >&2
}

cleanup() {
    echo "Cleaning up..."
}

main() {
    log "Starting"
    error "Something failed"
    cleanup
}

main
EOF

$ chmod +x script.sh
$ ./script.sh
[ [10:00:00] Starting ]
[ [ERROR] Something failed ]
[ Cleaning up... ]

# ============================================
# PART 12: VALIDATING ARGUMENTS
# ============================================

$ divide() {
>     if [ "$2" -eq 0 ]; then
>         echo "Error: division by zero" >&2
>         return 1
>     fi
>     echo $(( $1 / $2 ))
>     return 0
> }
$ result=$(divide 10 2) && echo "Result: $result"
[ Result: 5 ]
$ result=$(divide 10 0) || echo "Failed"
[ Error: division by zero ]
[ Failed ]

# ============================================
# PART 13: UNIQUE TEMP FILE WITH FUNCTION
# ============================================

$ make_temp() {
>     mktemp "/tmp/${1:-script}.XXXXXX"
> }
$ f=$(make_temp backup)
$ echo "$f"
[ /tmp/backup.aB3xYz ]

Quick Reference

Defining Functions

SyntaxPortability
function name() { ... }Bash, ksh
name() { ... }POSIX — all shells
function name { ... }Bash only

Calling Functions

CommandPurpose
nameCall the function
name arg1 arg2Call with arguments
$(name args)Capture output

Inside a Function

VariableMeaning
$1, $2, …Arguments
$#Number of arguments
$@All arguments (separate)
$*All arguments (one word)
$0Script name (not function name)
$?Exit status of last command

Returning Values

MethodPurposeExample
echoReturn valueresult=$(add 5 3)
return NReturn status (0–255)if fn; then ...
Global varSimple flagRESULT=...
readarrayMultiple valuesreadarray -t a < <(fn)

Scope

KeywordEffect
(none)Global
localFunction-local
declareDeclare inside function

Managing Functions

CommandPurpose
declare -fList all functions
declare -f nameShow one function
unset -f nameRemove a function

Best Practices

✅ Do This:

# Use POSIX syntax for portability
myfunc() { ... }                      # ✅

# Use local for internal variables
myfunc() {
    local tmp="x"
}                                     # ✅

# Validate arguments
if [ $# -ne 2 ]; then
    echo "Usage: $0 <a> <b>" >&2
    exit 1
fi                                    # ✅

# Return values with echo
add() { echo $(( $1 + $2 )); }
result=$(add 5 3)                     # ✅

# Return status with return
is_even() { [ $(( $1 % 2 )) -eq 0 ]; } # ✅

# Use "$@" to preserve argument boundaries
for arg in "$@"; do ...; done         # ✅

# Provide defaults
name="${1:-Guest}"                    # ✅

# Put functions at the top
# ... functions ...
main "$@"                             # ✅

# Use descriptive names
log_message() { ... }                 # ✅

❌ Don’t Do This:

# Don't use global variables for return values
RESULT=""
add() { RESULT=$(( $1 + $2 )); }      # ⚠️  fragile

# Don't return values > 255
return 300                            # ❌ wraps to 44

# Don't forget local
myfunc() {
    tmp="x"                           # ❌ leaks to global
}                                     # ✅ use local tmp="x"

# Don't call a function before defining it
greet                                 # ❌ not yet defined
greet() { echo "Hi"; }

# Don't use $0 as function name
echo "Function: $0"                   # ❌ it's the script name

# Don't trust unvalidated arguments
add() { echo $(( $1 + $2 )); }        # ❌ fails if $1 is empty

# Don't use function keyword if you want portability
function myfunc() { ... }             # ⚠️  bash-only

# Don't nest functions unnecessarily
outer() {
    inner() { ... }                   # ⚠️  inner redefined each call
}

Common Pitfalls

PitfallProblemSolution
Called before definedcommand not foundDefine first
return 300Wraps to 44Use echo for values
Forgot localVariable leaksUse local
$0 inside functionScript name, not functionSave it if needed
Argument countMissing argsValidate $#
$@ vs $*Word splittingUse "$@"
Global var returnOverwritesPrefer echo + $()
Nested functionsRedefined each callDefine once outside

Real-World Examples

1. Simple Greeting

#!/bin/bash
greet() {
    echo "Hello, World!"
}
greet
[ Hello, World! ]

2. Greeting with Arguments

#!/bin/bash
greet() {
    echo "Hello, $1! You are $2 years old."
}
greet "Kronos" 30
[ Hello, Kronos! You are 30 years old. ]

3. Add Function

#!/bin/bash
add() {
    local sum=$(( $1 + $2 ))
    echo $sum
}
result=$(add 5 3)
echo "The sum is: $result"
[ The sum is: 8 ]

4. Logging Function

#!/bin/bash
log() {
    echo "[$(date +%T)] $1"
}
log "Starting"
log "Done"
[ [10:00:00] Starting ]
[ [10:00:00] Done ]

5. Error Function

#!/bin/bash
error() {
    echo "[ERROR] $1" >&2
    return 1
}
error "File not found"
[ [ERROR] File not found ]

6. Check Even/Odd

#!/bin/bash
is_even() {
    [ $(( $1 % 2 )) -eq 0 ]
}
if is_even 4; then echo "even"; fi
[ even ]

7. Usage Message

#!/bin/bash
usage() {
    echo "Usage: $0 <src> <dst>" >&2
    exit 1
}
if [ $# -ne 2 ]; then
    usage
fi

8. Cleanup Function

#!/bin/bash
cleanup() {
    echo "Cleaning up temp files"
    rm -f /tmp/script.*
}
trap cleanup EXIT
echo "Working..."

9. Loop Over Arguments

#!/bin/bash
print_all() {
    for arg in "$@"; do
        echo "Argument: $arg"
    done
}
print_all one two three
[ Argument: one ]
[ Argument: two ]
[ Argument: three ]

10. Default Arguments

#!/bin/bash
greet() {
    local name="${1:-Guest}"
    echo "Hello, $name!"
}
greet
greet Alice
[ Hello, Guest! ]
[ Hello, Alice! ]

11. Validation Function

#!/bin/bash
is_number() {
    [[ "$1" =~ ^[0-9]+$ ]]
}
if is_number "42"; then
    echo "It's a number"
fi
[ It's a number ]

12. Divide with Error Handling

#!/bin/bash
divide() {
    if [ "$2" -eq 0 ]; then
        echo "Error: division by zero" >&2
        return 1
    fi
    echo $(( $1 / $2 ))
}
result=$(divide 10 2) && echo "Result: $result"
result=$(divide 10 0) || echo "Failed"
[ Result: 5 ]
[ Error: division by zero ]
[ Failed ]

13. Multiple Return Values

#!/bin/bash
get_info() {
    echo "Alice"
    echo "30"
    echo "Paris"
}
readarray -t info < <(get_info)
echo "Name: ${info[0]}"
echo "Age: ${info[1]}"
echo "City: ${info[2]}"
[ Name: Alice ]
[ Age: 30 ]
[ City: Paris ]

14. Retry Function

#!/bin/bash
retry() {
    local n=$1
    shift
    for i in $(seq 1 "$n"); do
        if "$@"; then
            return 0
        fi
        echo "Attempt $i failed, retrying..."
        sleep 1
    done
    return 1
}
retry 3 curl -s https://example.com

15. Confirm Function

#!/bin/bash
confirm() {
    local prompt="${1:-Are you sure?}"
    read -r -p "$prompt [y/N] " answer
    [[ "$answer" =~ ^[Yy]$ ]]
}
if confirm "Delete files?"; then
    echo "Deleting..."
else
    echo "Cancelled"
fi

16. Array Return

#!/bin/bash
get_fruits() {
    echo "apple banana cherry"
}
read -ra fruits <<< "$(get_fruits)"
for f in "${fruits[@]}"; do
    echo "Fruit: $f"
done
[ Fruit: apple ]
[ Fruit: banana ]
[ Fruit: cherry ]

17. Counter with Local

#!/bin/bash
count_up() {
    local i
    for ((i=1; i<=$1; i++)); do
        echo "$i"
    done
}
count_up 3
[ 1 ]
[ 2 ]
[ 3 ]

18. Main Function Pattern

#!/bin/bash

log() { echo "[$(date +%T)] $1"; }
error() { echo "[ERROR] $1" >&2; return 1; }

main() {
    log "Starting"
    if [ $# -eq 0 ]; then
        error "No arguments"
        return 1
    fi
    log "Processing: $@"
    log "Done"
}

main "$@"

19. Reusable Config Loader

#!/bin/bash
load_config() {
    local file="${1:-/etc/myapp.conf}"
    if [ ! -f "$file" ]; then
        echo "Config not found: $file" >&2
        return 1
    fi
    # shellcheck disable=SC1090
    source "$file"
}
load_config ./myapp.conf
echo "Host: $HOST"

20. Colors as Functions

#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'

success() { echo -e "${GREEN}$1${NC}"; }
failure() { echo -e "${RED}$1${NC}" >&2; }

success "All good"
failure "Something broke"
[ All good ]      # in green
[ Something broke ]  # in red, on stderr

Visual: Function Anatomy

┌──────────────────────────────────────────────┐
│           Function structure                 │
│                                              │
│  name() {                                    │
│      local var="value"                       │
│      echo "Using $1"                         │
│      return 0                                │
│  }                                           │
│    │                                         │
│    │  ┌─────────────────────────────────┐    │
│    │  │ name      → function name       │    │
│    │  │ ()        → no parameters here  │    │
│    │  │ { }       → body                │    │
│    │  │ local     → function-local var  │    │
│    │  │ $1        → first argument      │    │
│    │  │ echo      → output (return val) │    │
│    │  │ return    → exit status (0–255) │    │
│    │  └─────────────────────────────────┘    │
│    │                                         │
│    ▼                                         │
│  result=$(name arg1 arg2)                    │
│                                              │
└──────────────────────────────────────────────┘

Visual: Return vs Echo

┌──────────────────────────────────────────────┐
│           Two ways to "return"               │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │  return N (exit status)                │  │
│  │                                        │  │
│  │  myfunc() {                            │  │
│  │      if [ ... ]; then                  │  │
│  │          return 0    ← success         │  │
│  │      else                              │  │
│  │          return 1    ← failure         │  │
│  │      fi                                │  │
│  │  }                                     │  │
│  │                                        │  │
│  │  if myfunc; then echo "ok"; fi         │  │
│  └────────────────────────────────────────┘  │
│                                              │
│  ┌────────────────────────────────────────┐  │
│  │  echo VALUE (capture with $())         │  │
│  │                                        │  │
│  │  myfunc() {                            │  │
│  │      echo "result"                     │  │
│  │  }                                     │  │
│  │                                        │  │
│  │  x=$(myfunc)                           │  │
│  │  echo "$x"                             │  │
│  └────────────────────────────────────────┘  │
│                                              │
└──────────────────────────────────────────────┘

Summary

ConceptSyntaxExample
Define (POSIX)name() { ... }greet() { echo hi; }
Define (bash)function name { ... }function greet { echo hi; }
Callnamegreet
Call with argsname arg1 arg2greet "Kronos" 30
First arg$1echo "$1"
Arg count$#echo "$#"
All args"$@"for a in "$@"; do
Local variablelocal var=vallocal sum=$(( $1 + $2 ))
Return valueecho ... + $()x=$(add 5 3)
Return statusreturn Nreturn 0
Default arg${1:-default}local name="${1:-Guest}"
List functionsdeclare -fdeclare -f
Remove functionunset -f nameunset -f greet

Key takeaways:

  • Functions encapsulate reusable blocks — define once, call many times
  • Two syntaxes: name() { } (portable) and function name { } (bash)
  • Define functions before calling them
  • Arguments are accessed with $1, $2, …, and $@ for all
  • $0 inside a function is the script name, not the function name
  • Use local to keep variables from leaking into the global scope
  • Functions return a status (0–255), not a value
  • To return a value, echo it and capture with $(...)
  • Use return N for success/failure and if fn; then to test
  • For multiple return values, use echo + readarray or read
  • Use "$@" in loops to preserve argument boundaries
  • Always validate arguments — check $# and provide defaults
  • The main function pattern — define functions at the top, then call main "$@" at the bottom

Remember: Functions turn a script from a list of commands into a program. Define them at the top, use local for internals, pass arguments as $1/$2, return values with echo and $(...), and use return for status. Watch out for the two big gotchas: $0 is the script name, and return only takes 0–255. Master functions, and your scripts become modular, testable, and readable.

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!