Table of Contents
- Chapter 1: Hello World!
- Chapter 2: Variables
- Chapter 3: Prompting for input
- Chapter 4: Heredoc
- Chapter 5: Comparing Values
- Chapter 6: Working with numbers
- Chapter 7: Functions
- Chapter 8: Loops
- Chapter 9: Conditional Logic
- Chapter 10: Expansions and Substitutions
- Chapter 11: Formatting output with printf
- Chapter 12: Arrays
- Chapter 13: Case Statement
- Chapter 14: Extracting Substrings
- Chapter 15: Debugging Scripts
Chapter 1: Hello World!
—
## Hello World: Our First Script
Above script is stored in file welcome.sh
The shebang line /usr/bin/env bash tells which interpreter to use (in this case, bash) and also informs commands like file
that welcome.sh is a Bash script.
Linux command file welcome.sh
will show that it’s a bash script and an ASCII text executable. If we do not include the shebang line, the file command will not recognize that the file is a shell script.
It’s best practice to write the shebang line in a way that uses the shells environment to locate the bash executable. Not all systems have bash installed at the same place (/bin/bash
for example). Using /usr/bin/env bash
, tells whatever bash executable appears in current user’s $PATH
variable.
Executing the script
—
Now that we have our first script, lets see how to execute it:
- Execute with
bash welcome.sh
. No need to set the execute permission while executing this way. - To execute the script directly (i.e.
./welcome.sh
), we need to set the execute permission,chmod a+x welcome.sh
umask
—
When a new file or directory is created, the permissions of that new object are determined by the default permissions for the file or directory. umask controls what permissions are not given to newly created file or directory. It does not affect the permissions of existing objects, only newly created objects.
- The default Unix permission set for newly created directories is
777
- The default permissions for newly created files is
666
Without the umask
, all new directories would be created with full 777
permissions, and all new files would be created with full 666
permissions. The umask
blocks certain permissions from being given to newly created file system objects.
Every bit set in the umask “masks”, or “takes away”, that permission from the default. “Mask” does not mean “subtract”, in the arithmetic sense.
Below is the octal representation of permissions:
-
4
for Read -
2
for Write -
1
for Execute
Shell command umask 022
sets to —-w–w- permissions to be removed from the default permissions. Therefore, setting umask to this value will result in a file having 644 (rw-r—-r—) permission and directory having 755 (rwxr-xr-x) permission.
Using the chmod
command without specifying whether you want to change User, Group, or Other permissions causes chmod to use your umask to decide what sets of permissions to change. If you want everyone (user, group, others) to have execute permissions, use chmod a+x file_name.sh
.
Adding file to $PATH
—
In the $PATH
environment variable, include the location where your script is and you can execute it by just specifying the name (welcome.sh
). Else, use ./welcome.sh
Positional Parameters
—
-
$1, $2, $3....${10}
: Upto 9, no need to use braces (we can if we want to). From 10 onwards, use braces to indicate it’s number ten and not 1 followed by 0. -
$0
is NOT positional and represents the script name. -
$#
represents the number of positional parameters that have been supplied. -
$*
will list all parameters as a list, we can iterate through the same.
Chapter 2: Variables
—
Quotations
—
- Single Quotes: Bash will treat as literal text, it will not attempt any substitutions, replacements etc.
- Double Quotes: When you wrap up text in double quotes, bash will still interpret substitutions, expansions, evaluations,variables, and so on, but it will be reasonable about not trying to do that when it doesn’t it make sense to.
Setting Variables
—
Syntax to set variables: var_name=value
- Make sure there is NO space around the assignment operator.
- Use lowercase for variable names, to distinguish them from environment variables.
- If the value has spaces in it, enclose it in double quotes.
- Variable names are case sensitive.
- Variable name in bash must begin with a letter or an underscore, and can be followed by any letter or number, or more underscores.
- Variables can be changed, re-assigned during the course of the script.
Specifying Default Values
—
If users do not provide the required inputs, we can provide default values to variables:
Reading Variables
—
Syntax to read variables: "$var_name"
Best practice is to quote the variables - this is to protect elements like spaces within our variables.
If you expand a variable that doesn’t exist, Bash won’t issue any warnings and just replace the variable with an empty string. If you want Bash to throw an error whenever an unset variable is expanded, use
set -u
To Brace or Not to Brace?
—
If we need to push other characters against our variable name, use braces "${var_name}"
If you need to distinguish the variable name from characters around it, you can use curly braces, { }
- they are optional, but serve to protect the variable to be expanded from characters immediately following it which could be interpreted as part of the name.
In the below example, since the variable name was not delimited, the _
character was considered part of the variable name - the shell tried to expand the non-existent $a_
, which does not exist and therefore nothing was returned. Wrapping the variable with curly braces solves this problem:
We will use braces for most parameter expansions, because if we left them off, the shell would just interpret the part after the parameter name, as characters and show them instead of using them to do what we intend.
Built-in Variables
—
-
command -V echo
will show thatecho
is a built-in, which will take precedence over other commands. -
enable -n echo
will run command version (/bin/echo
) of echo instead. I can re-enable it withenable echo
-
enable -n
will show the builtins that are disabled -
echo string
will output the string with a newline. This is the builtin version. I can mentionbuiltin echo string
to specify that I want to run builtin version, orcommand echo string
for command version. - Builtins use a different documentation that regular man pages. There’s a builtin called
help
that shows supporting information about builtins. Example,help echo
-
help
will show a list of all builtins.
Environment Variables
—
Variable assignments as done above are only visible in the current shell - their values don’t affect a program called by a script (i.e. a forked process)
If you want a way for variables to be visible and usable by any of the programs you call on the script or command line, you need to use environment variables using the export
builtin command.
- The
-x
in the output tells that the variable has been exported to the environment. - You can use
declare -x
to get a list of all environment variables and their values. - We can also use
env
and will need to sort its output to read it.
You should only export variables when you actually need to use their values in subprocesses - you risk overwriting an important environment variable used by a program without intending to, which can be hard to debug.
declare
Command
—
declare
is a shell built-in, and is used to declare shell variables and functions, set their attributes and display their values.
The --
in above example tells that there’s no special attribute associated with the variable
-
declare -r myvar=value
will make the variable read only and cannot be changed later on.
Below options allow us to turn the characters in the value to lower or uppercase. Even when the value of the variable is changed later on, it will be transformed to lower or uppercase. These help us normalize user inputs for consistence.
-
declare -l myvar=value
will turn the characters in the value to lowercase. -
declare -u myvar=value
will turn the characters in the value to uppercase.
declare -p
will show all variables set in the current session.
Chapter 3: Prompting for input
—
read
can be used to prompt for user input:
If we do not supply the variable name to the read command, the value will be stored in $REPLY
Another way to read is using heredoc notation, which we will see later in the book:
If we use the -n
option followed by an integer, we can specify the number of characters to accept before continuing. For example, if you want to read only 1 character, use read -n1
- there’s no need for the user to hit enter after providing the input.
We can also accept user input in an array with the read -a
flag. Note that indexing is 0-based:
Chapter 4: Heredoc
—
When writing shell scripts you may be in a situation where you need to pass a multiline block of text or code to an interactive command. A Here document (Heredoc) is a type of re-direction that allows you to pass multiple lines of input to a command. It uses a token word or delimiter to specify where a document for a command’s standard input should finish. Below is the syntax:
Below is an example of a heredoc:
- Any string can be used as a delimiter, EOF and END are most common.
- If delimiter is unquoted, shell will substitute variables, commands and special characters before passing the here-document lines to the command.If you want to expand variables or do command substitution inside a here-document, you can leave out the single quotes around the delimiter.
- If you really want to indent your here-documents, you can include a hyphen between
<<
and the delimiter, i.e.<<-[delimiter]
the tabs (but not spaces) at the front of your input lines will be ignored. Use this when your heredoc is inside an if statement for example.
Since bash does not support multiline comments, we can use heredoc to overcome this. If you are not redirecting heredoc to any command, the interpreter will simply read the block of code and will not execute anything:
Heredoc output can be sent to a file or be piped to another command.
Chapter 5: Comparing Values
—
Bash has a built-in command called test
, which is also represented by single brackets, [ ]
.
For example, [ -d ~ ]
will test if my home directory is a directory. It will return an exit status of 0 (success)
or non-zero number (fail)
. We can use the value of $?
to read the value of return status.
Comparing Numerical Values
—
To test numbers, use eq
, -ne
, -lt
, -le
, -gt
, -ge
.
Comparing Strings
—
Use ==
and !=
to compare strings. We must use a single space before and after these operators.
Extended Test
—
Extended test uses double brackets, [[ ]]
, and allows us to use more than one expression within a test, for creating a little bit more complex logic.
For example, below, we are checking if my home directory is a directory and whether the bash binary exists:
Similarly, we can use ||
for OR operation, which executes the next command only if the previous command fails.
Extended test also allows us to use regular expression. Below we are testing if “cat” starts with c: [[ "cat" =~ c.* ]]; echo $?
It is recommended to use extended tests as a best practice.
Chapter 6: Working with numbers
—
- Bash can do integer math, not decimal or fractional.
- Supports 6 operators,
+ - * / % **
-
$((...))
- Is an Arithmetic Expansion, and returns the result of the mathematical operation. This does NOT modify the existing variable. -
((...))
- Is an Arithmetic Evaluation and performs calculations and changes the values of existing variables. They do not provide any return value, but an exit status instead. - When a variable is referenced inside the double parentheses, whether for arithmetic expansion or arithmetic evaluation, it does not need to be prefaced with a dollar sign.
We can use declare -i b=3
to tell Bash that the variable is an integer and not treat it as a string. It’s a good idea to use declare when we know the variables we use will be integers.
echo $RANDOM
will return a pseudo-random value between 0 and 32,767. Use $(( 1 + $RANDOM%20))
to get a random number between 1 and 20.
Chapter 7: Functions
—
Declaring Functions
—
Function Parameters
—
We may send data to the function in a similar way to passing command line arguments to a script. We supply the arguments directly after the function name. Within the function they are accessible as $1
, $2
.. etc
Return Values
—
Unlike programming languages, Bash does not allow functions to return values - it does however allow us to set a return status. Similar to how a program or command exits with an exit status which indicates whether it succeeded or not. We use the keyword return
to indicate the return status:
- Typically a return status of 0 indicates that everything went successfully.
- A non zero value indicates an error occurred.
Chapter 8: Loops
—
for Loop
—
There are 2 variations of for loop in Bash:
Below is a type of for loop which is characterized by counting. The range is specified by a beginning and end:
A second variation involves a three-parameter loop control expression; consisting of an initializer (EXP1), a loop-test or condition (EXP2), and a counting expression/step (EXP3).
while Loop
—
Chapter 9: Conditional Logic
—
if - else Statement
—
Below is the syntax:
- There must be a space between the opening and closing brackets and the condition you write. Otherwise, the shell will complain of error.
- There must be space before and after the conditional operator (
=, ==, <=
etc). Otherwise, you’ll see an error like"unary operator expected"
.
Useful Unary Operators
—
Below are useful unary operators we can use with if statements:
Unary Operator | Description |
---|---|
-e file_name |
True if file exists |
-d dir_name |
True if specified directory exists |
-h file_name |
True if the file exists & is a symlink |
-s file_name |
True if file exists and size > 0 |
-r file_name |
True if file exists and is readable |
-w file_name |
True if file exists and is writable |
-x file_name |
True if file exists and is executable |
-v var_name |
True if variable is set (even if it has an empty value |
-z string_var |
True if length of string is zero |
-n string_var |
True if length of string is non-zero |
Avoiding if-else Statements
—
Operator | Behavior | ||
---|---|---|---|
; | Allows you to chain commands together. It will run all commands regardless of whether they succeeded or failed. | ||
&& | Similar to semicolon, but next command runs ONLY IF previous command succeeded | ||
Next command is run ONLY IF previous command did NOT succeed | |||
& | All the above (;, &&, | ) will wait for execution of first command, then run 2nd command and so on. However, & will start the execution of first command and then it will start the execution of 2nd command, regardless of whether execution of first command was completed or not. | |
-z “$var_name” | Checks if the variable is set to nothing. Equivalent code would be “$my_var” = “” | ||
[ -z “$my_var” ] | This is an implicit if statement - you don’t need if, then, else, fi etc. |
Chapter 10: Expansions and Substitutions
—
Tilde Expansion
—
-
~
represents$HOME
environment variable -
~-
represents$OLDPWD
, and will show the previous directory you were in (if you had recently changed directories).
Brace Expansion
—
It is used to generate strings at the command line or in a shell script. For example, touch file{0..12}
will create file1, file2,.., file12.
Parameter Expansion
—
We’ve seen this while reading variables - parameter expansion lets us recall stored values:
Pattern Substitution
—
It is a special case of parameter expansion.
Command Substitution
—
Allows us to use the output of a command within another command. For example, echo "$(uname -r)"
.
Chapter 11: Formatting output with printf
—
printf
is a bash built-in and does not add a newline by default.
We can enclose variables within the printf statement:
When you enclose your arguments with single quotes the variable and command will be treated as plain text. You have to enclose the arguments with double quotes if you want the variable and command to be expanded.
We can assign printf output to a variable in 2 ways:
The -v
option causes the output to be assigned to the variable var rather than being printed to the standard output.
Width Modifiers
—
Since Ronaldo is 7 characters, and the specified width is 20, it will add spaces to justify the width.
We can use a -
to justify the alignment:
For integers, we can replace the space with zeros by adding a 0
flag modifier:
Pricision Modifiers
—
It is used to decide the number of characters to be printed, use the .
followed by number of characters:
We can also use *
to specify the precision:
Chapter 12: Arrays
—
Bash supports 2 kinds of arrays:
- Indexed Arrays: Set or Read values by referring to their position (using an index). Indexing is 0-based.
- Associative Arrays (Bash v4 & above): These are key:value pairs
- We cannot create nested arrays in Bash.
Defining Arrays
—
Retrieving Values
—
Setting Values
—
Appending to an array
—
Length of an array
—
${#array_variable[@]}
will retrieve the length
Looping through an array
—
Associative Arrays
—
Chapter 13: Case Statement
—
Case statement is generally used to simplify complex conditionals when you have multiple different choices, and is preferred over nested if-else statements.
Syntax
—
- The
)
operator terminates a pattern list. - Each clause must be terminated with
;;
- It is a common practice to use the wildcard (*) as a final pattern to define the default case. This pattern will always match.
- If no pattern is matched, the return status is zero. Otherwise, the return status is the exit status of the executed commands.
Example
—
Chapter 14: Extracting Substrings
—
Using Bash Substring Expansion
—
- Syntax:
${str_var:offset:length}
- Start extracting from the offset, up to the specified length.
- The
offset
is used to specify the position from where to start the extraction of a string. Thelength
is used to specify the range of characters to be extracted, excluding theoffset
- Note: Indexing is 0-based
Using IFS
—
IFS (Internal Field Separator), is an environment variable that determines how Bash recognizes word boundaries while splitting a sequence of character strings. Default values are space, tab and newline.
In the below example, we have set the IFS to an underscore, which is what will be used to split the numbers variable with underscore as the delimiter. Once split, we can access the words using $1, $2 and so on.
Using cut
—
-
-d
option allows us to specify the delimiter -
-f
sets the field to be extracted
Chapter 15: Debugging Scripts
—
- If we want to look at the verbose output from our script and the detailed information about the way the script is evaluated line by line, we can use the
-v
option,bash -v script_name.sh
- The
-x
option, which displays the commands as they are executed, is more commonly used. Traces of each command plus its arguments are printed to standard output after the commands have been expanded but before they are executed.