Bash shell scripting introduction
Shells come in many varieties, the two that I am most familiar with are bash
and zsh
. I use zsh
and in fact, since 2019, macOS has been shipping with zsh
as the default shell. Being able to effectively write a shell script is a super power. There is a ton of automation that can be done with shell scripts, and what’s possible to write is only limited by your imagination. The purpose of this post is to give a high-level overview of what shell scripts are, and to dive right in to an introduction of things to know when crafting shell scripts. We’ll only skim the surface of shell scripts by reviewing variables, the test
command, if-else statements, and some other basics to get you started.
A hello world script
Let’s dive right into it with a Hello World!
script!
#!/bin/sh
# This is a bash script to display 'Hello World!'
echo "Hello World!"
Breakdown:
#!/bin/sh
- the ‘shebang’, this is needed at the top of the file to indicate that it is a shell script# This is a bash script to display 'Hello World!'
- anything starting with#
is a single-line commentecho " Hello World!"
- theecho
command just echos back whatever input was made
We can see in this StackOverflow article that the ‘shebang’ tells the OS what interpreter to use to run the program. I don’t have a perl
or php
interpreter on my local, so replacing the shebang with examples other than sh
such as:
false
perl
php
results in failed execution of the script, but once we revert it to sh
then it can run successfully:
You’ll also want to run chmod +x <name of file>
in order to be able to execute the program. And as seen in the above screenshot, you execute the program by prepending ./
to the name of the file.
Now that we’ve got the very basics out of the way, let’s look into some other features of programming shell scripts.
Initializing variables
To initialize a variable, type in the name of the variable that you want to initialize followed by an equal (=
) sign and the value that you want to assign it, some examples:
x=test
my_name=Elijah
the_year=2023
You can’t space out the end of the variable name from the equal sign to the beginning of the variable value. You also can’t simply declare a variable without initializing it. This is because if you simply try to declare a variable, you’ll receive a command not found:
error
To see the value of the variable, run: echo $variable
. Here are some basic variable rules regurgitated from here:
- A variable in single quotes
'
is treated as a literal string, and not as a variable. - Variables in quotation marks
"
are treated as variables. - To get the value held in a variable, you have to provide the dollar sign $.
- A variable without the dollar sign $ only provides the name of the variable.
The availability of the variable only lasts as long as the command-line window or program is active.
You can also reference other variables in another variable, here’s an example:
combined_variable="$x $my_name"
will print outtest Elijah
when used with theecho
statement:echo $combined_variable
The test
command
While I was learning about shell scripts initially, I remember being frightened away from them because of all of the ‘weird’ syntax is that gets combined together, this was several years ago now, because I never really dove into the inner workings of said syntax.
Today, however, I can’t help but to get entrenched in fascination with the ways in which completely different syntax can perform the same task.
Take the test
command for instance, running man test
prints out the following on the command line:
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).
What follows in the manual are a series of flags that you can use in combination with the test
command to run different kinds of boolean evaluations (don’t worry about memorizing, or frankly becoming super familiar with all or any of these flags):
-b file True if file exists and is a block special file.
-c file True if file exists and is a character special file.
-d file True if file exists and is a directory.
-e file True if file exists (regardless of type).
-f file True if file exists and is a regular file.
-g file True if file exists and its set group ID flag is set.
-h file True if file exists and is a symbolic link. This operator is retained for compatibility with previous versions of
this program. Do not rely on its existence; use -L instead.
-k file True if file exists and its sticky bit is set.
-n string True if the length of string is nonzero.
-p file True if file is a named pipe (FIFO).
-r file True if file exists and is readable.
-s file True if file exists and has a size greater than zero.
-t file_descriptor
True if the file whose file descriptor number is file_descriptor is open and is associated with a terminal.
-u file True if file exists and its set user ID flag is set.
-w file True if file exists and is writable. True indicates only that the write flag is on. The file is not writable on a
read-only file system even if this test indicates true.
-x file True if file exists and is executable. True indicates only that the execute flag is on. If file is a directory,
true indicates that file can be searched.
-z string True if the length of string is zero.
-L file True if file exists and is a symbolic link.
-O file True if file exists and its owner matches the effective user id of this process.
-G file True if file exists and its group matches the effective group id of this process.
-S file True if file exists and is a socket.
file1 -nt file2
True if file1 exists and is newer than file2.
file1 -ot file2
True if file1 exists and is older than file2.
file1 -ef file2
True if file1 and file2 exist and refer to the same file.
string True if string is not the null string.
s1 = s2 True if the strings s1 and s2 are identical.
s1 != s2 True if the strings s1 and s2 are not identical.
s1 < s2 True if string s1 comes before s2 based on the binary value of their characters.
s1 > s2 True if string s1 comes after s2 based on the binary value of their characters.
n1 -eq n2 True if the integers n1 and n2 are algebraically equal.
n1 -ne n2 True if the integers n1 and n2 are not algebraically equal.
n1 -gt n2 True if the integer n1 is algebraically greater than the integer n2.
n1 -ge n2 True if the integer n1 is algebraically greater than or equal to the integer n2.
n1 -lt n2 True if the integer n1 is algebraically less than the integer n2.
n1 -le n2 True if the integer n1 is algebraically less than or equal to the integer n2.
As we can see, there are many choices, but I suspect only a few are used often. The -z
flag being one that I know is used often, especially for error handling.
Recall that the -z
flag returns true if the length of string is zero. This would help in situations where a user fails to supply the program a string because the program could simply exit out with some sort of echo
message.
But what was also interesting to me was how these flags can also be used with if-else statements. We’ll talk more about this when we get to that section, but for now, let’s run a few example test
commands to show how it works.
The basic syntax for using the test
command looks like this:
test “var1” operator “var2”
An example looks like this:
test 10 -eq 20
This will run just fine, but we won’t receive any output. Recall that -eq
is an ‘equals’ boolean evaluation. The command-line will silently return false in the background. However, we can add to this script to get human-readable output:
test 10 -eq 20 && echo "true" || echo "false"
This will return false
and if we change it such that the two integers are equal to one another, then it will return true
.
Continuing to build on this, we don’t even have to use the test
command, instead we can use square brackets:
[ 10 -eq 20 ] && echo "true" || echo "false"
Running the above will also return false
. Interesting, those square brackets might come in handy with if-else statements, if you’d like to read more about the test
command, I recommend checking out this link.
If-else statements
If-else statemens are the bread and butter of any programming language, whether we explicitly state them like we do when we explicitly use an if-else statement or implicitly execute an if-else statement like we did with the test
command, the if-else concept is a cornerstone of programming.
If you want to read more about if-else statements, you can check out this link. I’m going to keep my example much more brief.
An example if-else statement program that builds on the test
command discussed earlier looks like this:
#!/bin/bash
echo -n "Enter a number: "
read VAR
if [ $VAR -gt 10 ]
then
echo "The variable is greater than 10."
elif [ $VAR -eq 10 ]
then
echo "The variable is equal to 10."
else
echo "The variable is less than 10."
fi
This program reads a VAR
environment variable, runs an if-else statement of the user inputted value, and then returns a statement about whether the value was greater than, equal to, or less than 10. You can read more about the read
command here.
The VAR
command could have been anything, we’re simply creating an ephemeral environment variable to use in our program until the program exits. A few more things I’d like to point out are:
elif
is just a short-hand forelse-if
- After
if
andelif
we pass thethen
keyword, but not after theelse
- We end if-else statements with the
fi
keyword to finish the statement, this holds true for if, if-else, and if-else-if statements
Notice that we’re using special keywords in combination with the test
command alias of square brackets to craft our if-else statements. Also take note that we’re only using a single set of square brackets, and that we have spaces inside of the square brackets. We could use single or double square brackets, but for reasons that you can read more about here we really only need a single set of square brackets. If we don’t include spaces inside of our square brackets, the program will error, so keep that in mind. With that said, you will receive instant feedback if you forget to include spaces that looks something like this:
./elif_demo.sh: line 6: [: missing `]'
The single most important takeaway that I want to mention with regards to the test
commmand and the if-else statement is that when you are reading through bash scripts that contain if-else statements, you are also implicitly seeing the test
command in action. I thought this was really interesting which is why I wanted to discuss the test
command before diving into the if-else statement. I first learned about this relationship in this StackOverflow link.
Conclusion
I hope you enjoyed reading about some high-level shell scripting capabilities, the possibilities are truly endless with shell scripting, and as one grows more advanced in their information security career, they’ll undoubtedly write shell scripts to help automate tasks to use against targets. As I continue to learn about shell scripting, I may either update this blog, or create additional posts to discuss specific learnings.
For my own learnings, I have begun to maintain a repository that contains bash scripts, you can find it here:
This will continue to grow as I add more scripts that can be used. Ideally, I’d like to separate out the repository into scripts that I found from other people and scripts that I have created myself through the use of some folders.
Cheers!