Demystifying a Common Shell Script

Axel Hodler
3 min readMay 13, 2020
Image by Susbany on Pixabay

Being around for a while each of us might have seen the following code

if [ -z $1 ]; then
echo "please provide the input parameter foobar"
exit 1
fi
# do some work

In the wild it can be found at the beginning of some shell scripts. It is a guard clause.

Our script requires an input and fails if the length of the input is zero. If the length is zero it’s not present. Thus the -z.

Without the input it would make no sense to continue with the execution of thhe script. As a result we exit with a non-zero status. exit 1.

After a quick search for “bash check if input is provided” we could grab the above straight from Stackoverflow. No further considerations. It’s fine 😉

Let’s have some fun with the shell to dive deeper.

We regard> as the prompt and the line which follows afterwards as the output.

> [ -z "not empty"
[: ']' expected
> [ -z "" ] && echo "is empty"
is empty

Hah. Seems like [ is a command. No if required.

We check out the manual (manpages)

> man [

Typing /-z we search and find the docs of the -z argument stating

-z | string | True if the length of string is zero.

It also states [ is the utility test. In fact we’re able to interchange [ with test.

> test -z "" && echo "is empty"
is empty

We can even build an alternative to test in our script. For the sake of improving the readability.

We formulate the most important requirements of our alternative assert-emptyinto tests.

failed() {
echo "failed"
exit 1;
}

./assert-empty "hi" && failed
./assert-empty "" || failed
./assert-empty || failed

Voila. A home grown test framework which consists of a single function.

The && failed leads to a failure if the command before exits successfully. Which it should not.

And || failed triggers if the command before exits unsuccessfully. Which again, it should not.

Let’s go for a quick and dirty implementation in C. If we have no input or if the input has a length of zero we exit successfully.

#include <string.h>

int main(int argc, char *argv[]) {
if (argc == 1 || strlen(argv[1]) == 0) {
return 0;
} else {
return 1;
}
}

We compile the above

gcc -o assert-empty assert-empty.c

And run the tests

sh ./test-assert-empty

Success!

Copy the compiler output to /usr/local/bin to reuse it

cp assert-empty /usr/local/bin/

The script above could now start with

if assert-empty $1; then
echo "please provide the input parameter foobar"
exit 1
fi

We should not use it in our scripts though. Their portability would be destroyed. Only our system will have the assert-empty utility.

Having a look at the original source of test in C we find it offers a lot more than our assert-empty. It had time to mature. The commit Initial revision of the file dates back to November 1992.

Spelunking around the tests folder of the repository we find how test is used to test other coreutils, such as rm.

Check out some cool parts in one of the files used to test rm

mkdir -p b/a/p
#...
rm -rf b
#...
test -d b/a/p || fail=1

We recognise the classic Arrange-Act-Assert.

  • Arrange: We create a directory.
  • Act: We delete the directory
  • Assert: We verify whether it still exists with test

Thus what seemed like an innocent bracket [ helps to make sure the foundations we build upon run smoothly.

--

--

Axel Hodler

Building things. Usually by writing code. www.hodler.co. Software Engineering @porschedigital