Chapter 05Lesson 04~70 minutes

Functions, Local Variables, and Return Status

Functions turn shell statements into reusable internal commands. A good function has explicit inputs, local state, a clear success/failure contract, and predictable output channels.

BeginnerLoops & functionsHands-on lab

Learning objectives

By the end of this lesson

  • Define and invoke Bash functions.
  • Use local variables to reduce shared state.
  • Return status separately from data output.
  • Validate function arguments.
  • Control side effects so functions remain composable.

1. Functions create named command units

deploy_service() {
  printf 'deploying\n'
}

deploy_service

Functions run in the current shell environment unless you explicitly introduce a subshell.

2. Function arguments use positional parameters

deploy_service() {
  local service=$1
  local environment=$2
  printf 'service=%s env=%s\n' "$service" "$environment"
}

deploy_service api staging

Inside the function, $1, $2, $#, and "$@" refer to the function call.

3. local avoids accidental global mutation

name="global"

demo() {
  local name="function-local"
  printf 'inside=%s\n' "$name"
}

demo
printf 'outside=%s\n' "$name"
Default habit

Declare function-internal variables with local unless modifying shared shell state is intentionally part of the interface.

4. Bash local variables are dynamically scoped

outer() {
  local mode="staging"
  inner
}
inner() {
  printf 'mode=%s\n' "$mode"
}
outer

Called functions can see active locals from callers. Prefer explicit arguments for dependencies that matter to readability and testing.

5. return communicates command status

is_supported_env() {
  case $1 in
    dev|staging|prod) return 0 ;;
    *) return 1 ;;
  esac
}

if is_supported_env staging; then
  printf 'supported\n'
fi

return does not return an arbitrary string object. It sets the function's exit status.

6. Emit result data on stdout when appropriate

artifact_path() {
  local service=$1
  printf 'looking up %s\n' "$service" >&2
  printf '/srv/artifacts/%s.tar.gz\n' "$service"
}

artifact=$(artifact_path api)
printf 'artifact=%s\n' "$artifact"
Stream contract

When callers use command substitution, stdout is a data interface. Diagnostics belong on stderr.

7. Check status before trusting captured output

lookup_version() {
  local service=$1
  if [[ $service == api ]]; then
    printf '2.14.7\n'
    return 0
  fi
  printf 'unknown service\n' >&2
  return 1
}

if version=$(lookup_version api); then
  printf 'version=%s\n' "$version"
fi

8. Validate at the function boundary

require_file() {
  local path=${1:-}

  if [[ -z $path ]]; then
    printf 'require_file: path required\n' >&2
    return 64
  fi

  if [[ ! -r $path ]]; then
    printf 'unreadable: %s\n' "$path" >&2
    return 1
  fi
}

Early validation keeps empty or malformed state from leaking into lower-level operations.

9. Preserve failure status before cleanup

run_with_cleanup() {
  local tmp status
  tmp=$(mktemp) || return 1

  if perform_work "$tmp"; then
    status=0
  else
    status=$?
  fi

  rm -f -- "$tmp"
  return "$status"
}

Otherwise the cleanup command may accidentally become the function's final status.

10. Confine directory and option side effects

with_workspace() {
  local workspace=$1

  (
    cd "$workspace" || exit 1
    run_build
  )
}

The subshell prevents the directory change from leaking into the caller.

11. Name functions at the domain level

Names such as validate_config, fetch_artifact, wait_for_ready, and deploy_service describe intent. Names such as helper2 merely hide code behind another label.

12. Hands-on lab: function-based deployment plan

mkdir -p "$HOME/devops-academy/bash/chapter05/lesson04"
cd "$HOME/devops-academy/bash/chapter05/lesson04"

cat > functions.sh <<'EOF'
#!/usr/bin/env bash

validate_environment() {
  local environment=${1:-}
  [[ $environment == dev || $environment == staging || $environment == prod ]]
}

artifact_path() {
  local service=${1:-}
  [[ -n $service ]] || return 64
  printf './artifacts/%s.tar.gz\n' "$service"
}

report_plan() {
  local service=$1
  local environment=$2
  local artifact=$3
  printf 'PLAN service=%s env=%s artifact=%s\n'     "$service" "$environment" "$artifact"
}

service=${1:-api}
environment=${2:-staging}

validate_environment "$environment" || exit 2
artifact=$(artifact_path "$service") || exit $?
report_plan "$service" "$environment" "$artifact"
EOF

bash functions.sh api staging
bash functions.sh api qa || true

Verification checklist

13. Knowledge check

Question 1. What does return 0 mean?

Question 2. Why use local?

Question 3. How should a function provide a string result to command substitution?

Question 4. Why save a failure status before cleanup?

14. Summary

Bash functions are internal command interfaces. Give them explicit arguments, local state, clean stdout/stderr behavior, and documented return statuses so they can be reused and tested safely.

15. Further reading

  • GNU Bash Reference Manual — Shell Functions.
  • GNU Bash Reference Manual — local and return.
  • GNU Bash Reference Manual — Shell Parameters.
  • ShellCheck documentation — function scope and status.
Next lesson

Breaking Large Scripts into Reusable Components

Continue Chapter 5 by building the next layer of reusable shell logic.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.