Functions

Named groups of commands that can be called with arguments.

Source: src/scripting/control_flow.f90:1201-1216, src/ast/evaluator_simple_real.f90:2498-2589, src/execution/builtins.f90:2307-2539

Defining Functions

Standard Syntax

function name {
    commands
}

POSIX Syntax

name() {
    commands
}

Both forms are equivalent. Function definitions are stored in the shell's function table and can be called by name.

Calling Functions

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

greet "World"
# Output: Hello, World!

Arguments

Functions receive arguments as positional parameters:

show_args() {
    echo "Function: $0"      # Script name (not function name)
    echo "Arg count: $#"
    echo "All args: $@"
    echo "First: $1"
    echo "Second: $2"
}

show_args one two three

The caller's positional parameters are saved before function execution and restored afterward (evaluator_simple_real.f90:2551-2553).

Local Variables

Source: builtins.f90:2307-2371

Variables declared with local are scoped to the function:

outer="global"

myfunc() {
    local outer="local"
    echo "Inside: $outer"
}

myfunc
echo "Outside: $outer"
# Output:
# Inside: local
# Outside: global

Declaration Forms

func() {
    local var              # Declare without value
    local var=value        # Declare with value
    local a=1 b=2 c=3      # Multiple declarations
    local readonly=5       # "readonly" is just a variable name here
}

Scope Rules

  • Local variables shadow global variables of the same name
  • Modifications to local variables don't affect globals
  • Child functions can see parent's local variables (dynamic scope)
  • Local declarations only valid inside functions (error otherwise)
outer() {
    local x=1
    inner
}

inner() {
    echo "x=$x"  # Sees outer's local x
}

outer
# Output: x=1

Implementation

Local variables are stored in a 2D array indexed by [function_depth, variable_index]. Each function level has its own count via local_var_counts. There's a limit of MAX_LOCAL_VARS per function level.

Return Statement

Source: builtins.f90:2510-2539

Exit a function with an optional status code:

check_file() {
    if [[ ! -f "$1" ]]; then
        return 1
    fi
    return 0
}

if check_file "/etc/passwd"; then
    echo "File exists"
fi

Return Values

return          # Return with last command's exit status
return 0        # Success
return 1        # Failure
return N        # Return specific status (0-255)

Context Requirements

return is only valid inside:

  • Functions (function_depth > 0)
  • Sourced scripts (source_depth > 0)

Outside these contexts, return produces exit status 2.

Returning Data

Since return only provides a numeric status, use other methods for data:

# Command substitution
get_hostname() {
    hostname -s
}
result=$(get_hostname)

# Global variable
get_hostname() {
    RESULT=$(hostname -s)
}
get_hostname
echo "$RESULT"

# Nameref (if supported)
get_hostname() {
    local -n ref=$1
    ref=$(hostname -s)
}
get_hostname myvar
echo "$myvar"

Recursion

Functions can call themselves:

factorial() {
    local n=$1
    if [[ $n -le 1 ]]; then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}

factorial 5
# Output: 120

Note: Deep recursion may hit control stack limits.

Function Attributes

Export Functions

Make a function available to child processes:

myfunc() {
    echo "Hello"
}
export -f myfunc

bash -c 'myfunc'  # Works in subshell

List Functions

declare -f           # List all functions with bodies
declare -F           # List function names only
type myfunc          # Show function definition

Unset Functions

unset -f myfunc

Common Patterns

Argument Validation

process_file() {
    if [[ $# -lt 1 ]]; then
        echo "Usage: process_file <filename>" >&2
        return 1
    fi

    local file="$1"
    if [[ ! -f "$file" ]]; then
        echo "Error: $file not found" >&2
        return 1
    fi

    # Process file...
}

Default Arguments

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

greet          # Hello, World!
greet "User"   # Hello, User!

Error Handling

die() {
    echo "Error: $*" >&2
    exit 1
}

[[ -f config ]] || die "Config file not found"

Cleanup on Exit

cleanup() {
    rm -f "$tmpfile"
}

main() {
    trap cleanup EXIT
    tmpfile=$(mktemp)
    # Work with tmpfile...
}

Option Parsing in Functions

create_user() {
    local name="" shell="/bin/bash" home=""

    while [[ $# -gt 0 ]]; do
        case $1 in
            -n|--name)  name="$2"; shift 2 ;;
            -s|--shell) shell="$2"; shift 2 ;;
            -h|--home)  home="$2"; shift 2 ;;
            *) break ;;
        esac
    done

    # Create user with $name, $shell, $home
}

create_user --name john --shell /bin/zsh

Library Pattern

# lib.sh
_lib_loaded=1

lib_init() {
    # Initialize library
}

lib_cleanup() {
    # Cleanup
}

# main.sh
source lib.sh
lib_init
# Use library...
lib_cleanup

Special Functions

command_not_found_handle

Source: src/execution/executor.f90:738-791

When you type a command that doesn't exist on PATH, fortsh checks whether a function named command_not_found_handle is defined. If it is, fortsh calls it instead of printing the default error message.

This matches bash's behavior and is useful for suggesting corrections, installing missing packages, or logging unknown commands.

command_not_found_handle() {
    echo "fortsh: '$1': command not found" >&2
    return 127
}

Arguments

The function receives the command name as $1 and all original arguments as $2, $3, etc:

command_not_found_handle() {
    local cmd="$1"
    shift
    echo "Unknown command: $cmd (args: $@)" >&2
    return 127
}

# Typing: foobar -x hello
# Output: Unknown command: foobar (args: -x hello)

When It Triggers

The handler runs only when all of these are true:

  1. The command is not a builtin, alias, or function
  2. The command name contains no / (path-based commands like ./foo skip the handler)
  3. The command is not found anywhere on PATH
  4. The command builtin is not active (i.e., command missing_cmd bypasses the handler)

Practical Examples

Suggest similar commands:

command_not_found_handle() {
    echo "fortsh: $1: command not found" >&2

    # Suggest packages on Fedora/RHEL
    if type dnf &>/dev/null; then
        echo "Try: sudo dnf install $1" >&2
    fi

    return 127
}

Log unknown commands:

command_not_found_handle() {
    echo "$(date): unknown command '$1'" >> ~/.fortsh_missing.log
    echo "fortsh: $1: command not found" >&2
    return 127
}

Return Value

The handler's exit status becomes $?. By convention, return 127 (the standard "command not found" code). If the handler returns non-zero, the ERR trap fires as usual.

Default Behavior

If no command_not_found_handle function is defined, fortsh prints a color-highlighted error with "Did you mean?" suggestions when running interactively.