Functions I have known
In my previous post (and yes, I know I promised to write this sequel “soon” but shit happens), I talked about shell aliases and shared some of my favorites. I ended that post by mentioning that both Bash and Zsh recommend using functions instead of aliases for anything more complex than a single command. Today, I want to dive into functions, explain why they’re more powerful than aliases, and share some functions I use regularly.
Why functions over aliases?
Aliases are great for simple command substitutions, but they have limitations:
- They don’t accept arguments in a flexible way
- They can’t contain control structures like conditionals or loops
- They’re designed for interactive use, not scripting
Functions solve these problems. They’re essentially mini-scripts that live in your shell configuration, can accept arguments, run complex logic, and even return values.
Creating functions
Let’s start with a basic function:
function mkcd() { mkdir -p "$1" && cd "$1"}
This function you’ll see in a lot of people’s dotfiles. It creates a directory and then changes into it. The syntax breaks down as:
function mkcd()
- Declares a function namedmkcd
{}
- Curly braces contain the function body"$1"
- This is the first argument passed to the function
You can also use a shorter syntax that omits the function
keyword:
mkcd() { mkdir -p "$1" && cd "$1"}
Both are equally valid in Bash and Zsh, but I prefer the explicit function
keyword for clarity.
Arguments in functions
Arguments in shell functions are accessed using positional parameters:
$1
,$2
,$3
, etc. - Individual arguments$@
- All arguments as separate strings$*
- All arguments as a single string$#
- Number of arguments
Here’s a function that demonstrates this:
function argdemo() { echo "First argument: $1" echo "Second argument: $2" echo "All arguments: $@" echo "Number of arguments: $#"}
If you run argdemo hello world
, you’ll get:
First argument: helloSecond argument: worldAll arguments: hello worldNumber of arguments: 2
Permanent functions
Like aliases, functions only exist for the duration of your shell session unless you add them to your shell configuration. I usually place my functions in ~/.bashrc
or ~/.zshrc
, just like aliases. For organization, you might create a separate file like ~/.bash_functions
or ~/.zsh_functions
and source it from your main configuration file:
# Add this to your .bashrc or .zshrcif [ -f ~/.bash_functions ]; then . ~/.bash_functionsfi
Some functions I use regularly
Most of these have been copied or adapted from other folk’s dotfiles that I’ve read over the years, check out this search for dotfiles repos you can check out for ideas.
Directory manipulation
We already saw this function, which creates a directory and changes into it in one step:
function mkcd() { mkdir -p "$1" && cd "$1"}
Here’s one that extracts almost any compressed file (which I think I adapted from here):
function extract() { if [ -f "$1" ]; then case "$1" in *.tar.bz2) tar xjf "$1" ;; *.tar.gz) tar xzf "$1" ;; *.bz2) bunzip2 "$1" ;; *.rar) unrar e "$1" ;; *.gz) gunzip "$1" ;; *.tar) tar xf "$1" ;; *.tbz2) tar xjf "$1" ;; *.tgz) tar xzf "$1" ;; *.zip) unzip "$1" ;; *.Z) uncompress "$1" ;; *.7z) 7z x "$1" ;; *) echo "'$1' cannot be extracted via extract()" ;; esac else echo "'$1' is not a valid file" fi}
This extract
function demonstrates a key advantage of functions over aliases: conditional logic. It checks the file extension and uses the appropriate extraction command.
Git helpers
Here’s a function to create a new git branch and switch to it in one command:
function gcb() { git checkout -b "$1" && git push -u origin "$1"}
Want to easily clean up old, merged git branches? Here’s a function for that:
function gitclean() { # Switch to main branch git checkout main || git checkout master # Update from remote git pull # Remove branches that are already merged git branch --merged | grep -v "\*" | grep -v "main" | grep -v "master" | xargs -n 1 git branch -d # Prune remote tracking branches git fetch --prune}
Docker helpers
This one lists containers with their IP addresses, useful for debugging:
function dips() { docker ps -q | xargs -n 1 docker inspect --format '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' | sed 's#^/##'}
Need to easily clean up Docker resources? Here’s my nuclear option:
function dclean() { echo "Stopping all containers..." docker stop $(docker ps -a -q) echo "Removing all containers..." docker rm $(docker ps -a -q) echo "Removing all images..." docker rmi $(docker images -q) echo "Removing all volumes..." docker volume prune -f echo "Removing all networks..." docker network prune -f echo "Done!"}
Kubernetes functions
Kubernetes commands are notoriously verbose. These functions make common tasks simpler:
function kuse() { kubectl config use-context "$1"}
function kns() { kubectl config set-context --current --namespace="$1"}
function kdump() { kubectl get "$1" -o yaml}
The kuse
function quickly switches Kubernetes contexts, kns
changes the current namespace, and kdump
dumps a resource’s YAML configuration.
Working with JSON and APIs
I often need to format JSON data. This function pipes JSON through jq
with color highlighting:
function json() { if [ -t 0 ]; then # If input is from a file cat "$1" | jq -C . | less -R else # If input is from a pipe jq -C . | less -R fi}
Here’s a function to make HTTP requests and automatically format JSON responses:
function jsonget() { curl -s "$1" | json}
Better defaults
This function adds useful defaults to the find
command for finding files:
function ff() { local path=${2:-.} find "$path" -type f -name "*$1*" 2>/dev/null}
To find a file containing “config” in the current directory, just run ff config
.
Fun with functions
Here’s a silly but useful function to remind myself to take breaks:
function focus() { local minutes=${1:-25} local seconds=$((minutes * 60))
echo "Focus time: $minutes minutes. GO!" sleep $seconds
# On macOS if [[ "$(uname)" == "Darwin" ]]; then osascript -e 'display notification "Time to take a break!" with title "Focus Timer"' say "Time to take a break" # On Linux else notify-send "Focus Timer" "Time to take a break!" fi}
This implements a simple Pomodoro timer that reminds you when your focus time is up.
Functions vs scripts
You might be wondering when to use a function versus when to create a standalone script. Here’s my rule of thumb:
- Use functions for operations you commonly perform in your shell
- Use scripts for more complex operations, things that need to be portable, or operations that might be used by other users or systems
Functions live in your shell environment, so they can interact with your current directory and environment variables without special handling. Scripts, on the other hand, run in their own process and need to be made executable and placed in your PATH.
Advanced function techniques
Return values
In shell functions, the return value is actually an exit status (0-255), with 0 meaning success:
function is_even() { if (( $1 % 2 == 0 )); then return 0 # Success (true) else return 1 # Failure (false) fi}
if is_even 42; then echo "42 is even"else echo "42 is odd"fi
To return actual string or numeric values, you can echo the result and capture it:
function double() { echo $(($1 * 2))}
result=$(double 5)echo "Double of 5 is $result"
Local variables
Use the local
keyword to declare variables that are only accessible within the function:
function count_files() { local directory="${1:-.}" local count=$(ls -1 "$directory" | wc -l) echo "Files in $directory: $count"}
Without local
, variables defined in functions would pollute your global shell environment.
Default arguments
You can provide default values for function arguments:
function greet() { local name="${1:-World}" echo "Hello, $name!"}
The syntax ${1:-World}
means “use the first argument, but if it’s not provided, use ‘World’”.
Debugging functions
To debug a shell function, you can use the set -x
command to enable tracing:
function debug_me() { set -x # Turn on debugging echo "Argument 1: $1" local result=$(($1 * 2)) echo "Result: $result" set +x # Turn off debugging}
When you call this function, you’ll see each command printed before it’s executed.
Conclusion
Functions are an essential tool in any shell user’s toolkit. They allow you to create custom commands that fit your workflow, accept arguments, include complex logic, and even return values. I find they strike the perfect balance between quick aliases and full-blown scripts.
In my workflow, I start with aliases for simple command substitutions. When I find myself wanting to add arguments or logic, I upgrade to a function. When a function grows too complex or needs to be shared, I graduate it to a standalone script (or write an app, or a Hammerspoon Spoon, or … you get the idea).
I hope these examples inspire you to create your own functions. Don’t be afraid to experiment! One of the joys of shell functions is how quickly you can iterate on them.