Bash Functions for DevOps
In the previous article we used grep, awk, and sed to process logs and configuration files. As scripts grow longer, a new problem appears: repeated blocks of code — checking files, writing logs, validating environment variables, retrying commands, or creating backups before editing config.
Functions let you group a set of commands into a named block that can be called many times. For DevOps work, this is an important step from scripts that merely “work” to scripts that are easier to read, test, and maintain.
Declaring a Function in Bash
Bash supports two common function declaration styles:
| |
In practice, the name() { ...; } style is used often because it is short and more portable across POSIX-like shells. In this article we focus on Bash, so both styles work.
Call a function like a command:
| |
Pay attention to spaces and the ; when writing a one-line function:
| |
When writing multiple lines, the ; before } is not required because the newline already ends the command.
Passing Parameters with $1, $@, and $#
Inside a function, Bash uses positional parameters just like a script:
$1,$2, …: the first, second, … argument.$@: all arguments, preserving each argument separately when quoted as"$@".$#: the number of arguments.$0: the script name, not the function name.
Example function for checking a service:
| |
When you need to pass all arguments to another command, use "$@":
| |
"$@" preserves argument boundaries. It is safer than $* when arguments contain spaces.
Validating Input Parameters
A function should validate required parameters early so errors are clear.
| |
Using ${1:-} avoids an “unbound variable” error when the script enables set -u and the function is called without enough arguments.
Return Value in Bash Means Exit Code
A Bash function does not return string data like many programming languages. In Bash, return returns an exit code from 0 to 255:
0: success.- Non-zero: error or a special state.
| |
In this example, the function does not need an explicit return. The exit code of the last command ([[ ... ]]) becomes the function exit code.
If you want a function to “return data”, print it to stdout and use command substitution:
| |
A practical rule: use exit codes for success/failure, and stdout for data.
Use Local Variables to Avoid Side Effects
By default, variables inside Bash functions are global within the current shell. Use local to limit a variable to the function scope.
| |
Without local, variables like timestamp or base_name can accidentally overwrite variables with the same name elsewhere in the script. This becomes hard to debug as scripts grow.
A good habit is to declare local near where the variable is used and quote variables when passing them to commands:
| |
Split Functions into Another File with source
When multiple scripts need the same logging, validation, or retry logic, you can move functions into a small library file.
Example structure:
| |
File scripts/lib/log.sh:
| |
File scripts/deploy.sh:
| |
source runs a file in the current shell, so functions from log.sh become available to deploy.sh. In Bash, ${BASH_SOURCE[0]} is better than $0 for locating the current script file when the file is sourced or called from another directory.
DevOps Practice: A Small Logging Library
A minimal logging library should include timestamp, level, and proper stdout/stderr separation.
| |
Naming the internal function _log is a simple convention that signals “do not call this directly from outside.” Bash does not have true private functions, so this is only a convention.
Use it in a healthcheck script:
| |
Here, curl --fail makes HTTP 4xx/5xx responses count as errors, --silent --show-error reduces noise while still showing useful errors, and --max-time prevents the script from hanging too long.
DevOps Practice: Retry a Command with a Function
Retry is a very common pattern when calling APIs, pulling images, checking a service after restart, or running commands that can fail temporarily.
| |
Important details:
shift 2removes the two configuration arguments, leaving the command to run."$@"runs the command while preserving argument boundaries.- The function returns
0as soon as the command succeeds, or1after all attempts fail.
You can combine it with the logging library:
| |
DevOps Practice: A Deploy Script with Clear Functions
This small deploy script splits each step into a function so the flow is easier to read:
| |
The main "$@" pattern gives the script a clear entrypoint. Functions above it define behavior, while main describes the primary execution flow.
Common Mistakes
- Forgetting
local: Variables inside functions can overwrite global variables unexpectedly. - Using
return "text":returnis only for numeric exit codes; useecho/printfto stdout for text data. - Not quoting
"$@": Command wrappers can break when arguments contain spaces. - Calling a function before declaring it: Bash reads and executes from top to bottom; a function must be defined before it is called.
- Using
$1directly withset -u: If the argument is missing, the script fails immediately. Use${1:-}and then validate. - Writing error logs to stdout: Warnings and errors should go to stderr (
>&2) so stdout remains available for data or pipelines.
Implementation Notes
- When applying this to your own project, start with small functions around the parts you repeat most:
log_info,log_warn,log_error.require_file,require_dir,require_env.retryfor commands that may fail temporarily.backup_filebefore editing config.
- Best practices:
- Use
localfor variables inside functions. - Use
returnfor success/failure state, and stdout for data. - Use
"$@"when a wrapper function calls another command. - Put shared libraries in
scripts/lib/*.shand load them withsourceusing a path based on${BASH_SOURCE[0]}. - Keep each function focused on one clear responsibility; if a function grows too long, split it again.
- Use
- Troubleshooting:
- Function returns the wrong status? → Print
$?right after calling it or runbash -x script.sh. - Variable changed unexpectedly? → Check whether a function is missing
local. sourcecannot find the file? → CheckSCRIPT_DIRand test the script from another directory.
- Function returns the wrong status? → Print
🎯 Conclusion
Functions are the natural next step when you write Bash for DevOps beyond a few lines. They help reuse logic, centralize logging/validation/retry code, reduce copy-paste, and make deploy or healthcheck flows easier to understand.
In the next article, we will explore variables and environment management in Bash: local variables vs export, .env, getopts, $IFS, and special variables such as $?, $$, and $!. 🚀
References
- GNU Bash Manual — Shell Functions — Official documentation for functions, positional parameters, and exit status.
- GNU Bash Manual — Bourne Shell Builtins — Reference for
return,shift, and related builtins. - GNU Bash Manual — Bash Variables — Reference for
${BASH_SOURCE[@]}and special Bash variables. - curl Documentation — Reference for
--fail,--silent,--show-error, and--max-timeused in the healthcheck example.
