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.
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
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
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
fiThen ~/.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.