What is Bash ? or what’s that Black screen that normally appears in Linux as Terminal.
Thats what is Bash and we are going to have a peek of it in this Blog.
Bash is a Unix shell written by Brian Fox for the GNU Project as a free software replacement for the Bourne shell. It was released in 1989 and has been distributed as the Linux and macOS default shell for a long time.
Shells and modes
The user bash shell can work in interactive and non-interactive login shells.
- interactive login shell: You log into a remote computer via, for example
ssh
. Alternatively, you drop to a tty on your local machine (Ctrl+Alt+F1) and log in there. - interactive non-login shell: Open a new terminal.
- non-interactive non-login shell: Run a script. All scripts run in their own sub shell and this shell is not interactive.
- non-interactive login shell: This is extremely rare, and you’re unlikely to encounter it. One way of launching one is
echo command | ssh server
. Whenssh
is launched without a command (sossh
instead ofssh command
which will runcommand
on the remote shell) it starts a login shell. If thestdin
of thessh
is not a tty, it starts a non-interactive shell. This is whyecho command | ssh server
will launch a non-interactive login shell. You can also start one withbash -l -c command
.
Comments
Scripts may contain comments. Comments are special statements ignored by the shell
interpreter. They begin with a #
symbol and continue on to the end of the line.
Given Examples will clear it for you:
#!/bin/bash
# This script will print your username.
whoami
Tip: Use of comments for explaining the purpose of script is always a plus point.
Variables
Variables creating in Bash is as simple as most programming language.
Bash knows no data types. Variables can contain only numbers or a string of one or more characters. There are three kinds of variables you can create: local variables, environment variables and variables as positional arguments.
Local variables
Local variables are variables that exist only within a single script. They are inaccessible to other programs and scripts.
A local variable can be declared using =
sign (as a rule, there should not be any spaces between a variable’s name, =
and its value) and its value can be retrieved using the $
sign. eg:
username="knoldus" # declare variable
echo $username # display value
unset username # delete variable
We can also declare a variable local to a single function using the local
keyword.
local local_var="I'm a local value"
Environment variables
Environment variables are variables accessible to any program or script running in current shell session. They are created just like local variables, but using the keyword export
instead.
export GLOBAL_VAR="I'm a global variable"
There are a lot of global variables in bash. Here is a quick lookup table with the most practical ones:
Variable | Description |
---|---|
$HOME | The current user’s home directory. |
$PATH | A colon-separated list of directories in which the shell looks for commands. |
$PWD | The current working directory. |
$RANDOM | Random integer between 0 and 32767. |
Arrays
Like in other programming languages, an array in bash is a variable that allows you to refer to multiple values.
When dealing with arrays, we should be aware of the special environment variable IFS
. IFS, or Input Field Separator, is the character that separates elements in an array. The default value is an empty space IFS=' '
.
Array declaration
In bash you create an array by simply assigning a value to an index in the array variable:
fruits[0]=Apple
fruits[1]=Pear
fruits[2]=Plum
fruits=(Apple Pear Plum)
Individual array elements are expanded similar to other variables:
echo ${fruits[1]} # Pear
The entire array can be expanded by using *
or @
in place of the numeric index:
echo ${fruits[*]} # Apple Pear Plum
echo ${fruits[@]} # Apple Pear Plum
Array slice
We can extract a slice of array using the slice operators:
echo ${fruits[@]:0:2} # Apple Desert fig
In the example above, ${fruits[@]}
expands to the entire contents of the array, and :0:2
extracts the slice of length 2, which starts at index 0.
Adding elements into an array
Adding elements into an array is quite simple too. We can use them like this:
fruits=(Orange "${fruits[@]}" Banana Cherry) echo ${fruits[@]} # Orange Apple Desert fig Plum Banana Cherry
The example above, ${fruits[@]}
expands to the entire contents of the array and substitutes it into the compound assignment, then assigns the new value into the fruits
array mutating its original value.
Deleting elements from an array
To delete an element from an array, use the unset
command:
unset fruits[0] echo ${fruits[@]} # Apple Desert fig Plum Banana Cherry
Streams, pipes and lists
Bash has powerful tools for working with other programs and their outputs.
Pipes give us opportunity to create conveyors and control the execution of commands.
It is paramount we understand how to use this powerful and sophisticated tool.
Streams
Bash receives input and sends output as sequences or streams of characters.
There are three descriptors:
Code | Descriptor | Description |
---|---|---|
0 | stdin | The standard input. |
1 | stdout | The standard output. |
2 | stderr | The errors output. |
Redirection makes it possible to control where the output of a command goes to, and where the input of a command comes from. For redirecting streams these operators are used:
Operator | Description |
---|---|
> | Redirecting output |
&> | Redirecting output and error output |
&>> | Appending redirected output and error output |
< | Redirecting input |
<< | Here documents syntax |
<<< | Here strings |
Redirection examples :
# output of ls will be written to list.txt ls -l > list.txt # append output to list.txt ls -a >> list.txt # all errors will be written to errors.txt grep da * 2> errors.txt # read from errors.txt less < errors.txt
Pipes
In the example below, command1
sends its output to command2
, which then passes it on to the input of command3
:
command1 | command2 | command3
Constructions like this are called pipelines.
In practice, this can be used to process data through several programs. For example, here the output of ls -l
is sent to the grep
program, which prints only files with a .md
extension, and this output is finally sent to the less
program:
ls -l | grep .md$ | less
If you want your pipelines to be considered a failure if any of the commands in the pipeline fail, you should set the pipefail option with:
set -o pipefail
Lists of commands
A list of commands is a sequence of one or more pipelines separated by ;
, &
, &&
or ||
operator.
If a command is terminated by the control operator &
, the shell executes the command asynchronously in a subshell.
Commands separated by a ;
are executed sequentially: one after another. The shell waits for the finish of each command.
# command2 will be executed after command1 command1 ; command2 # which is the same as command1 command2
Lists separated by &&
and ||
are called AND and OR lists, respectively.
The AND-list looks like this:
# command2 will be executed if, and only if, command1 finishes successfully (returns 0 exit status) command1 && command2
The OR-list has the form:
# command2 will be executed if, and only if, command1 finishes unsuccessfully (returns code of error) command1 || command2
Working with the file system:
Primary | Meaning |
---|---|
[ -e FILE ] | True if FILE exists. |
[ -f FILE ] | True if FILE exists and is a regular file. |
[ -d FILE ] | True if FILE exists and is a directory. |
[ -s FILE ] | True if FILE exists and not empty (size more than 0). |
[ -r FILE ] | True if FILE exists and is readable. |
[ -w FILE ] | True if FILE exists and is writable. |
[ -x FILE ] | True if FILE exists and is executable. |
[ -L FILE ] | True if FILE exists and is symbolic link. |
[ FILE1 -nt FILE2 ] | FILE1 is newer than FILE2. |
[ FILE1 -ot FILE2 ] | FILE1 is older than FILE2. |
Working with strings:
Primary | Meaning |
---|---|
[ -z STR ] | STR is empty (the length is zero). |
[ -n STR ] | STR is not empty (the length is non-zero). |
[ STR1 == STR2 ] | STR1 and STR2 are equal. |
[ STR1 != STR2 ] | STR1 and STR2 are not equal. |
Arithmetic binary operators:
Primary | Meaning |
---|---|
[ ARG1 -eq ARG2 ] | ARG1 is equal to ARG2 . |
[ ARG1 -ne ARG2 ] | ARG1 is not equal to ARG2 . |
[ ARG1 -lt ARG2 ] | ARG1 is less than ARG2 . |
[ ARG1 -le ARG2 ] | ARG1 is less than or equal to ARG2 . |
[ ARG1 -gt ARG2 ] | ARG1 is greater than ARG2 . |
[ ARG1 -ge ARG2 ] | ARG1 is greater than or equal to ARG2 . |
Conditions may be combined using these combining expressions:
Operation | Effect |
---|---|
[ ! EXPR ] | True if EXPR is false. |
[ (EXPR) ] | Returns the value of EXPR . |
[ EXPR1 -a EXPR2 ] | Logical AND. True if EXPR1 and EXPR2 are true. |
[ EXPR1 -o EXPR2 ] | Logical OR. True if EXPR1 or EXPR2 are true. |
Sure, there are more useful primaries and you can easily find them in the Bash man pages.
Using an if
statement
if
statements work the same as in other programming languages. If the expression within the braces is true, the code between then
and fi
is executed. fi
indicates the end of the conditionally executed code.
# Single-line if [[ 1 -eq 1 ]]; then echo "true"; fi # Multi-line if [[ 1 -eq 1 ]]; then echo "true" fi
Likewise, we could use an if..else
statement such as:
# Single-line if [[ 2 -ne 1 ]]; then echo "true"; else echo "false"; fi # Multi-line if [[ 2 -ne 1 ]]; then echo "true" else echo "false" fi
Sometimes if..else
statements are not enough to do what we want to do. In this case we shouldn’t forget about the existence of if..elif..else
statements, which always come in handy.
Look at the example below:
if [[ `uname` == "Adam" ]]; then echo "Do not eat an apple!" elif [[ `uname` == "Eva" ]]; then echo "Do not take an apple!" else echo "Apples are delicious!" fi
Using a case
statement
If you are confronted with a couple of different possible actions to take, then using a case
statement may be more useful than nested if
statements. For more complex conditions use case
like below:
case "$extension" in "jpg"|"jpeg") echo "It's image with jpeg extension." ;; "png") echo "It's image with png extension." ;; "gif") echo "Oh, it's a giphy!" ;; *) echo "Woops! It's not image!" ;; esac
Each case is an expression matching a pattern. The |
sign is used for separating multiple patterns, and the )
operator terminates a pattern list. The commands for the first match are executed. *
is the pattern for anything else that doesn’t match the defined patterns. Each block of commands should be divided with the ;;
operator.
Loops
There are four types of loops in Bash: for
, while
, until
and select
.
for
loop
The for
is very similar to its sibling in C. It looks like this:
for arg in elem1 elem2 ... elemN do # statements done
During each pass through the loop, arg
takes on the value from elem1
to elemN
. For example, if we need to move all .bash
files into the script
folder and then give them execute permissions, our script would look like this:
#!/bin/bash for FILE in $HOME/*.bash; do mv "$FILE" "${HOME}/scripts" chmod +x "${HOME}/scripts/${FILE}" done
while
loop
The while
loop tests a condition and loops over a sequence of commands so long as that condition is true. A condition is nothing more than a primary as used in if..then
conditions. So a while
loop looks like this:
while [[ condition ]] do # statements done
Just like in the case of the for
loop, if we want to write do
and condition in the same line, then we must use a semicolon before do
.
A working example might look like this:
#!/bin/bash # Squares of numbers from 0 through 9 x=0 while [[ $x -lt 10 ]]; do # value of x is less than 10 echo $(( x * x )) x=$(( x + 1 )) # increase x done
until
loop
The until
loop is the exact opposite of the while
loop. Like a while
it checks a test condition, but it keeps looping as long as this condition is false:
until [[ condition ]]; do #statements done
select
loop
The select
loop helps us to organise a user menu. It has almost the same syntax as the for
loop:
select answer in elem1 elem2 ... elemN do # statements done
The select
prints all elem1..elemN
on the screen with their sequence numbers, after that it prompts the user. Usually it looks like $?
(PS3
variable). The answer will be saved in answer
. If answer
is the number between 1..N
, then statements
will execute and select
will go to the next iteration — that’s because we should use the break
statement.
A working example might look like this:
#!/bin/bash PS3="Choose the package manager: " select ITEM in npm gem pip do echo -n "Enter the package name: " && read PACKAGE case $ITEM in npm) npm install $PACKAGE ;; gem) gem install $PACKAGE ;; pip) pip install $PACKAGE ;; esac break # avoid infinite loop done
This example, asks the user what package manager would like to use.
If we run this, we will get:
$ ./my_script 1) npm 2) gem 3) pip Choose the package manager: 2 Enter the package name: bash-handbook <installing bash-handbook>
Functions
In scripts we have the ability to define and call functions.
Calling a function is the same as calling any other program, you just write the name and the function will be invoked.
We can declare our own function this way:
my_func () { # statements } my_func # call my_func
We must declare functions before we can invoke them.
Below is a function that takes a name and returns 0
, indicating successful execution.
# function with params greeting () { if [[ -n $1 ]]; then echo "Hello, $1!" else echo "Hello, unknown!" fi return 0 } greeting Manas # Hello, Manas! greeting # Hello, unknown!
For more info run the man page of bash.
