Ever found yourself wanting to learn more about Linux shell scripting, but you weren’t sure where to begin? Are you relatively new to the world of Unix-based operating systems and want to extend your skills to do some basic shell programming? This tutorial for beginners will go over the basics of Linux shell scripting with Bash, including creating and running a script, as well as working with strings and loops.
Regardless of the operating system, shell scripts are used to automate repetitive administrative tasks. For example, in Windows you can rename files using File Explorer. But if you need to rename many files, it would be a time-consuming task to use the graphical shell. PowerShell allows you to automate the task and repeat it reliably.
In Linux-based operating systems, Bash and other shells are used to automate tasks like working with files, modifying system configuration, and many other tasks that could otherwise be performed by typing single commands.
To write and run a Bash script, you only need three things:
The terminal emulator is where you’ll type commands and run them by hitting Enter or Return. As for Bash, whether you have it pre-installed will depend on your platform:
To find your Bash version, run the bash –version command. While even older Bash versions give you a lot of power, Bash 3 and 4 both introduce nice shorthand notations for certain basic commands. If a command requires one of these versions of Bash, it will be mentioned below.
In the computing world, a shell is a program that serves as an interface for the underlying OS. A shell can be a graphical user interface (GUI), like the Windows shell.
However, people generally use the term to refer specifically to a command line interface (CLI) — an interface consisting of lines of text that you interact with using only the keyboard. Here are some examples of shell scripting languages for *nix operating systems:
Here, we’ll focus on the Bash shell. It’s a popular free Unix shell preinstalled on most Linux distros and on macOS.
Shells have their own programming language. You use this language to send commands to the shell, which it then executes. You can type these commands into the shell directly, or you can save them to a file — a script — and then execute that file from the shell. The syntax for writing commands is the same in both cases.
This article will cover shell scripting basics to create this file.
Let’s start with some basic shell scripting. To write a simple script we will learn some simple shell scripting commands in Linux:
#!/bin/bash
as the first line.The #!/bin/bash
line is called a “shebang.” It tells your shell that this script should be executed in Bash and should be the first line in your script. If you switch to a different shell, your script will still be run in Bash.
To try this process out yourself, create a file called ‘hello_world’ in your home directory:
#!/bin/bash echo "hello world"
That’s it — you’ve created a Bash script!
Before you can run it, you will probably need to change permissions on the file.
To modify the permissions on our ‘hello_world
‘ file, you would run this specific command in your terminal emulator. This gives the user that owns the file the permission to execute the file.:
chmod u+x 'hello_world'
If you just want to run your shell script, you can skip to the next section. For those curious about the chmod
command, chmod
is short for “change mode”, and serves to change file “modes” (or permissions) in Unix. In Unix-like operating systems, you can set file permissions for 3 classes of users:
u
in chmod).g
).o
).With the chmod
command, you can also use a
to refer to all of these.
Each file has 3 types of permissions (or “modes”):
r
)w
)x
)And you can also add (+
) or remove (-
) permissions.
The first parameter in chmod is a combination of these three — user first, action second, and mode third. Here are a few command examples:
chmod gu+rw 'hello_world'
would add read and write permissions for the owner and the owning group.chmod a-x 'hello_world'
would remove executable permissions for everyone.chmod u+rwx 'hello_world' 'hello_world_2'
would give the owner the permission to read, write, and execute the “hello_world” and “hello_world_2” files.We’ve only covered the very basics of the chmod
command here. There’s also a more complicated, but less verbose way of defining these modes (the “numeric notation”), as well as a different command you can use to see which permissions your files have (ls -l
). We won’t go into these topics here.
Time to execute our first script. In general, to run a script, simply type its path into the terminal emulator and hit enter.
./hello_world
You can use the relative or the absolute path. When using the relative path, always use ./
at the beginning of your command: This tells the terminal to look in the current folder (represented by '.'
), rather than in the directories defined in the PATH
environment variable.
Everything after a #
on a single line in a Bash script is considered a comment. These can be helpful for communicating what a complex line does or giving a rundown of what bigger pieces of your script do.
For example:
#!/bin/bash # # This shell script prints "hello world". # echo "hello world" # This line prints "hello world".
When writing scripts, it can be useful to define variables. In Bash, you do this by typing the variable name and value, separated by an equals sign: VARIABLENAME='VALUE'
.
You should not put spaces next to the equals sign — Bash would think you want to run a process instead.
Use single quotes to surround the value to prevent Bash from interpreting it as something else. In Bash, variables don’t have types — everything is basically a string. It’s up to Bash programs to parse the string as a different type, such as a number.
To refer to a variable’s value, use the variable name preceded by the dollar sign: $VARIABLENAME
.
To try this in practice, you can change your script to this:
#!/bin/bash HELLO="hello variable world" echo $HELLO # should print "hello variable world"
The individual words that you write when typing a command are called arguments. In our chmod u+x 'hello_world'
example, chmod
, u+x
, and 'hello_world'
are three different arguments. chmod
is the command name while u+x
and hello_world
are called parameters — arguments that provide extra information to the command.
In your script, you can access these arguments through variables. To avoid conflicting with local variables, these variables are named using numbers — $0
refers to the command name, $1
is the next argument that follows, $2
the one after, and so on.
Let’s try this out:
#!/bin/bash HELLO="hello $1 world" echo $HELLO
Now, run this script with these parameters:
./hello_world bash script
The output should be hello bash world
, with the first parameter used and the second ignored.
If you wanted bash script
to be seen as one parameter, you would need to put it in quotes:
./hello_world 'bash script'
One of the core things that programmers want to do inside a script is run a piece of code only if a certain condition is met. Bash has the if
statement for that:
NUM=$RANDOM if (( $NUM % 2 )) # if CONDITION then echo "$NUM is odd" fi # this is how you end an if statement
Tip: From here on out, these examples are assumed to be part of a larger script and omit the #!/bin/bash
at the beginning. Don’t forget it as the first line of your script, though!
You can also use else
inside an if statement to specify what to do if a condition is not met, or an elif
(short for “else if“) statement to specify another condition if the first condition wasn’t met:
NUM=$RANDOM if [ $NUM -eq 12 ] then echo "$NUM is my favorite number" elif (( $NUM % 2 )) then echo "$NUM is odd" else echo "$NUM is even"fi
‘fi‘ is used to close the if statement.
Tip: If you’re not sure how to write the condition itself, look into the test
, single brackets ([]
), and double parentheses ((())
) notation.
Now that we’ve covered running code conditionally, let’s look at how to run code a certain number of times as long as a condition is met.
The for
loop is perfect for such a task — specifically its “three expression syntax”. The idea behind it is to assign a loop-specific variable and gradually change it until a certain condition is met. Here’s how it’s structured:
for (( ASSIGNMENT_EXPRESSION ; CONDITION_EXPRESSION ; UPDATE_EXPRESSION )) do COMMANDS done
For example, if you want to have a loop run 10 times with values for i going from 0 to 9, your for
loop might look like this:
for (( i=0; i<10; i++ )) do echo $i done
Let’s break it down:
i=0
is the assignment expression here. It’s run only once before the loop is executed, which is why it’s useful for initializing a variable.i<10
is our condition expression. This expression is evaluated before each iteration of a loop. If it is equal to zero (which means the same as “true” in Bash), the next iteration is not run.i++
is our update expression. It’s run after each iteration of a loop.Aside from the three expression syntax, you can also use the in
keyword to define a for
loop. This alternative syntax is used to iterate through a series of items.
The most basic example is simply to list the set of items you want to iterate through after the in
keyword, separated by spaces. For example:
for i in 0 1 2 3 4 5 6 7 8 9 # space-separated list items do echo $i done
You can also iterate through items output by a command:
for i in $(seq 0 1 9)
The $()
notation is used for command substitution in general — it runs a command and its output is used as the input for the parent command surrounding it.
If you’re iterating through integers, it’s better to use Bash’s built-in range syntax, which is more efficient than the seq
command. However, this syntax is only available in more recent Bash versions:
for i in {0..9}
, available in Bash 3.for i in {0..9..1}
, available in Bash 4, where the last number represents the increment.In the same vein, you can iterate through strings too:
for s in 'item1' 'item2' 'item3'
One of the more common use cases for the for loops discussed in the previous section is iterating through individual files.
To tackle this, we need to cover so-called “glob expansions” first. This is a feature in Bash that let you specify filenames using pattern matching. There are special characters called wildcards that you use to define those patterns.
Before going in-depth with this, let’s look at a few specific examples:
echo *
: A command that returns the names of all files in your current directory, except hidden ones.echo *.txt
: A command that returns the names of all non-hidden files with.a txt
extension in your current directory.echo ????
: A command that returns all four-letter filenames in your current directory.The wildcards that we used were * and ? here. There’s also one more wildcard that we didn’t use. Here’s an overview:
*
) represents any number of characters (including 0) in a file or directory name.?
) represents a single character in a file or directory name.**
) represents any number of characters in a full file path. It’s a feature in Bash 4 and above and has to be enabled by running shopt -s globstar
.[]
) are used to represent a character within a set of symbols in a file or directory name. For example [st]ake
would find files named sake
or take
, but not stake
.Note that all hidden files (which are files with names starting with a period .
) are ignored when using glob expansion.
The square bracket notation allows a bit more complexity, including:
[1-8]
[!3]
To treat one of these special characters as a normal character without any meaning instead, simply put a backslash before it — e.g. \?
.
Now that we’ve covered the basics of glob expansion, let’s take a look at how to use it to iterate through files.
We can simply use the glob operators in the for loop itself. Here’s a simple example of a loop printing the name of each file in the current directory:
for f in * do echo $f done
To print the names of each file in the current directory as well as its subdirectories, check that you’re running Bash 4.0 or above by running bash --version
, and then you can run the following command:
shopt -s globstar # enables using ** for f in ** do echo $f done
Tip: If you’re running an older version of Bash, you won’t be able to use globbing with a for loop for this. Your best course of action would be the find
command, but we won’t go over that in this article.
These are, of course, just some of the simplest loops you can run, but there’s a lot more you can do. For example, to rename all the JPGs in a folder to give them a consistent sequential filename, you could run:
i=1 for f in *.jpg do mv -i -- "$f" "image_$i.jpg" let i=i+1 done
The for loop isn’t the only type of loop we can use in Bash — we also have while
: This type of loop runs as long as a specific condition is true.
The syntax is similar to the for loop syntax:
while CONDITION do COMMANDS done
For a practical example, this is how you could read a file line-by-line (except leading or trailing whitespaces) until you hit the end of the file:
while read -r line do echo "$line" done < FILENAME # Replace FILENAME with the path to a text file you'd like to read
You can also replace a for loop with a while loop, e.g. to iterate from 0 to 9:
i=0 while [ $i -lt 10 ] do echo $i let i=i+1 done
And you could also theoretically run a loop forever. Here’s a command example:
while true do echo "running forever" done
Tip: To kill the script, just press Ctrl+C.
While this infinite loop may seem useless at first glance, it can actually be quite useful, especially when combined with the break
statement.
The break
statement is used to break out of a loop.
This allows you to run an infinite loop and break out of it when any breaking conditions arise.
To look at a simple example, we can replicate our loop running from 0 to 9 with an infinite loop like this:
i=0while true do if [ $i -eq 10 ] then break fi echo $i let i=i+1 done
If you have several nested while loops, you can add a number after the break statement to mention which level of loop to break from: break 1
is the same as break
and will loop out of the closest surrounding loop, break 2
will break out of the loop one level above, etc.
Let’s look at a quick example, this time with for loops, iterating through every four-letter word combination until the word “bash” is hit:
for l4 in {a..z} do for l3 in {a..z} do for l2 in {a..z} do for l1 in {a..z} do echo "$l4$l3$l2$l1" if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ] then break 4 fi done done done done
A related keyword that’s also worth a mention is continue
, which skips to the next iteration of the loop. Just like break
, it also takes an optional numeric argument that corresponds to the loop level.
Here’s a silly example where we skip all words with Es in our shortened four-letter word list:
for l4 in {a..z} do if [ $l4 = "e" ] then continue fi for l3 in {a..z} do if [ $l3 = "e" ] then continue fi for l2 in {a..z} do if [ $l2 = "e" ] then continue fi for l1 in {a..z} do if [ $l1 = "e" ] then continue fi echo "$l4$l3$l2$l1" if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ] then break 4 fi done done done done
We could also perform all these continue
statements at the deepest loop level:
for l4 in {a..z} do for l3 in {a..z} do for l2 in {a..z} do for l1 in {a..z} do if [ $l4 = "e" ] then continue 4 fi if [ $l3 = "e" ] then continue 3 fi if [ $l2 = "e" ] then continue 2 fi if [ $l1 = "e" ] then continue fi echo "$l4$l3$l2$l1" if [ $l4 = "b" -a $l3 = "a" -a $l2 = "s" -a $l1 = "h" ] then break 4 fi done done done done
Sometimes, you want the user to interact with your script directly rather than just using initial script arguments. That’s where the read
command comes in.
To get user input and save it to a variable called NAME
, you’d use this command:
read NAME
This is the simplest form of the command, consisting of just the command name and the variable you want to save the input into.
More frequently, though, you’ll want to prompt the user so that they know what to type. You do that with the -p
argument, after which you write your preferred prompt.
Here’s how you might ask for a name, assigning it to a variable called NAME
:
read -p "Your name: " NAME echo "Your name is $NAME." # this line is here just to show that the name has been saved to the NAME variable
There are a number of characters in Bash that you have to be careful about using. For example, spaces within filenames, quotes within strings, or backslashes just about anywhere.
To tell Bash to ignore their special meaning in certain places, you can either “escape” them or wrap them in a literal quote.
“Escaping” a special character means telling Bash to treat it as just a character without any special meaning. To do that, write a backslash before that character.
Let’s say we have a file named img \ 01 *DRAFT*, for instance. In Bash, we could refer to it this way:
img\ \\\ 01\ \*DRAFT\*
Here’s a non-exhaustive list of special characters in Bash:
If you’re new to Bash, though, it might be a hassle to remember which characters have special meaning. As such, in many situations, it might be easier to use a literal quote: Simply surround the text containing any special characters with single quotes, and all special characters within those quotes will be ignored.
Here’s how that would look for our example:
'img \ 01 *DRAFT*'
What if you do want to use a special character, but escape the others? You could escape every other character using a backslash, but you can also save yourself the trouble and surround everything except that special character with literal quotes.
For example, say you have several files named img \ 01 *v1 DRAFT*, img \ 01 *v2 DRAFT*, img \ 01 *ROUGH DRAFT*, etc., and you want to use a glob expansion to match all those filenames. Here’s what you could write:
'img \ 01 *'*' DRAFT*'
If what you need to write contains a single quote — e.g. img \ 01 *’FINAL’* — you can use a similar strategy, combining literal strings and escaping:
'img \ 01 '\''FINAL'\'
Let’s say you have two or more string variables — a first and last name, for instance:
FIRST_NAME="Johnny"LAST_NAME="Appleseed"
To combine those variables into one string, perhaps with a custom delimiter, simply create a new string consisting of those two variables:
NAME="$LAST_NAME"', '"$FIRST_NAME"
You can also use double-quotes with inline variables for this, using curly brackets to separate variable names from the surrounding text:
NAME="${LAST_NAME}, ${FIRST_NAME}"
Bash also allows the use of the +=
operator to append text to a string, like so:
NAME='Appleseed'NAME+=', 'NAME+='Johnny'
Bash also allows you to use the output of a command inside a string, also known as command substitution. Simply surround your command with $()
. For example, to print a current timestamp, you can run:
echo "Current timestamp: $(date)"
You might also remember this syntax from a previous for loop example, when we iterated through a sequence of integers from 0 to 9:
for i in $(seq 0 1 9) do echo $i done
The substituted command is run in a subshell. This means that, for example, any variables created during the command will not affect the environment in which you’re running your script.
For a more complex script, it’s customary to have it return an exit code — a number between 0 and 255 that tells people whether the script ran successfully or encountered an error.
As for which numbers to use, the official Bash manual specifies these:
N
, the exit status is 128 + N.As you can see, all numbers aside from 0 indicate some type of error. The exit code 1
is commonly used for general errors. In most cases, you won’t need to use any exit code above 2
.
To return an exit code from your script, just use the exit
command. For example, to exit with a code of 2, you would write exit 2
.
If you don’t use the exit
command in your script or use the command without specifying a code, the exit status of the last executed command in your script will be returned.
To get the exit code of the last run command in your shell, use the $? variable: echo $?
.
When writing a longer script or one with repeating chunks of code, you might want to separate some code out into functions. There are two formats you can use to define functions: In both cases, all function code is contained inside curly braces and just the function declaration differs.
The more compact format uses parentheses that follow the function name to declare a function:
function_name () { echo "This is where your function code goes" }
The other format uses the function
keyword in front of a function name:
function function_name { echo "This is where your function code goes" }
Functions must be declared before they are called in your script. You call a function just like you execute a regular command, using the function name as the command:
function_name
In Bash, functions cannot take arguments. To send information to a function, you need to use global variables in your script.
For example:
is_your_name_defined () { if [ -z "$YOUR_NAME" ] then echo "It doesn't seem like I have your name." else echo "Is this your name: ${YOUR_NAME}?" fi } read -p "Your name: " YOUR_NAME is_your_name_defined
Unlike what you may be used to in other languages, the variables you define inside your function are global, and visible to the code outside your script. To define a variable scoped locally only to your function (meaning inaccessible to all code outside it), use the local
keyword: e.g. local i=0
.
If you’d like to return values from a function, you need to use global variables as well. In Bash, functions can only return exit codes, using the return
keyword. Let’s look at an example:
is_your_name_defined () { if [ -z "$YOUR_NAME" ] then MESSAGE="It doesn't seem like I have your name." else MESSAGE="Is this your name: ${YOUR_NAME}?" fi } read -p "Your name: " YOUR_NAME is_your_name_defined echo $MESSAGE
Bash scripting allows you to do quite a bit on a UNIX-based operating system. This article touched on some of the basics of Bash scripting, including creating and running scripts, working with strings, and using loops in your code. Hopefully, it will serve as a good start to your journey in writing powerful Bash scripts that meet your needs.
There’s a lot more to learn about Bash though, including some of the most useful commands, filesystem navigation, and more. Let us know in the comments what topics we should cover next.
Related Article: