Learn Bash and linux command line with no prior knowledge
Introduction
Hey there!
My name is Drew and I write long articles no one ever reads about languages and technologies I'm learning. These are more notes for myself to myself to refer back to later in life but if anyone actually finds any value from it, I'd love to hear about it in the comments section at the bottom of this article.
This article is about learning how to write code in Bash, a Shell Language which is very easy to learn and would be an excellent starting place on your coding journey. You will use bash throughout your career and getting a handle on it early will truly accelerate your career outlook.
After reading it, you won't be able to develop an application without imagining some use case for how bash could make the deployment or running combined terminal commands easier.
If you haven't already, I highly recommend my article https://codingwithdrew.com/how-to-customize-your-zsh-terminal-with-bash/ on how to customize your bash terminal prompt, which I hope you love. You don't need to know any bash or do anything complicated in order to work through it.
When you are all done customizing your bash prompt in the article above, head back over here for an in depth understanding of bash in an easy to follow format. I have organized the various sections in the most logical way for myself and to build off previous knowledge built in this article. Every section is titled as a question in case you ever need to refer back to it and for better search results.
Warm Regards, Drew Karriker
Table of Contents
- Introduction
- Getting Started with Bash
- Variables and Expansions
- How does bash process command lines?
- Bash Inputs
- Logic
- Loops
- Glossary
Why is it called Bash?
BASH is a play on words and an acronym for Bourne Again SHell, which is based on a shell called "Bourne" from 1979. Bash is the most commonly used Linux shell today but that may change with Mac's setting their default shell to ZSH. I'm personally not a big fan of zsh, but it may just be that I'm not that accustomed to it.
What is a "Shell"?
A shell is "A program that interprets the commands you type into your terminal and passes them to the operating system".
The purpose of the shell is to make it more convenient for you to issue commands to your computer.
For this article, you will want to have bash shell set as your default. I may refer to bash/shell interchangeably at this point.
You can do this with the following command:
chsh -s /bin/bash
You can then verify you are running in the correct shell by running the following command:
echo ${SHELL}
๐ก The curly brackets are not required for this type of variable but for now we will use
${variable_name}
notation to keep things simple.
You should see /bin/bash
output and may need to provide admin password to get it to run.
So, why bash? Bash is feature rich, fast, and commonly used making it a great go to for all your scripting needs.
What is a shell script?
A shell script is a file containing commands for the shell, or "script" for short.
A Bash script is a file that contains commands that are run one after the other for the Bash shell.
Why use scripting?
Scripts are convenient ways to store sets of commands to automate tasks, save time, and increase reliability.
Manually operating using copy paste or as I like to call it "click ops" is a recipe for mistakes especially when there are a lot of steps.
Any time a human has to type something in or do that thing at a specified time, it's bound to have errors at some point. After talking with some old timers, they used to use MS word documents and would copy paste their "script" of commands one at a time. MS office formats text and changes ASCII characters in some cases causing problems and they eventually switched this to using plain text documents. This is from a time before devops was popularized. My suggestion: Automate everything where ever possible and store your automation as code in a repository.
Getting Started with Bash
What are the core components of a bash script?
There are three core components of a bash script
- A "shebang" line or
#!
- there can be no line before this line. It tells the shell what interpreter to use to read the file. It will look like this:#!/bin/bash
- The commands written in bash - each line of the script is executed in order, one line at a time.
echo "Hello World!"
๐ก The echo command will output whatever follows it into the terminal output.
- Exit commands - these close the script and will allow you to capture errors. This is optional.
exit 0
If we put it all together, and saved it in our home directory and named it "~/my_first_script_file_name", it would look like this:
#!/bin/bash
echo "Hello World!"
exit 0
How do you run a shell script on your computer?
In order for our script to run, we have to give it permissions. Typically any new file that is created on your computer will not have executable permissions. We can do this simply by using the chmod
command.
chmod +x my_first_script_file_name
๐ก
chmod
is the command and system call used to change the access permissions of file system objects sometimes known as modes. The+x
adds "execution" permissions to the file. We will go into further detail on this later.
To run that script, all we have to do is run it using the ./
notation (which means "in this directory"). It'll look something like this:
./my_first_script_file_name
How do you create clarity in your scripts?
Let's look at an example:
#!/bin/bash
echo "Hello World!" # This is a comment.
exit 0
Comments start with a # (hashtag) and this tells the shell to ignore everything after it on that line. Good comments that explain what a section does can be very helpful for your future self or co-workers.
There are 5 other pieces you can should add:
# Author: Drew
- Leave details about who created the script for easier tracking in the future.# Date Created: 07/21/2020
# Last Modified: 07/02/2021
- When you edit the file, don't forget to update this line.# Description
- use this piece to add details about what the script does.# Prints "Hello World" to the terminal.
# Usage
# my_first_script_file_name
- this is much like a "man page" that will explain how to use the script.
These pieces above will allow your script to look much more professional. Let's put it all together.
#!/bin/bash
# Author: Drew
# Date Created: 07/21/2020
# Last Modified: 07/02/2021
# Description
# Prints "Hello World" to the terminal
# Usage
# my_first_script_file_name
echo "Hello World!" # This is a comment.
exit 0
What are the basics of file permissions on Linux?
To view permissions in terminal we can use ls -l
(the -l
flag lists the files and their details). You don't have to understand what is here just yet, but you'll want to refer back to it as you read. It'll look something like this:
PERMISSIONS | NO. Of Files | USER | GROUP | SIZE | DATE | TIME | NAME |
---|---|---|---|---|---|---|---|
drwx------+ | 25 | drew | admgroup | 800 | Jun 18 | 13:03 | Desktop |
drwxr-xr-x | 17 | drew | admgroup | 544 | Jun 21 | 10:15 | Dev |
drwx------+ | 5 | drew | admgroup | 160 | May 18 | 11:55 | Documents |
drwx------+ | 64 | drew | admgroup | 2048 | Jun 21 | 09:31 | Downloads |
drwx------@ | 72 | drew | admgroup | 2336 | Jun 15 | 12:26 | Library |
drwx------+ | 4 | drew | admgroup | 128 | Apr 21 | 09:51 | Movies |
drwx------+ | 3 | drew | admgroup | 96 | Apr 20 | 16:47 | Music |
drwx------+ | 5 | drew | admgroup | 160 | Apr 22 | 19:59 | Pictures |
drwxr-xr-x+ | 4 | drew | admgroup | 128 | Apr 20 | 16:47 | Public |
-rw-r--r-- | 1 | drew | admgroup | 0 | May 26 | 11:11 | my_first_script_file_name |
The permissions are written in 10 digit codes, you can tell which items listed are directories because they start with "d".
Our script is obviously not a directory so that first character is -
. The rest of the 9 digits are really 3 groups of 3 characters. The 3 groups (written in order) are "Owner", "Group", "Everyone" They can have either -
, r
, w
, and or x
.
-
No permissions (can be in any of the 3 character spaces)r
Read permissionsw
Write permissionsx
Executable permissions - Shell scripts will always need this.
How do I set file permissions on my script, appropriately?
If we chmod our file, we will see it change to: -rwxr-xr-x
This will provide everyone permission to execute it but from a security perspective, we really only want the file owner to be able to and everyone else read only access. We can do this simply with chmod 744
. If you aren't familiar with what 744 means, let's take a look.
744 is what is called an octal number and chmod uses these as a way of expressing which are set.
Read = 4 Write = 2 Execute = 1
If you add these you will get 7 meaning all permissions. If you had read only you'd have 4. Remember how the 10 digit code was broken down into 10 digits where the first of which doesn't count and the preceding 9 digits were 3 groups of 3 characters long code?
In octal format, these 3 numbers (744) represent the same information. In that 744, a 7 for all permissions is on the first group (owner), 4 or "read only permissions" for the "Group", and the last number 4 represents "everyone else" has read only permissions.
To see a really good representation of this in action and have a hands on to play with it - you can use http://permissions-calculator.org/.
Bear in mind, if you grant execute permissions, you have to also include read permissions, otherwise you won't be able to execute them - the interpreter will not be able to read it to execute it. What would your permissions be if they were read and execute only but not able to modify (write) to the file?
Try to edit your file to have permissions for only your user to read, write and execute and no other permissions for anyone else but to execute it. Let know how you did it in the comments section below!
In the next section, we will learn how we run our scripts anywhere in our system without being in the same directory as the file.
What is the PATH variable?
If you tried to run ./my_first_script_file_name from any directory other than the one it lives in, you'd get an error command not found
. This is because it doesn't know where to look. When we run a command initially, linux will initially search for it in the active working directory, if it doesn't find it there it will look for it in the PATH.
So what is the PATH?
The path allows a command to be ran from any directory on your computer.
We can see the path by running echo "${PATH}"
The output is a bit scary to look at but basically what you'd see is a list of files separated by :
(colons). When we enter a command to run, the system will look through that list of files for an executable file by that name until it finds one.
Let's create a directory and add it to our bash path.
How do I add my script to PATH?
In your terminal run mkdir -p ~/bash_drewlearns/scripts
and then change your working directory to the root folder we just created (cd ~/bash_drewlearns
).
Move our script from ~
(home path) into our new folder. mv ~/my_first_script_file_name ~/bash_drewlearns/scripts/my_first_script_file_name
From the home directory, you will want to edit your .bash_profile or .profile. There may not be a bash_profile or .profile file in the directory, this is fine. If it doesn't exist you'll want to create one. If you are using a computer with multiple shells like zsh and bash, you'll want to use .profile. If you are only going to use bash, then you can use .bash_profile.
Edit the file using nano ~/.profile
or nano ~/.bash_profile
as applicable.
Paste the following in:
export PATH="$PATH:$HOME/bash_drewlearns/scripts"
Save and exit the file with command x
followed by y
then return
We can reboot the terminal or we can source it. Source can either be written our as source
or shorthand .
(dot space) followed by the file name.
We want to source our . ~/.profile
or . ~/.bash_profile
(respectively).
If we look back at echo ${PATH}
in our terminal now, we will see /Users/YOUR_USER_NAME/bash_drewlearns/scripts
added to the end of the list of paths.
Now if we run my_first_script_file_name
without the ./
in any directory the terminal will output `Hello World!". Pretty neat, huh?
Any time we create a new script and place it in that directory /Users/YOUR_USER_NAME/bash_drewlearns/scripts
with the proper permissions, it will be accessible in any directory path we are working in.
Variables and Expansions
Variables allow us to store useful data under convenient names, much like every other coding language. If this is your first foray into development, this is a fundamental tool you will use over and over. Also, shell expansions are very powerful features that allow us to retrieve data, process it's output and perform tasks, these two tasks (creation and expansion) will be tantamount in your development career. Every language has their own way to do this but bash is probably among the easiest.
How do you create a variable and how do I use parameter expansion to get data out of variables?
A parameter is anything that stores values, there are three kinds but for the moment we will focus on variables
- Variables (most common type) - a parameter is any value that can be manually changed. There are 2 kinds, user-defined variables and shell variables.
- Positional Parameters
- Special Parameters
Imagine a variable is just a piece of data that can be stored in memory under a name you create and then you can pull that piece of data out of memory any time you want by referencing that name you created. It's pretty much that simple. Let's look at an example!
๐ก It's important to know that when you create a variable, that variable's name and the value have no spaces around the
=
character.
#!/bin/bash
website="https://Drewlearns.com"
echo Go to ${website} to learn more!
exit 0
To make it easier to understand - create a variable with name=value
and expand it's value using ${parameter}
. That later part is called parameter expansion
We just created a user-defined variable called "website" that when called by echo results in "Go to https://DrewLearns.dev to learn more!". Note the weird ${website} syntax, this is used to expand our variable to it's value. This is called interpolation in most languages.
Let's look at some examples:
name=DrewLearns
echo ${name}
# outputs DrewLearns
# How to Change the output for the first letter to a lowercase:
echo ${name,}
#drewLearns
# How to Change the output for all letters to a lowercase:
echo ${name,,}
#drewlearns
# How to Change the output for the first letter to Uppercase:
lowercase=abcdefghijklmnopqrstuvwxyz
echo ${lowercase^}
#Abcdefghijklmnopqrstuvwxyz
# How to Change the output for all letters to Uppercase:
lowercase=abcdefghijklmnopqrstuvwxyz
echo ${lowercase^^}
#ABCDEFGHIJKLMNOPQRSTUVWQYZ
# How to "SLICE" a variable
number=0123456789
# ${parameter:offset:length}
echo ${number:1:3}
# 123
# How to start at the end and slice a variable
# ${parameter: -offset:length} # Mind the space!
echo ${number: -1:3}
# 9 # Because there are no numbers after 9
# Another "How to start at the end and slice a variable" example
# ${parameter: -offset:length}
echo ${number: -5:5}
# 56789
Does bash make variables easier?
There is another kind of variable than the one we have learned so far (user-defined) called "Shell Variables". Let's learn about what they do, how to use them, and lastly learn some commonly used shell variables.
In total, there are about 105 shell variables you could use. We aren't going to get into all of them. If you'd like a comprehensive list, you can check out Chapter 5 of the bash manual for a rivetting read: https://www.gnu.org/software/bash/manual/bash.html#Shell-Variables. The goal is to get familiar with some of them and then know how to reference them if you need to in the future.
What are some common shell variables that I should know?
Remember at the beginning of this article when we mentioned how to set up your path. We used a Shell variable called echo ${PATH}
which returned all the directories that have executables for command names. This shell variable will be very good to know in the future.
Shell variables are prebuilt and are differentiated from user-defined variables by all capital letters in the name.
๐ก For simple variable expansion, we do not need to use the curly brackets as I mentioned before - we could use just
$PATH
instead of${PATH}
, but the brackets notation is more officially endorsed even though the shorthand does work. Parameter expansion can get a lot trickier so for now, let's just understand there are two notation types that can be used. Mustaches are required for parameter expansions ๐ฅธ
HOME shell variable is especially useful to determine the "Home" directory path in your shell. Similar to ~
this is considered an absolute path. Here is an example
[17:40:41] ~/Dev/bash ๐ฆ echo $HOME
/Users/drewlearns
Let's take a look at the USER shell variable. USER will contain the current username that is calling the Shell variable. Let's take a look:
[09:47:13] ~/Dev/bash ๐ฆ echo $USER
drewlearns
The most fun bash variable is the PS1
Variable which contains the prompt shown in the terminal before each command. If you ran through my article from the introduction, you are probably familiar with what it does, if you haven't already, give it a look here: https://codingwithdrew.com/how-to-customize-your-zsh-terminal-with-bash/.
How does escaping work? (Also known as quoting)
There are three types of quoting in bash but its basically about removing special meanings. As you probably are aware - bash has key words and commands that if they aren't "quoted" the shell will search for the command. This allows the words/characters that are quoted to be interpreted literally.
- Backslash (\) - will remove any special meaning of the character proceeding it.
- Single Quotes('') - will remove any special meaning of the characters inside them.
- Double Quotes (" ") - will remove all special meaning of characters except dollar signs ($) and backticks (`).
What is command substitution in bash?
This feature is super useful to save the output of a command into variables and how to use a outputs of one command inside another.
The way we do this is similar to how we create user-defined variables but instead of mustaches (${}
) you would use $(command)
. This will run the command and the output of the command will be what will replace the "command".
How do we perform math?
If you want to do math in bash, you change the command substitution syntax just a little to $((expression))
.
You can use the following expressions:
expression | description |
---|---|
+ | addition |
- | subtraction |
/ | division |
* | multiplication |
** | to the power of |
% | remainder - typically used for true false related to evens/odd determination |
๐ก It is important to note that you cannot use decimal numbers in bash and any outputs that result in decimal numbers will be truncated to the whole number. This is where the
bc
command will come into play but I'm not going to get too into it.bc
command is a "basic calculator" that will allow piping into it your expression like this:"scale=2; 9/2 | bc
which will result in 4.50. The scale will determine how many decimal places it will use. It's also good to know that exponents in bc use^
instead of**
.
What is brace expansion in bash?
This is the bash scripting super power, it will allow you generate text that follows a pattern such as file names.
String lists allow you to list out your names of files but you can also use a range list.
The values on a string list do not need any specific order.
๐ก Note the lack of spaces!
String lists can look like this:
echo {1,b,3,d,-5,anyword}
# 1 b 3 d -5 anyword
echo {1, b, 3, d, -5, anyword, 2}
# { 1, b, 3, d, -5, anyword, 2 } # OPE - no expansion
Range lists are easier to write:
echo {1..10}
# 1 2 3 4 5 6 7 8 9 10
echo {10..1} # reversed
# 10 9 8 7 6 5 4 3 2 1
echo {a..z}
# a b c d e f g h i j k l m n o p q r s t u v w x y z
echo {{z..a}} #reverse
# z y x w v u t s r q p o n m l k j i h g f e d c b a
echo {1..10..3} #
What is Word Splitting in bash?
Word Splitting the process bash shell (and other shells) do to split the result of some unescaped expansions into separate words. This effect can have some very significant effects on how command lines are interpreted so it's important to know them.
Word Splitting is only performed on the results of unescaped (not quoted) parameter expansions, command substitutions, or mathematical expansions.
Tab, space, and new line (\n
) is a field separator. These will separate words into their own commands.
What is globbing in bash?
File name expansion is a bash super power, it is used as a shortcut for listing the files that a command should operate on.
The name originates from the glob program in early versions of Bell Lab's Unix Operating system from 1969 to 1975.
Globbing is only performed on Words and not operators. We will get into "words" vs "operators" the next section but for now just know that a word is a grouping of characters and an operator determines how to terminate or output a command.
If you are already familiar with the use of regex (regular expression) this will not really be a new idea for you, but globbing patterns are words that contain an unescaped "special pattern" character such as *
, ?
, or []
.
If the shell finds one of these characters, globbing will be performed and Bash will then try to match on the proceeding pattern.
*
is the most common/flexible pattern. It means "everything". Think of using something like ls *.txt
to search for all txt files. It will even match on "nothing".
?
the question mark will match any single character but will not match on "nothing" and any extra characters.
[]
this will only match the characters placed inside of its brackets.
๐ก These are case sensitive.
How does bash process command lines?
Ok, so far we know a bunch of miscellaneous pieces to bash but don't know how they all work together. Let's break down how a bash shell operates.
Bash uses six steps to go through and interpret a command line.
Step 1 - Tokenization
A token is a set of characters that is considered a single unit by the shell.
When Bash has broken up a line into tokens, much like punctuation in a sentence, it then classifies those tokens as either words or operators.
๐ก A word in bash is a token that does not contain an unescaped meta-character.
๐ก Operators are a token that contains at least one unescaped meta-character
Step 2 -Command Identification
Each command is terminated by a control operator. These tell the shell what to do after the individual words are interpreted.
There are 2 types of commands, simple and compound commands (commands that include logic).
We have been looking at simple commands so far which are terminated by one of the control operators listed below.
Control Operators |
---|
newline |
| |
|| |
& |
&& |
; |
;; |
;& |
;;& |
|& |
( |
) |
space |
We will introduce compound commands in a later section when discussing "logic". The key difference is each compound command will start and end with a corresponding reserved word like if/fi, while/done, or do/done and they also don't have to live on one line.
Basically, the Shell knows where to start and end by looking for those special "Meta-Characters" listed above.
Step 3 - Expansions
We have covered this already to some degree already but we will talk about the 4 steps the shell goes through when performing shell expansions.
- Step 1
- Brace Expansion
- Step 2
- Parameter Expansion
- Mathematical Expansion
- Command Substitution
- Tilde Expansion
- Step 3
- Word Splitting
- Step 4
- Globbing (file name expansion)
๐ก Expansions used in later stages cannot be used in expansions of earlier steps. Example: You cannot declare a variable and then use brace expansion on that variable using a parameter expansion.
x=10
echo {1..$x}
โ error
๐ก Expansions in the same step are given the same priority so they are performed in the order received when reading left to right.
Step 4 - Quote removal (escaping)
We often add quotes to control how the commands are interpreted, so this step will simply remove prescribed escapes/quotes.
During this step, the shell will remove all unquoted escapes that did NOT result from a shell expansion.
The easiest way to imagine this step is that it will only remove the first "layer" of escaped characters. For example: echo "\$HOME"
will only remove the double quotes but not the \
, this is because the \
is nested inside the " "
resulting in \$HOME
Step 5 - Redirection
In this step, the shell will (if present) redirect outputs.
We first need to understand "data streams", there are three to be aware of.
- Stream 0 = Standard Input (stdin). This provides us with an alternative way of providing input to a command aside from command line arguments.
- Stream 1 = Standard Output (stdout). This is the data that is produced after a command is successfully executed.
- Stream 2 = Standard Error (stderr). This is the error output that is produced after a command is unsuccessfully executed.
Redirection Operators | Meaning |
---|---|
< | stdin |
> | stdout |
<& | Connects stdout and stderr to the same place |
<< | Append to stdin |
>> | Append to stdout |
๐ก Redirecting standard error works in a very similar way to
<
and>
, we use2>
. The 2 is here because the data stream we are looking for is different. Another important note is a&> /dev/null
is rather common because it is used to redirect the output to a directory that is immediately deleted. You will come across this.
Step 6 - Execute the command line that is left over
At this step, all of the commands have been interpreted and will be executed upon given the user initiating it has the appropriate permissions.
Bash Inputs
In this section we are going to learn how to use positional arguments to work with command line arguments in your scripts. This is a great way to prompt users for an input and have that input carried over into the command without hard coding anything
What are positional arguments (parameters) in Bash??
command argument1 argument2 ...
In this example above, and I'm sure you are probably familiar with this format with git for example, you pass parameters into the command as arguments. We can take advantage of expansions in these arguments as well.
Let's create a new script in ~/bash_drewlearns/scripts/
and call it "inputs"!
#!/bin/bash
echo "Hello my name is $1"
echo "I'm $2 years old"
echo "I have a pet named $3"
Let's give the file the appropriate permissions (744).
When we run our command like this: inputs Drew 34 Charlie
what do you think the output will be?
Hello my name is Drew
I'm 34 years old
I have a pet named Charlie
What you can see with these arguments I have passed is they are positional. They are called by their positions $1
, $2
, and $3
(there is no limit to how many you can use, by the way.=).
Then we bring together all the things we have learned so far by passing those positional arguments into our script as inputs which are expanded when executed. Pretty neat, right?
๐ก It's uncommon to have a ton of positional arguments in a script and should be avoided but if you do, you'll need to add curly brackets for double digits like
${12}
instead.
What are Special Parameters in Bash?
We are going to learn about parameters that bash gives special meaning to. These are similar to shell variables which we can change, but we cannot change the value of special parameters. The values of special parameters are calculated for us based on our current script.
- The first special parameter is
$#
which expands to the number of positional arguments that are passed into our script. - The second is
$0
, this one is a bit more complicated. In a regular terminal, if you run$0
you will get the output of-bash
which is the shell you are in. If you were to include$0
in a script, it will output the name of our script. $*
- Allows us to access all positional arguments that are passed and will place a comma between every positional argument avoiding word splitting and can be very useful when creating csv files. You'll want to setIFS=,
in your bash script when using this special parameter if you are using CSV format instead of spaces between each. Anything you set IFS to will be used to separate the outputs of the positional arguments."$@"
- similar to $*, this allows us to access all the positional arguments that are passed at the same time which is subject to word splitting. When in doubt, add double quotes to this special parameter to avoid word splitting.
What is the read command?
The read command will allow you to get input from the end user and then save that input as a variable into your script.
When the user provides input, it will be saved in the $REPLY
variable.
Let's look at an example. In your terminal run:
$ read input1 input2
You'll be prompted to input two values separated by a space. If you pressed return and then used echo $input1
, you'd get the first value you typed and the same as echo $input2
. You can name these anything you like by the way!
These are useful of course but require us to know what the script is expecting ahead of time and the expected order. This is not really user friendly or intuitive. There has to be a better way!
The read command has a really nice -p
option you can pass which will allow us to prompt the user for the information we want by declaring the variable name after the prompt. Let's look at an example.
#!/bin/bash
#Prompts
read -p "Input your first name: " name
read -p "Input your pet's name: " pet
read -p 'Input your age in numbers: " age
# REPLY
echo "Hello my name is $name"
echo "I'm $age years old"
echo "I have a pet named $pet"
There is also a -t
option that can be passed that will give the user how ever many seconds you specify to respond. This is useful if there are defaults and you want a script to continue running even if the user fails to provide inputs.
Lastly, there is the -s
option that can be passed which will hide what ever information is provided in the prompt such as keys and passwords. This is still stored as plain text in a variable.
What if I'd like to provide a menu to select from?
If you'd prefer to have an interactive menu instead of manually typing entries, you can use the select command.
We will be introduced to four new features of bash here along side the select
command. We provide an option in
a space separated value list then terminate the list with a ;
. Then we have to tell the script what to do
with the command and when we are done
with the command.
break
is used to stop the select command from looping through over and over again which typically wouldn't be desired behavior and comes before the done
command. It will perform the do
/done
block 1 time before continuing on with the script.
Similar to the -p
option from the read command, you'll want to provide a PS3
value which is similar to the PS1 variable we learned originally. The PS3 variable should be set just above the select
command.
The select command works similarly to the read command, let's look at an example.
#!/bin/bash
# Select format:
# select <variable_name> in <list_of_options>; do
PS3="Select a letter"
select my_variable1 in a b c d e f g; do
echo "The selected number is $my_variable1"
break
done
๐ก
done
terminatesdo
.
Logic
In order for our commands to do more than just perform one command after another in series, we will want to introduce a topic which will be especially useful in all other languages and bash makes this pretty easy though looking at it without a trained eye can be some what intimidating at first.
How do we make our scripts think?
A script with logic will contain if statements and/or loops which will allow it to operate over and over again similar to what we were just introduced to in the select command. Understanding how to leverage these will unlock bash super powers that you can use to perform complex operations.
How do we "chain" commands together?
The first thing you'll want to know is: "What is a list?". When you put one or more commands on a given line, this is a "list" of commands.
List operators behave in different ways as you will see in the table below:
List Operators | What it does |
---|---|
& | Continue this command that came before this character in the background |
&& | When the proceeding command exits with a 0 (successful), perform the next command |
; | When the proceeding command stops, perform the next command (regardless of the exit) |
|| | When the proceeding command exits with a non 0 (failure), perform the next command (great for error handling) |
What are test commands and conditional operators in bash?
A test command allows you to compare two different pieces of information. This is especially useful to determine "if" a condition is met or not. The test command outputs to what is known as a boolean, that is an output of "true" or "false". True will return an exit code of 0 where as a false will return an exit code of 1. "true=0, false=1"
To create a test, all you need is two square brackets, the contents must have a space between them and the brackets.
These can do many different types of comparisons, for example [ 1 -eq 1 ]
tells the shell to test if 2 numbers were equal to each other. In this example, it will resolve to true or exit 0.
In bash we can check the status of the last command using $?
which will show a 0
or a 1
and in this case it will be 0.
Another useful comparison is [ 1 -ne 2 ] ; echo $?
which the -ne
means "not equal". This example will evaluate to true or exit 0.
๐ก The following only work on integers.
There are a few others to know:
Integer Test operators | Meaning |
---|---|
[ <int1> -eq <int2> ; echo $? | equal |
[ <in1> -ne <int2> ; echo $? | not equal |
[ <in1> -gt <int2> ; echo $? | greater than |
[ <in1> -lt <int2> ; echo $? | less than |
[ <in1> -geq <int2> ; echo $? | greater than or equal to |
[ <in1> -leq <int2> ; echo $? | less than or equal to. |
๐ก The following only work on strings and not values of integers. It will treat numbers as plain text strings.
String Test operators | Meaning |
---|---|
[[ $string1 = $string2 ]] ; echo $? | compares two strings and will exit 0 (success) if they are the same. Other languages compare using "==" or "===" |
[[ $string1 != $string2 ]] ; echo $? | compares two strings and will exit 1 (fail) if they are the same. This != is pretty common throughout other languages. |
[[ -z $some_variable ]] ; echo $? | tests if a string is empty, this is commonly used to see if a file exists or not. If it's null or "none" then it will return an exit of 0. |
[[ -n $some_variable ]] ; echo $? | Does the opposite of the -z operator in that it will exit 1 if $some_variable is null or "none". (It's checking to see if the string is not empty) |
File Test operators | Meaning |
---|---|
[[ -e $file_name ]] ; echo $? | This operator will exit 0 if the file specified "exists" or not. |
[[ -f $file_name ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the specified file is a "file" and not a directory. If it's a directory it will exit 1 |
[[ -d $file_name ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the parameter passed is a directory. |
[[ -x $file_name ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the file specified has executable permissions. |
[[ -r $file_name ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the file specified has readable permissions. |
[[ -w $file_name ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the file specified has writable permissions. |
[[ $file_name1 -nt $file_name2 ]] ; echo $? | This operator checks to see if a file exists and will exit 0 if the file specified is newer than. |
How do I write logical if statements in bash?
If statements in bash are a type of compound command, these start with if
and end with fi
and will check the exit status of a test command which we covered in the last section is a 0. If it exits with a non-zero test command, then the if block will not run.
These if statements are structured like this:
#!/bin/bash
if [ 1 -eq 2 ] ; then
echo "1 equals 2"
fi
if [ 2 -eq 2 ] ; then
echo "2 equals 2"
fi
If you were to run the script above, you would get a terminal output of "2 equals 2" since the first if statement exit's with a status of 1 the rest of the if statement is skipped and the second if statement is run.
There is also an elif
statement that can be used if there is another conditional statement.
We could also use else
to add to our logic. Basically saying "if this; then do that [or] else do this". There are no conditionals to add to this statement.
It'll look like this:
#!/bin/bash
if [ 1 -gt 2 ] ; then
echo "1 is greater than or equal to 2"
elif [ 1 -eq 2] ; then
echo "1 is equal to 2"
else
echo "1 is less than 2"
fi
๐ก There is no limit to the number of elif statements but you cannot put an
elif
after anelse
statement.else
always comes last and there can only be one. You can also nest if statements inside of other if statements.
How do we combine conditions in our logic with bash?
Bash like many languages, allows you to use the "and" (&&
) operator to evaluate two or more conditionals, if they are both true, it will exit with 0. If either or both conditions evaluate to false, the whole statement is false and exits with a 1.
You can also use the "or" (||
) operator to combine two conditional statements where if either condition exits with a 0, the whole expression is evaluated to 0.
Let's look at an example where we are trying to evaluate if 3 files contain the same information, if they do, we want to delete 2 of them but if they are different, we don't want to do anything.
Create a new file named "logic.sh" with the following:
#!/bin/bash
a=$(cat file1.txt)
b=$(cat file2.txt)
c=$(cat file3.txt)
if [ $b = $c ] || [ $a = $c ]; then
rm file3.txt
echo "Removed file3.txt"
echo "Only $(ls file*.txt) remains"
elif [ $a = $b ] && [ $a = $c ]; then
rm file3.txt
echo "Removed file3.txt"
echo "Only $(ls file*.txt) remains"
else
echo "Files $(ls file*.txt) aren't the same."
fi
Now give the file the proper executable permissions (744).
Run echo "Same" > file1.txt && echo "Same" > file2.txt && echo "Same" > file3.txt
See how we are building off everything we have learned? We just ran a command to output and echo statement into each of our 3 files, "Same". If we ls
our directory we will see file1.txt, file2.txt, file3.txt. If we run our script now we will see:
Removed file2.txt and file3.txt
Only file1.txt remains.
But what if the files are different?
Run echo "Same" > file2.txt && echo "Different" > file3.txt
now.
If you run your ./logic.sh
command, what will the output be?
Files file1.txt file2.txt file3.txt aren't the same.
What is a case statement with bash and how do I use it?
In the last section we learned how to create branching if logic using "if", "elif", and "else" statements, but another way to create this branching logic is with "cases".
Case statements start with case
and end with esac
(backwards case) - instead of using tons of 'elif' statements, we can do the same thing with less typing but the catch is they can only perform logical checks on a single variable. It's not necessarily a use this or that way, but more of another tool to put in your belt when presented with a problem where a case statement could be used. I've been developing for years now and have only used case statements a handful of times as a way to save time.
Let's look at an example:
#!/bin/bash
read -p "Enter a number (0-99) or letter (a-z): " my_variable
# CASES:
case "$my_variable" in
[0-9]) echo "It's a single digit";;
[0-9][0-9]) echo "It's a double digit";;
[a-z]|[A-Z]) echo "It's a letter";;
*) echo "This is a default option and has to be last"
esac
๐ก The dollar symbol (
$
) and double quotes are absolutely necessary for cases.
๐ก The closing parenthesis is how bash knows a case match is ended.
๐ก Every case line needs to end with a ;;
๐ก The
*)
is a catch all like an "else" statement.
Loops
Every language you'll learn will have some form of loops which allow you to repeat sequences of code while a condition is met. Each language handles them slightly differently but it's a fundamental, nailing down this skill will be exceptionally helpful in your future. Bash is no exception and the way it handles loops is very simple comparatively.
In bash, a "while" loop runs a set of commands "while" a set conditions are exiting with a zero.
When the command gives a non-zero exit code, the loop ends.
Every while loop starts with the reserved keyword while
and ends with done
. The rest of the syntax is very similar to an if statement.
๐จ Infinite loops, a phrase you will get familiar with in all languages, occur when you create a while loop with no exit meaning they will always be true and will run to infinity. While this may be desirable in certain edge cases, it can cause computer crashes and should be avoided. Design your loops so they will eventually produce an exit 1.
Here is an example of a while loop that will prompt the user for a single digit number and then increase the number until it reaches 10. It will add 1 to that number until it causes the variable to be equal to 10. Let's take a look:
#!/bin/bash
read -p "Enter a single digit number: " my_variable1
while [ $my_variable1 -lt 10 ] ; do
echo "$my_variable1"
my_variable1=$(($my_variable1 +1))
done
What is getopts command with bash?
This command allows you to "get" the "options" passed for a given command - this is an especially useful tool when producing scripts that allow you to pass options and change depending on inputs.
We will use getopts as a tool to set our conditions. Every option is separated by a colon and saved as a variable, in this example we save it as opt
. The option that is passed is saved as the OPTARG
variable in bash.
We are going to convert temperatures to degrees c or f respectively based on the option passed.
We are going to pull together everything we have learned already using cases as well. Let's take a look!
Create a file named temperature_conversion.sh.
#!/bin/bash
while getopts "f:c:" opt; do
case "$opt" in
f) result=$(echo "scale=2; ($OPTARG - 32 * ( 5 / 9))" | bc);;
c) result=$(echo "scale=2; ($OPTARG * ( 9/ 5)) +32" | bc);;
\?) echo "That's not a valid option" ;;
esac
done
echo result
Now let's provide the temperature_conversion.sh file with appropriate permissions and test it out.
What is the result when you run: ./temperature_conversion.sh -c 32
and what is the result when we run ./temperature_conversion.sh -f 70
How do we iterate over files with read-while loops?
If we wanted to iterate over the contents of a file line by line, we would want to use "read-while" loops. "read-while" loops are just while loops that use the read command as their test condition.
Let's look at an example file called file.txt that has the following contents:
This is line 1
This is line 2
This is line 3
Let's look at how a read-while loop is structured and would iterate over this example file.txt which we will pass as the first positional parameter to our script.
We will want to pull the output of the file as the standard input for the script. This is performed by redirecting the done < "$1"
at the end of the read-while loop. This will redirect the standard input (stdin) of the while loop instead of the stdin of the read command.
Create a file called read_while.sh
#!/bin/bash
while read my_line_variable; do
echo "$my_line_variable"
done < "$1"
๐จ If you were to create the redirect in the line
while read line < "$1"; do
you would create an infinite loop! Don't do that!
Save it and provide it the proper permissions (744).
Now if we run our command ./read_while.sh file.txt
, the output will be:
This is line 1
This is line 2
This is line 3
Basically we just recreated the cat
command. Of course we can do a lot more than just read out the lines.
Data structures (Arrays) in bash
In bash you can store a lot of data in Arrays as guides for your scripts. Let's take a look at how to use them.
How do I create an Indexed Arrays in bash?
Variables are similar to Arrays but variables can only be used to store one piece of data at a time whereas an array can store a list of data. Arrays are fundamental tools in every language though they may use different names and carry various types, the basics are the same.
An Array is a list of multiple values that are automatically indexed by their position in the array. Each item in the array will have an associated "key" value, or index. This index starts with 0 and counts up, much like every other language I've worked with. In bash, unlike other languages, we do not separate items in the arrays with anything other than a space (no comma separated values).
๐ก Each item in the array is known as an element.
Here is an example of an indexed array:
example=(a b c d e f)
Each element in this index has an associated positional index. a
for example is in the 0 position, b
in the 1 position and so on.
How can I use an indexed array in Bash?
We can call these elements by calling their positional index. You can see the positional index by adding a bang before the array in your script like this: echo "${!example[@]}"
and you'd see an output of 0 1 2 3 4 5
.
In bash though, if you only did a simple parameter expansion on that array like this: echo ${example}
we would just get an output of the first element in the list at index 0 (a
in this case); but, if we wanted to call the letter c
from the example array, we'd only have to call echo "${example[2]}"
and that would output c
. If we wanted to call all of the elements in the Array, we'd just call @
index like this: echo "${example[@]}"
.
We learned before that we can slice a parameter using an :offset :length
after the parameter name. We can run echo "${example[@:1:2]}"
which would result in b c
.
You can remove an element from the array using unset ${example[2]}"
and add/modify elements using the set command and passing it the array and position like this: example[0]=z
.
How do I use the readarray command?
You can create arrays both from the contents of files and the outputs of commands. This makes processing data from that command or file much easier using something called a for loop, which we will learn more about later.
The readarray
takes what ever is in the stdin and outputs to an array. To demonstrate, create a text file called months.txt and input the following:
January
February
March
April
May
June
July
August
September
October
November
December
Let's use this new command to read the contents of a file and output it to an array like this:
#!/bin/bash/
readarray -t my_months_variable < months.txt
# The -t will remove trailing newlines !IMPORTANT
echo ${my_months_variable[@]@Q}
# The @Q will show all "quiet symbols" including newline
Now let's do the same thing but read the contents of a command. Let's create 100 .txt files using brace expansion: mkdir array/ && cd array/ && touch file{001..100}.txt
. Let's store an array that will store the absolute path to each of these files. To do this we'd just need to run ls ~/bash_drewlearns/scripts/array/*
.
But how do we get that to be the input of the readarray command?
Introducing process substitution - This will allow us to represent the standard output of a command as a file. We can then just read this representative file into our readarray command. It will look like this: readarray my_variable < <(command)
.
#!/bin/bash/
readarray -t my_files_variable < <(ls ~/bash_drewlearns/scripts/array/*)
# The -t will remove trailing newlines !IMPORTANT
echo ${my_files_variable[@]}
How do I use for loops to iterate over an array?
"For loops" are a cornerstone of programming that allows us to manipulate data and perform actions on an array. This foundational programming tool is key to nail down early on because it's used in every language I've touched.
It allows you to leverage the structure of arrays with a while loop on every element in an array over and over again and perform any number of commands "for" each element.
Let's learn how to use for-loops!
#!/bin/bash
readarray -t my_files_variable < <(ls ~/bash_drewlearns/scripts/array/*)
for my_variable in "${my_files_variable[@]}"; do
echo $myvariable >> file_list.txt
done
cat file_list.txt
๐ก This example will output the path of each file into a new file called file_list.txt and then outputs the contents of the file into the terminal.
I hope that you have enjoyed learning bash as much as I have. Please drop a comment below or share with a friend if you found this useful! - Drew Karriker
Table of Contents
- What is the best language to learn first and why did you say Bash?
- Learn Bash and linux command line with no prior knowledge
- Introduction
- Table of Contents
- Getting Started with Bash
- Variables and Expansions
- How do you create a variable and how do I use parameter expansion to get data out of variables?
- Does bash make variables easier?
- What are some common shell variables that I should know?
- How does escaping work? (Also known as quoting)
- What is command substitution in bash?
- How do we perform math?
- What is brace expansion in bash?
- What is Word Splitting in bash?
- What is globbing in bash?
- How does bash process command lines?
- Bash Inputs
- Logic
- Loops
- Data structures (Arrays) in bash
- Table of Contents
- Learn Bash and linux command line with no prior knowledge
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.