How to automate your computing (and life) with all the commands you already know.
Marek Šuppa
Ondrej Jariabka
Adrián Matejov
Much of the UNIX philosophy we've seen so far was focused on composability of small programs doing one thing, and doing it well
Creating larger compositions out of these "small primitives" is the obvious next step
The easiest way of starting with automation
Apart from being a terminal shell, shell/Bash is also a full-fledged programming language
Bash scripts often start as oneliners in the shell and get moved to a specific "script"
Apart from being a terminal shell, shell/Bash is also a full-fledged programming language
Bash scripts often start as oneliners in the shell and get moved to a specific "script"
Scripts can become programs like any other we've met so far
Usually text files with the .sh
(or .bash
) file extension
Usually text files with the .sh
(or .bash
) file extension
Let's start with a simple one, called grouplist.sh
:
#!/bin/bashcat /etc/group | cut -d: -f1 | sort
Usually text files with the .sh
(or .bash
) file extension
Let's start with a simple one, called grouplist.sh
:
#!/bin/bashcat /etc/group | cut -d: -f1 | sort
The script can be executed by passing it to bash
:
$ bash grouplist.shadm adman audio backup bifadm bin cdrom crontab daemon ... [ 55 lines omitted ] ...
Anything after the hash mark (#
) is considered a comment:
$ echo "Hi there!" # This text will be ignoredHi there!
Anything after the hash mark (#
) is considered a comment:
$ echo "Hi there!" # This text will be ignoredHi there!
The first line of grouplist.sh
is actually a special kind of comment called "shebang".
Anything after the hash mark (#
) is considered a comment:
$ echo "Hi there!" # This text will be ignoredHi there!
The first line of grouplist.sh
is actually a special kind of comment called "shebang".
Shebangs start with #!
, followed by the absolute path to the file's interpreter.
#!/bin/bashcat /etc/group | cut -d: -f1 | sort
If a file contains a shebang and has the executable (x
) permission set, the shell will use it to run it.
$ chmod +x ./grouplist.sh$ ./grouplist.sh | head -n 3admadmanaudio
In Bash (scripts or just shell), variables are defined using =
$ VAR=something
In Bash (scripts or just shell), variables are defined using =
$ VAR=something
And interpolated (expanded to the value they hold) by adding the $
prefix to their names:
$ VAR=something$ echo $VARsomething
In Bash (scripts or just shell), variables are defined using =
$ VAR=something
And interpolated (expanded to the value they hold) by adding the $
prefix to their names:
$ VAR=something$ echo $VARsomething
Many useful variables are pre-set out of the box:
# The name of the current user$ echo $USERmrshu# The home (~) directory of the# current user$ echo $HOME/home/mrshu
# The current working directory$ echo $PWD/tmp
You can see all of them by running env
or printenv
.
By default, the variables are local to the process they are defined in.
$ cat varprinter.sh#!/bin/bashLOCALVAR=localizedecho LOCALVAR=$LOCALVARecho VAR=$VAR$ chmod +x varprinter.sh$ VAR=something$ ./varprinter.shLOCALVAR=localizedVAR=
(Undefined variables default to empty strings.)
By default, the variables are local to the process they are defined in.
$ cat varprinter.sh#!/bin/bashLOCALVAR=localizedecho LOCALVAR=$LOCALVARecho VAR=$VAR$ chmod +x varprinter.sh$ VAR=something$ ./varprinter.shLOCALVAR=localizedVAR=
(Undefined variables default to empty strings.)
Using export
, they can be exported to (or "inherited" by) child processes:
$ export VAR=exported$ ./varprinter.shLOCALVAR=localizedVAR=exported
Just as other programs, Bash scripts can receive arguments.
$#
$0
$1
, $2
, ..., $9
$ cat argvprinter.sh #!/bin/bashecho "Running cmd: $0"echo "Number of arguments: $#"echo "First argument: $1"
$ ./argvprinter.shRunning cmd: ./argvprinter.shNumber of arguments: 0First argument:
$ ./argvprinter.sh showRunning cmd: ./argvprinter.shNumber of arguments: 1First argument: show
$ bash argvprinter.sh showRunning cmd: argvprinter.shNumber of arguments: 1First argument: show
Strings in apostrophes or single-quotes (such as 'some string'
) are printed verbatim.
$ echo 'I am logged in as $USER'I am logged in as $USER
Strings in apostrophes or single-quotes (such as 'some string'
) are printed verbatim.
$ echo 'I am logged in as $USER'I am logged in as $USER
In double-quote strings (like "some string"
), the variables are first interpolated.
$ echo "I am logged in as $USER"I am logged in as mrshu
Strings in apostrophes or single-quotes (such as 'some string'
) are printed verbatim.
$ echo 'I am logged in as $USER'I am logged in as $USER
In double-quote strings (like "some string"
), the variables are first interpolated.
$ echo "I am logged in as $USER"I am logged in as mrshu
This can be used very nicely for string concatenation:
$ echo "I am $USER@$HOSTNAME"I am mrshu@davos
When executing a shell command, the parsing process goes as follows:
>
, <
, |
, ...)*
, ?
, ...)$VAR
)$ echo I like *I like argvprinter.sh grouplist.sh$ echo '*'I like *$ echo Nice $VAR emoji! >*-bash: *: ambiguous redirect$ echo 'Nice $VAR emoji! >*'Nice $VAR emoji! >*$ VAR=smiling$ echo "Nice $VAR emoji! >*"Nice smiling emoji! >*
Output (written to stdout or stderr) of any command can be saved to a variable.
This concept is called "command expansion" and can be done either via $()
or backticks
$ a=$(echo 'hello' | tr '[:lower:]' '[:upper:]')$ b=$(echo 'WORLD' | tr '[:upper:]' '[:lower:]')$ echo "$a, $b"HELLO, world
Example from http://www.compciv.org/topics/bash/variables-and-substitution/
On finish, each program returns a so called "exit code".
In Bash, it is stored in the $?
variable.
$ grep cmd argvprinter.sh echo "Running cmd: $0"$ echo $?0
Exit code 0
generally denotes EXIT_SUCCESS
: the program finished successfully.
On finish, each program returns a so called "exit code".
In Bash, it is stored in the $?
variable.
$ grep cmd argvprinter.sh echo "Running cmd: $0"$ echo $?0
Exit code 0
generally denotes EXIT_SUCCESS
: the program finished successfully.
A non-zero exit code generally means that some error happened.
The most general one is EXIT_FAILURE
, which is set to 1
on Unix systems.
$ grep non-existent-word argvprinter.sh $ echo $?1
In Bash scripts, the exit code can be set in two ways:
Implicitly, as the exit code of the last executed command
Explicitly via the exit
command (which also stops its execution)
$ cat greeter.sh #!/bin/bashecho "Hello $1!"
$ bash greeter.sh thereHello there!$ echo $?0
$ cat exiter.sh #!/bin/bashexit 47echo "Hello $1!"
$ bash exiter.sh there$ echo $?47
In Bash scripts, the exit code can be set in two ways:
Implicitly, as the exit code of the last executed command
Explicitly via the exit
command (which also stops its execution)
$ cat greeter.sh #!/bin/bashecho "Hello $1!"
$ bash greeter.sh thereHello there!$ echo $?0
$ cat exiter.sh #!/bin/bashexit 47echo "Hello $1!"
$ bash exiter.sh there$ echo $?47
The concept of exit codes is also very useful when evaluating conditions.
if
conditionsThe basic syntax is as follows:
if cmd ; then another_command; further_command;fi
If cmd
finishes with exit code 0
, the commands following the then
clause are executed.
if
conditionsThe basic syntax is as follows:
if cmd ; then another_command; further_command;fi
If cmd
finishes with exit code 0
, the commands following the then
clause are executed.
A simple example to demonstrate it practically:
#!/bin/bashif grep -q "data" /etc/group; then echo 'Group data seems to exist.'fi
The execution procedure goes as follows:
grep -q "data" /etc/group
is executeddata
can be found in /etc/group
, the status code will be set to 0
and to 1
otherwise.0
, the echo
part will be executed.if
with test
In principle, any command can be used with if
.
In practice, the test
command is used quite often.
if test -d /tmp; then echo "The /tmp directory exists"fi
if
with test
In principle, any command can be used with if
.
In practice, the test
command is used quite often.
if test -d /tmp; then echo "The /tmp directory exists"fi
STRING1 == STRING2
STRING1 != STRING2
-e FILE
FILE
exists-f FILE
FILE
is a regular file-d FILE
FILE
is a directoryNUM1 -eq NUM1
NUM1
and NUM2
are numerically equalNUM1 -ne NUM1
NUM1
and NUM2
are not numerically equalNUM1 -gt NUM1
NUM1
is greater than NUM2
NUM1 -lt NUM1
NUM1
is less than NUM2
For much more, see man 1 test
.
if
with test
II#!/bin/bashif test -d .tmp; then echo "The directory .tmp exists; proceeding."fiif test -f .config; then echo "Config file .config exists; proceeding."ficp -R .tmp .config /backupif test $? -eq 0; then echo "Directory .tmp and file .config copied successfully."fi
if
with test
II#!/bin/bashif test -d .tmp; then echo "The directory .tmp exists; proceeding."fiif test -f .config; then echo "Config file .config exists; proceeding."ficp -R .tmp .config /backupif test $? -eq 0; then echo "Directory .tmp and file .config copied successfully."fi
Writing test
so often is a bit obnoxious, so POSIX also has a shortcut: [
and ]
:
#!/bin/bashif [ -d .tmp ]; then echo "The directory .tmp exists; proceeding."fi
Note the spaces. They are mandatory -- [
is actually a command (normally stored in /usr/bin/[
)
if
, elif
, else
Bash also supports the standard if
/elif
/else
conditions.
if cmd1 ; then other_if_command;elif cmd2 ; then other_elif_command;else else_command;fi
if
, elif
, else
Bash also supports the standard if
/elif
/else
conditions.
if cmd1 ; then other_if_command;elif cmd2 ; then other_elif_command;else else_command;fi
A quick example:
#!/bin/bashif [ "$USER" == "root" ]; then echo "You may proceed";elif groups | grep -q sudo; then echo "Please become root to run this"else echo "Sorry, only root is allowed to run this";fi
cmd1 | cmd2
cmd1 && cmd2
cmd2
will be executed only if cmd1
returns 0cmd1 || cmd2
cmd2
will be executed only if cmd1
does not return 0There are also true
/false
constants:
true
false
$ true && echo "We will see this"We will see this
$ false && echo "We will not see this"
$ false || echo "We will see this"We will see this
$ grep -q aZn31A /etc/passwd | true$ echo $?0
If we do not care about the exit code but would like to put multiple commands
on the same line, we can separate them with ;
.
$ echo "First"; echo "Second"FirstSecond
A quick example
#!/bin/bashif [ "$USER" == "root" ] || [ "$USER" == "mrshu" ]; then echo "You may proceed";elif groups | grep -q sudo; then echo "Please become root to run this"else echo "Sorry, only root or mrshu are allowed to run this";fi
while
loopCheck the exit code of cmd1
. If zero, execute cmd2
.
while cmd1; do cmd2;done
while
loopCheck the exit code of cmd1
. If zero, execute cmd2
.
while cmd1; do cmd2;done
The script below waits until firefox
is running.
#!/bin/bashwhile ps -ef | grep -v grep | grep firefox; do echo "Firefox not running, will check in 10 seconds" sleep 10done
for
loopIterates over the list of values such that i
is set to val1
, val2
and val3
.
for i in val1 val2 val3; do echo $idone
If we want to quickly generate a sequence of numbers, the seq
command can come handy:
$ seq 1 512345
$ cat iterator.sh#!/bin/bashfor i in $(seq 1 5); do echo "Checking number $i"done
$ bash iterator.shChecking number 1Checking number 2Checking number 3Checking number 4Checking number 5
case
A shortcut, so that one does not have to write out so many if
s.
Note the double semicolons (;;
) -- they are required in this case.
case string in str1) cmd1;; str2) cmd2;; *) catchall-cmd;;esac
$ cat login.sh#!/bin/bashcase "$1" in root) echo "Welcome, you can come in" ;; mrshu) echo "Please provide password" ;; *) echo "Name not recognized";;esac
$ bash login.sh mrshuPlease provide password$ bash login.sh rootWelcome, you can come in$ bash login.sh vidriduchName not recognized
tar
creates a "package" from a filesystem path
concatenates files and directories without compression
$ tar -cf backup.tar /home/mrshu
tar
package to the current directory$ tar -xf backup.tar
-O
sends output to stdoutgzip
compressiongzip file
file.gz
out of file
gunzip file.gz
file.gz
and creates file
-c
sends the output to stdoutgzip
is just one of the available compression methods, there is also bzip2
and xz
tar
+ compression$ tar -czf package.tar.gz path$ tar -xzf package.tar.gz
$ tar -cjf package.tar.bz2 path$ tar -xjf package.tar.bz2
$ tar -cJf package.tar.xz path$ tar -xJf package.tar.xz
tar
+ compression through pipeWith gzip
:
$ cat file | gzip > tmp.gz$ cat tmp.gz | gunzip > unzipped_file$ gunzip -c tmp.gz | head
With tar
$ cat package.tgz | gunzip | tar -x -O | less$ cat package.tar.gz | tar -x -z -O | less$ cat package.tar.bz2 | bunzip2 | tar -xO | tail
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |