Introduction to Shell

post hero image

Introduction

The shell is the command interpreter: executes the given commands one by one.

loop forever
  <accept command from console>
  <execute command>
end loop;

shell is a command processor that accept commands given from the terminal (or from a command file) and execute them until the file’s end.

As a convention, inside all the shell snippets written on those blog posts, $ symbol (dollar sign) and a space will precede a command you could launch on your Terminal. You must not write the $ and the space on your Terminal window, but what’s after it. I’ll also write the output in the lines right below that command.

Here’s an example:

COMMAND
# OUTPUT OF THE COMMAND
# my own comments about what's happening

Do not confuse COMMAND with $COMMAND, which is a variable named COMMAND.


^KEY means the key combination Ctrl + KEY. So ^C means Ctrl + C, ^X means Ctrl + X and so on. Here are some useful key combinations:

  • ^C stops the executing of a command in the Terminal
  • ^L clears the screen and place the cursor back on top

Moreover, I really suggest you to read UNIX Introduction article before moving on. Anyway, let’s make a very brief recap about wild cards and parsing.

Special characters

Wild cards are also named metacharacters or special characters. They allow the shell to execute pattern matching between a string and file names in the present working directory.

The symbol * abbreviates any string of zero or more characters in a file name:

[ccc...] stands for any character in a file name, between the elements the square brackets.

[c-c...] stands for any character in a file name, in all the elements between the extremes of the set(s) in the square brackets.

The hash symbol # comments until the end of the line (as you can see in almost all the examples).

The backslash (\ symbol) is the escape metacharacter: it tells bash not to interpret the subsequent character as a special character.

Please Note:

  1. Current directory is identified by the single dot (. symbol).
  2. Parent directory of a given directory is identified by a couple of dots (..).

Parsing

Before executing, the command line is parsed to look after special characters. The very first parsed metacharacters are redirection and piping. In the subsequent scanning, if the shell find any other special character, result in one of the following substitutions:

  1. command: commands place among (backquotes) are executed and the result is produced,
  2. variable & parameters: variables’ names ($VAR_NAME) are substituted with their actual values,
  3. file names: *, ?, [] metacharacters are substituted into the file names using pattern matching

Moreover, the use of the quotes (single and double) allow and disallow certain types of substitutions:

  1. Single quotes disallow all the substitution types
  2. Double quotes only disallow file names substitutions

Both single and double quotes disallow the shell to interpreter redirection and piping metacharacters.

In addition, eval command executes arguments as a shell command. It allows an additional phases of substitution.

Program the shell

The command processor can elaborate commands taking them from a file which is usually called command file.

A commands file can contain:

  • passing of parameters
  • variables
  • control flow statements

Note that what instructions are available depends on what shell is being used.

Please Note: shell, meant as a programming language, is interpreted (not compiled).

Shell types

In modern Linux (and Unix) systems, there are many types of shells:

  • sh: Bourne Shell
  • bash: Bourne Again Shell, advanced sh version
  • zsh: Z Shell, very advanced sh version
  • ksh: Korn Shell
  • csh: C Shell, syntax similar to C language
  • tcsh: Turbo C Shell, advanced csh version
  • rush: Ruby Shell, Ruby-based shell
  • hotwire

Hashbang

Based on the type of shell is being used, the command file, that below will simply be called script, must have a different hashbang (or shebang). The hashbang is the first line of the script, it tells the shell to run the given script with the specified command. It’s not mandatory for the hashbang line to be an executable of a shell. It can be anything, even the cat command: #!/bin/cat.

Note that it must start with #! (while ordinary comments only starts with #).

Here are some hashbang for the shells above:

#!/bin/sh
#!/bin/bash
#!/bin/zsh
# ...

Passing parameters

Arguments are positional variables in the invocation line.

$0 represents the name of the command file with is executing.
$1 represents the 1st argument, $2 represents the 2nd argument and so on.

echo $0
# /bin/bash

It’s possible to slide all arguments from right to left (shifting). $0 will never be lost.

Important variables

$* stores the set of positional variables which correspond to the command arguments ($1, $2, …).

$# stores the number of passed arguments (excluding $0).

$? stores the integer value returned by the last executed command.
If $? is equal to 0, the command has been executed correctly. If $? is major than 0 (holds an integer value), an error occurred during command execution.

$$ stores the PID (Process Identifier) of the process which is executing.

Other important variables are: $PATH, $HOME, $UID and $IFS.

$IFS contains the list of characters used as input separators. The default value is space, tab and new line (\n). You can modify $IFS when you need to process an input made up by data where spaces and tabs have a very different meaning.

Command Execution

To run your shell script, need to make it executable:

chmod +x my-command

# give execute permissions to all
# .sh files in the current directory
chmod +x *.sh

The shell looks for commands to execute only inside the directories specified in PATH environment variable. Paths are separated by colon sign:

 echo $PATH
/home/pit/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

# display them in a more readable way
echo $PATH | tr ':' '\n'
# /home/pit/.local/bin
# /usr/local/sbin
# /usr/local/bin
# /usr/sbin
# /usr/bin
# /sbin
# /bin
# /usr/games
# /usr/local/games
# /snap/bin

To execute a command that is not in a directory listed by PATH variable, you need to specify its absolute path.

If you want to execute a command placed in the current working directory:

./my-command

You can also invoke a shell to execute the command, it will bypass the hashbang:

# SHELL my-command
sh my-command
bash my-command
zsh my-command

In this way, you don’t need to use the prefix ./ since the shell does not look for commands in the paths of PATH variable, but simply runs my-command (whose name is passed as command line argument).


Given this short script, named my-script.sh:

#!/bin/cat

pwd
echo $0
echo $HOME

If you run it, it will execute cat command over the script:

chmod +x my-script.sh
./my-script.sh
#!/bin/cat

pwd
echo $0
echo $HOME

But if you invoke sh shell to execute the command, it will bypass the cat hashbang:

sh ./my-script.sh
# /home/pit/Documents
./my-script.sh
# /home/pit

Input & Output

To get the input from the keyboard you can use the read command.

read reads a single input line and assign its value to the variables passed as parameters. It consider as input separators those defined in $IFS.

read hello world
# Hello, World!
# write the above line on the Terminal

echo $hello
# Hello,

echo $world
# World!

echo $hello $world
# Hello, World!

To avoid errors while parsing the input, it’s better to launch a read for each variable you need to create.

echo command, as you saw many times, outputs a line of text on the standard output. You can also redirect the output to a file using > sign.

Data types

Variables

Variables are all strings. You must avoid spaces between = sign during assignment, otherwise the shell won’t understand what you’re doing.

hello="Hello, World!"
echo $hello
# Hello, World!

Arrays

Bourne Shell (sh) does not support arrays. You can simulate them in this way:

n=1
eval arr[$n]=a
eval echo \${arr[$n]}
a # output

n=2
eval arr[$n]=b
eval echo \${arr[$n]}
b # output

n=3
eval arr[$n]=c
eval echo \${arr[$n]}
c # output

n='*'
eval echo \${arr[$n]}
a b c # output

Note that many other types of shell does support arrays.

Control Structures

There are a couple of commands that are mandatory to understand and know how to use.

expr command evaluates arithmetical expressions taking string arguments.
test command is used in almost all control structures.

if

if <commands-list>
  then
    <comands>
  [else <comands>]
fi

Note that keywords (do, then, fi, …) must be placed after ; separator or in the line below. if checks the output value of the last command from <commands-list>.

Here’s a simple example:

#!/bin/sh

x="a"
y="a"

# both if statements are equivalent
# if [ $x = $y ]
if test "$x" = "$y"
    then echo "equal"
    else echo "NON equal"
fi

The output will always be equal unless you change the values of x and y variables.

case

case <var> in
  <pattern-1>) <comands> ;;
  <pattern-i> | <pattern-j> | <pattern-k>) <comands> ;;
  <pattern-n>) <comands> ;;
esac

case-test.sh contents is:

#!/bin/sh

case $1 in
    "NULL" ) echo "The input is NULL" ;;
    S* | s* | Y* | y* ) echo "Approved" ;;
    *) echo "Default branch: none of the above"
esac

The 1st case is given by:

./case-test.sh NULL
The input is NULL

The 2nd case:

./case-test.sh Yes
./case-test.sh yellow
./case-test.sh Sunglesses
./case-test.sh silent
Approved # all the above lines gets this output

The default branch is:

./case-test.sh car
./case-test.sh Butter
./case-test.sh Null # shell is case sensitive
Default branch: none of the above
# all the above lines gets this output

check if a parameter is a number

The script check-int.sh accepts one argument and ignore all the others.

#!/bin/sh

# save 1st argument into $num
num="$1"

# check if $num is (or not) a numeric value
case $num in
    *[!0-9]*) echo "$num is not a number"; exit 1 ;;
    *) echo "$num is a number";;
esac

The output is:

./_check-int.sh abc
# abc is not a number

./_check-int.sh 123
# 123 is a number

./_check-int.sh A123
# A123 is not a number

./_check-int.sh 456Z
# 456Z is not a number

for

for <var> [in <list of strings>]
do
  <comands>
done

The sketch below prints all the elements in the current directory:

#!/bin/bash

for i in *
do
    echo "print: "$i
done

cmd-args.sh script loops over the command arguments and prints them

echo "positional variables are:"
for i in $*
do
    echo $i
done

Let’s test it out:

./cmd-args.sh /dev/ hello good
positional variables are:
/dev/
hello
good

while

while <commands-list>
do
  <comands>
done

while-test.sh script prints on the standard output numbers from 0 to $1:

#!/bin/bash

a=0
while test $a -le $1
do
    echo $a
    a=`expr $a + 1` # increment
done

The output is:

./while-test.sh 3
0
1
2
3

# if $1 is not an integer number, throws an error
./while-test.sh hello
./while-test.sh: line 4: test: hello: integer expression expected

until

until <commands-list>
do
  <comands>
done

It’s a while loop with the inverse of the entry condition.

while-test.sh script prints on the standard output numbers from $1 to 0:

#!/bin/bash

a=$1
until test $a -le 0
do
    echo $a
    a=`expr $a - 1` # decrement
done

The output is:

./until-test.sh 3
3
2
1

# if $1 is not an integer number
# enter an infinite loop throwing errors
./until-test.sh hello
expr: syntax error: unexpected argument ‘1’
./until-tests.sh: line 4: test: -le: unary operator expected

# you need to manually stop the script
^C

select

select <variable> in <list-of-variables>
do
    case <variable> in
        <pattern-i>) <comands> ;;
        <pattern-j>) <comands> ;;
        <pattern-k>) <comands> ;;
    esac
done

Override the content of PS3 variable to change the message displayed to the user before the choice.

#!/bin/bash

PS3="Choice: "
echo "Where do you live? "

select answer in Rome Paris Copenaghen none
do
    case $answer in
        Rome) echo "Rome is the capital of Italy." ;;
        Paris) echo "Look the Eiffel Tower." ;;
        Copenaghen) echo "The most populous city of Denmark." ;;
        none) break ;;
        *) echo "You need to enter an item from the list!" ;;
    esac
done

The output is:

./select-test.sh
Where do you live?
1) Rome
2) Paris
3) Copenaghen
4) none
Choice: 1 # user input: 1
Rome is the capital of Italy.
Choice: 3
The most populous city of Denmark.
Choice: 2 # user input: 2
Look the Eiffel Tower.
Choice: 78 # user input: 78
You need to enter an item from the list!
Choice: 4 # user input: 4, which is the exit case

Quotes

References: