Skip to content

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.

Like the aliases in my previous post, these functions will work in both Bash and Zsh shells, with minimal differences. Some might need tweaking for other shells like Fish. The examples here are from my personal collection, evolved over many years across both macOS and Linux systems.

Why functions over aliases?

Aliases are great for simple command substitutions, but they have limitations:

  1. They don’t accept arguments in a flexible way
  2. They can’t contain control structures like conditionals or loops
  3. 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:

Terminal window
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 named mkcd
  • {} - 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:

Terminal window
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:

Terminal window
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: hello
Second argument: world
All arguments: hello world
Number 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:

Terminal window
# Add this to your .bashrc or .zshrc
if [ -f ~/.bash_functions ]; then
. ~/.bash_functions
fi
In Zsh, you can also use the `autoload` mechanism to load functions from separate files, which helps with organization. But that's a topic for another day!

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:

Terminal window
function mkcd() {
mkdir -p "$1" && cd "$1"
}

Here’s one that extracts almost any compressed file (which I think I adapted from here):

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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!"
}
Be careful with this one! It will remove ALL Docker containers, images, volumes, and networks on your system. Use only when you truly want to start fresh.

Kubernetes functions

Kubernetes commands are notoriously verbose. These functions make common tasks simpler:

Terminal window
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:

Terminal window
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:

Terminal window
function jsonget() {
curl -s "$1" | json
}

Better defaults

This function adds useful defaults to the find command for finding files:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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:

Terminal window
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.