What Bash Does Better
-
Bash is eschewed online for being hard to read and write. Depending on who you listen to, you should reach for something else anytime you need something longer than what fits in your terminal. But I’ve been having a great time writing bash scripts. They are at least twice concise as my boss’s equivalent javascript scripts, measuring objectively on lines of code and subjectively on how many lines I have to read to understand what a piece does.
There are some niceties that I like to set up for myself. For example, the die function gives me an easy way to write checks
die() {
echo "$@" >&2
exit 1
}
[[ "$(git rev-parse --is-in-work-tree 2>/dev/null)" == "true" ]] || die "Not in git repo"
I often need to look up the string manipulations, but it’s so simple to read them.
VERBOSE="${VERBOSE:-false}" # set VERBOSE to false if it is unset or null.
Bash’s real super power over a traditional language is the input and output flexibility. Its control flow is second-to-none. The mechanism is super, super simple, strings all around really. But streams are the default, and they are powerful.
while read -r line; do
# run a command every time a notification is sent.
done < <(curl -sN https://ntfy.sh/test/json)
The thing on the last line there looks a little weird, doesn’t it? < <(command) is another super power of bash. How commands can be called. You can treat a file as a string, a string as a file, open a named pipe on your system for inter process communication, inline a script in another language, and more! Bash takes it all in stride. The only thing I’ve seen recently that comes close is WebOrigami.
A=3 B=3 node -e "console.log(Number(process.env.A) + Number(process.env.B))" # 6
# heredoc
cat <<EOF
This is a long
inline string with
enters, spaces and everything!
EOF
# herestring
json='{"this": "Test", "hello": ["sun", "moon", "world"]}'
hello="$(jq -n '.hello[2]' <<<"$line")"
I can even control the process forking of my command with ease. () for a subshell, $() for a subshell, piping stdout bach to me. <() for a subshell, giving me back a file descriptor for it. Functions run in my process automatically, but I can run them in a subshell if I want. Commands may run in a subshell automatically, but I other scripts I can source and run in my process instead.
Lastly, its simple I/O mechanisms are available in every language and it can call any executable on your system with ease, by name. I consider jq part of my basic toolset nowadays. And gum a close second. They’re installed on my system, I don’t need to import them every time. I wrote a parseargs package because I didn’t like the current options and know Node well, now I can call that easily as well! Eventually I’ll rewrite it in Zig to be faster, but my scripts won’t need to change at all.
The importing is a double edged sword, I know. It’s a reason to limit the amount of dependencies I use, luckily bash has a lot of good already in it! But some dependencies are worth convincing other people to use. I trust my taste here and so do my colleagues, but sometimes I will write helpers that don’t require others to have packages installed that I do.
log() {
if command -v gum &>/dev/null; then
gum log --structured --level info "$@"
else
echo "$@"
fi
}
There’s some syntax to learn with bash. It’s probably somewhere between Ruby and Perl in that. There’s no one place to go and learn about it. You probably only use it for quick, one off scripts and don’t fire your whole engineering brain making it robust. But read enough tips and tricks, try new things each time, and I think you’ll find yourself reaching for it more and more often given that you can find it almost everywhere and it is amazingly expressive.