I was recently part of a team workshop where we had to write some automation scripts, and call me old, but I was shocked to see a lot of the team calling binaries from python
, rather than writing a bash
script to do all of the processing. After talking to a few colleagues, I discovered that bash
is not natural to them, like python
is.
I never went to university, so all of my computer science stuff has very much been self taught, so I suppose that could be the reason why I'd prefer to write "parse.sh
" rather than "parse.py
". But this got me thinking, bash
is really not that hard, we use the shell every day, so I wanted to write up why I'd prefer to write a bash
script over a python
one.
Getting help
The biggest gripe I've heard about writing bash
scripts is that you need to learn each command's syntax, and although not wrong, it shouldn't be a show stopper. Every command has either a man
page or at the very least a help file. Now days with LSP's and AI's, learning or writing bash
scripts with specific syntax may slow you down to begin with, but you could say that the same about picking up anything new.
A good example of this is the uname
command. I would normally use this command to identify the operating system the script is being ran on. But even after many years of writing bash
scripts, I can never remember the flag to just output the OS flavour, yet the man
page will tell me.
man uname
UNAME(1) General Commands Manual UNAME(1) NAME uname – display information about the system SYNOPSIS uname [-amnoprsv] DESCRIPTION The uname command writes the name of the operating system implementation to standard output. When options are specified, strings representing one or more system characteristics are written to standard output. The options are as follows: -a Behave as though the options -m, -n, -r, -s, and -v were specified. -m Write the type of the current hardware platform to standard output. (make(1) uses it to set the MACHINE variable.) -n Write the name of the system to standard output. -o This is a synonym for the -s option, for compatibility with other systems. -p Write the type of the machine processor architecture to standard output. (make(1) uses it to set the MACHINE_ARCH variable.) -r Write the current release level of the operating system to standard output. -s Write the name of the operating system implementation to standard output. -v Write the version level of this release of the operating system to standard output. If the -a flag is specified, or multiple flags are specified, all output is written on a single line, separated by spaces. ENVIRONMENT An environment variable composed of the string UNAME_ followed by any flag to the uname utility (except for -a) will allow the corresponding data to be set to the contents of the environment variable. The -m, -n, -r, -s, and -v variables additionally have long aliases that have historically been honored on MacOS, “UNAME_MACHINE”, “UNAME_NODENAME”, “UNAME_RELEASE”, “UNAME_SYSNAME”, and “UNAME_VERSION” respectively. These names have a higher priority than their shorter counterparts described in the previous paragraph. See uname(3) for more information. EXIT STATUS The uname utility exits 0 on success, and >0 if an error occurs. EXAMPLES The hardware platform (-m) can be different from the machine's processor architecture (-p), e.g., on 64-bit PowerPC, -m would return powerpc and -p would return powerpc64. SEE ALSO hostname(1), machine(1), sw_vers(1), sysctl(3), uname(3), sysctl(8) STANDARDS The uname command is expected to conform to the IEEE Std 1003.2 (“POSIX.2”) specification. HISTORY The uname command appeared in PWB UNIX 1.0, however 4.4BSD was the first Berkeley release with the uname command. The -K and -U extension flags appeared in FreeBSD 10.0. The -b extension flag appeared in FreeBSD 13.0. macOS 14.5 November 13, 2020 macOS 14.5
OK great, so its the -s
flag. I could then use this like so to determine the OS its being ran on.
if [[ $(uname -s) == "Darwin" ]]
then
echo "do stuff"
else
echo "fail"
fi
do stuff
Everything is a command
In the previous example, I tried to include some intermediate bash
syntax, as well as keeping it relatively simple. I'm talking about the double [
brackets and later in this section I'll talk about what the $()
is all about.
Basically, everything in bash
is considered a command, so that first line can be broken up like;
if condition in first set of [ ] condition commands in second set of [ ]
Because everything is a command, the [
is actually a command, and shorthand for the test
command. This can be verified by the man
page.
man [
TEST(1) General Commands Manual TEST(1) NAME test, [ – condition evaluation utility SYNOPSIS test expression [ expression ] DESCRIPTION The test utility evaluates the expression and, if it evaluates to true, returns a zero (true) exit status; otherwise it returns 1 (false). If there is no expression, test also returns 1 (false). ..SNIP..
This is where a lot of people I've spoken to find the variable assignment in bash
a little abnormal. As a space would also be considered an argument.
TEST1 = A
TEST2=B
echo $TEST1
echo $TEST2
/bin/bash: line 1: TEST1: command not found B
Now what if I had wanted to assign the output of a command to a variable that I can then later use ? This is where the $()
syntax comes into play. This is a sub shell, where the output of the command inside the $()
will get executed. The easiest way for me to show this is with an example.
OS=$(uname -s)
echo $OS
Darwin
Everything is "just text"
There are no types in bash
, which if you ask me makes life much easier. Because everything is just text, it does mean that there are a lots of ways to parse text. Programs like sed
, tr
and awk
all have their own syntax, where I prefer awk
for larger text processing, and use tr
for simple things, like replacing a character in a string. I also avoid sed
as much as possible.
Recently I was very proud of myself by implementing somewhat of a dictionary in a script (yes i know its actually disgusting).
function foo() {
declare -a values=("a:1" "b:2" "c:3" "d:4")
for v in ${values[@]}
do
KEY=$(echo $v | cut -d ":" -f 1)
VALUE=$(echo $v | cut -d ":" -f 2)
echo -e "key is: $KEY, and value is: $VALUE"
done
}
foo
key is: a, and value is: 1 key is: b, and value is: 2 key is: c, and value is: 3 key is: d, and value is: 4
Finishing up
This post got quit a bit longer that I intended for it to be, and there is a lot more that I could write with regards to bash
scripting. One of the main ones is the many many pitfalls that you should be aware of. However, I'm going to wrap up this post here, and leave you with some beautiful projects written in bash
.