Published 1 minute ago
Graeme Peacock is a seasoned Linux expert with more than 15 years of hands-on experience. He has worked extensively with Ubuntu, Gentoo, Arch Linux, Qubes, and Fedora, gaining deep proficiency in everything from routine terminal operations to highly customized system builds.
Graeme began his journey with Ubuntu, quickly mastering the command line and essential system administration skills. A year later, he moved to Arch Linux, where he spent nearly a decade refining his expertise through the installation and configuration of multiple minimalist systems. After some time, he moved to Gentoo, where he configured and compiled both server and desktop environments using normal and hardened profiles and frequently compiled custom kernels. Graeme moved to Qubes in 201…
Published 1 minute ago
Graeme Peacock is a seasoned Linux expert with more than 15 years of hands-on experience. He has worked extensively with Ubuntu, Gentoo, Arch Linux, Qubes, and Fedora, gaining deep proficiency in everything from routine terminal operations to highly customized system builds.
Graeme began his journey with Ubuntu, quickly mastering the command line and essential system administration skills. A year later, he moved to Arch Linux, where he spent nearly a decade refining his expertise through the installation and configuration of multiple minimalist systems. After some time, he moved to Gentoo, where he configured and compiled both server and desktop environments using normal and hardened profiles and frequently compiled custom kernels. Graeme moved to Qubes in 2016, where he has remained ever since.
Graeme has extensive experience with highly configurable tools such as Vim, Neovim, and Emacs, and he maintains his own complex configurations. He is also highly proficient with Bash, Zsh, and dozens of utilities.
Graeme holds a B.S. in software engineering and has a strong passion for programming and web development. He is proficient in Golang, Python, Bash, JavaScript, TypeScript, HTML, and CSS. He also has considerable experience with Docker and is currently working on learning Kubernetes.
If you’re just beginning Bash scripting, you may often find yourself repeating the same commands again and again in your scripts, but a better way exists. I will explain what "DRY" means and how you should use functions to do it.
When I wrote my very first program on Windows, it was a batch script. I essentially winged it, and with minimal understanding, I wrote a very literal set of instructions, peppered with GOTO statements, and repeated myself enough to make the script unreadable.
In programming, DRY means "don’t repeat yourself." Functions are the mechanism to achieve that, and I’ll show you how.
HTG Wrapped 2025: 24 days of tech
24 days of our favorite hardware, gadgets, and tech
A primer on Bash functions
I’m a visual learner and believe it helps to see the subject before the technical details. So here’s a function:
foo() {
echo "Hello, World!"
}
"foo" is the function name, and it’s how we invoke (call) it:
foo
foo
Notice we called the function twice? This is their nature: reusable snippets to make our code "DRY."
We can also use the "function" keyword to define them, but doing so means we cannot use the script in other shells:
function foo () {
echo "Hello, World!"
}
Moving on. A more realistic example is to wrap a useful command:
backup() {
cp "foo.txt" "foo.txt.backup-$(date +%Y%m%d)"
}
This function makes a backup of a file called "foo.txt" by simply appending "backup" and a date to the file name. We will make the function reusable in the next section.
Functions can be much larger, spanning multiple lines. Think of them as mini, reusable scripts or commands.
Passing arguments to a function
Functions are not very useful unless you can pass them values, and we can—they’re called parameters or arguments.
The distinction between these terms is minor. I will call them arguments, but the Bash manual may call them parameters.
Again, an example is best upfront:
foo() {
echo "${1}" "${2}"
}
The "${1}" and "${2}" are the first and second arguments, respectively. You pass them to "foo" like:
foo "Hello, " "World!"
foo "Hello, " "Universe!"
Place "quotations" around words to define their boundaries. Notice that "Hello" also includes a comma and a space? Using quotations this way makes their boundaries clear to Bash.
These are called strings, and I have a more advanced article that covers how to change strings in neat ways.
Anyway, back to functions. "${1}" and "${2}" are called positional parameters in Bash-speak (i.e., parameters with positions). There are multiple ways to access arguments; some are more advanced, but "${1}" and "${2}" are sufficient for beginners.
Let’s use arguments in a real example:
backup() {
cp "${1}" "${1}.backup-$(date +%Y%m%d)"
}
This improves our earlier "backup" function. It takes only one parameter: the filename (or path). Now the function will back up any file you want, which makes it drier than the Sahara Desert.
By enclosing "${1}" in double quotes, it allows Bash to replace (expand) it with the assigned value. Single quotes work differently and cause the string to become literal:
backup() {
echo '${1}' '${1}.backup-$(date +%Y%m%d)'
}
backup "foo.txt"
I’ve changed "cp" to "echo," so you can see what the "${1}" argument looks like. It’s not a variable anymore but a literal value: exactly as you specified it. More on variables below.
There’s one more little concept I want to introduce, which will make your code more readable (extremely important):
backup() {
local file_path="${1}"
cp "$file_path" "$file_path.backup-$(date +%Y%m%d)"
}
"local file_path=${1}" is called a variable assignment. A variable is a storage location for a value. We created a variable called "file_path" and stored "${1}" (the function argument) into it. Now "file_path" is equal to "${1}", and we use it in our command instead.
Use variable names this way to describe what the argument is. Your future self will thank you because you can make sense of it at a glance.
Returning values from a function
Returning values from Bash functions is a little different from other languages. In Bash, we can only return an exit code, which is a number that indicates the success status of a command. For example:
foo() {
return 0
}
foo && echo "success"
The "&&" will "echo" "success" only if "foo" has a successful exit code ("0").
Let’s look at an unsuccessful exit code (i.e., the command failed):
foo() {
return 1 # Non-zero means unsuccessful.
}
foo && echo "success" # Doesn't echo "success".
In short, the return value of a Bash function is only for exit codes.
Moving on. Let’s get a useful value from a function.
In Bash, every output is essentially a string. When a command prints a result (number or text), it’s a string. Think of Bash outputs like messages: you ask a command to do something, and it says a message. When we want to return a value from a function, we print a message with "echo":
foo() {
echo "Hello, World!"
}
If the command (e.g., "echo") fails, the function will implicitly return its exit code—but return a custom exit code as you wish.
foo() {
echo "Hello, World!"
return 0
}
Let’s do something with our result, like we would with any other command:
foo() {
echo "Hello, World!"
return 0 # This won't be processed by "tr".
}
foo | tr '[[:lower:]]' '[[:upper:]]'
"tr" will replace "Hello, World!" echoed from "foo" with an uppercase string. This demonstrates that we can use "echo" to return a value and operate on it like any other command.
To drive the point home, we can operate directly on the outputs of any command:
foo() {
echo "Hello, World!" | tr '[[:lower:]]' '[[:upper:]]'
}
foo | sed 's/WORLD/UNIVERSE/'
Here the function echoes "Hello, World!", which "tr" transforms into uppercase. Outside the function, we piped the result into "sed" and replaced "WORLD" with "UNIVERSE."
Related
Bash functions are essentially reusable wrappers around commands. You can use them to define complex command pipelines or to perform some detailed work and echo the result. They accept arguments and provide exit codes, just like commands do.
You will most often use Bash functions to make your shell life easier; instead of typing out complex command pipelines, create a function and inject arguments. You’d place these in your bashrc file, which makes them available in your shell, just like any other command. Alternatively, you can use them inside a script on your PATH.
Related