I used to write a new shell script for everything. Deploy this. Backup that. Check the certs. Each one a separate file in ~/bin with a chmod +x and a prayer.

Then I discovered bash functions in .bashrc and it changed everything. Not in a fancy way, just in a "I don't write 40-line scripts anymore" way.

Here is how I structure my bash functions and why I stopped reaching for standalone scripts.

Terminal with code on screen
Yes, I still use a dark terminal theme. No, I will not apologize for it.

Start With One Function

The mistake I made for years was thinking I needed a script. A script means a file, a shebang, permissions, a path. A function just needs a name and a body.

Here is the first function I ever wrote that actually stuck:

certcheck() {
    local domain="${1:?Usage: certcheck example.com}"
    echo | openssl s_client -connect "$domain:443" 2>/dev/null \\
        | openssl x509 -noout -dates -issuer -subject
}

That's it. Type `certcheck davideandreazzini.co.uk` and I get the cert dates. No file. No chmod. No PATH fiddling. It lives in .bashrc and it's there every time I open a terminal.

Local Variables Are Not Optional

Code editor with shell script
If your function modifies global variables, you have a script. Not a function.

I lost an afternoon once because a function I wrote clobbered `$file` that something else was using. `local` is not a suggestion. It's the difference between a function and a bug factory.

Here is a function I use to find the biggest files in a directory:

bigfiles() {
    local dir="${1:-.}"
    local count="${2:-10}"
    du -ah "$dir" 2>/dev/null | sort -rh | head -n "$count"
}

Two parameters, both with defaults. `local` on everything. If I call `bigfiles /var/log 5` it works. If I call `bigfiles` with nothing, it defaults to the current directory and shows 10. No crashes, no side effects.

Return Codes Matter

Functions that don't return proper exit codes are useless in conditionals. I learned this the hard way when a backup function always "succeeded" even when rsync failed.

safe_backup() {
    local src="$1"
    local dest="$2"
    [ -d "$src" ] || { echo "Source missing: $src"; return 1; }
    rsync -avz --delete "$src/" "$dest/" || return 1
    echo "Backup done: $src -> $dest"
    return 0
}

Now I can do `if safe_backup ~/projects /mnt/backup; then ... fi` and it actually works. The `return` codes propagate. The function is composable. That's the whole point.

Group Them in a File

Terminal screen showing code
One .bashrc, one source line, zero headaches.

Once you have more than 5 or 6 functions, .bashrc gets messy. I keep mine in a separate file and source it:

# ~/.bashrc
if [ -f ~/.bash_functions ]; then
    . ~/.bash_functions
fi

Then ~/.bash_functions is just a list of function definitions. Easy to read. Easy to version control. Easy to copy to a new machine.

I keep that file in a dotfiles repo now. New server? Clone the repo, source the file, done. Every function I need is there.

Use declare -f to Remember What You Wrote

I forget function names constantly. `declare -f` prints the full definition of any function. `declare -F` just lists names. I alias it:

alias funcs='declare -F | sort'
alias showfunc='declare -f'

Type `funcs` and I see every function available. Type `showfunc certcheck` and I see the implementation. No grep, no file hunting.

Conclusion

Bash functions replaced half my scripts. They're faster to write, easier to maintain, and always available. The key is `local` variables, proper `return` codes, and defaults on every parameter.

Start small. One function. See if you miss the script. You won't.