missing_class
Shell scripting
Table of Contents
- Assignment and string delimiters
- Functions
- Special Variables
- Streams
- Stream redirection
- Return codes and conditional execution
- Command and process substitution
- Shell globbing
- Shebang
- Shell functions and scripts: the differences
- Finding how to use commands
- Finding files
- Finding code
- History: Finding shell commands
- Directory Navigation
- Alternative Shells
- Exercises
- Solutions
Based on https://missing.csail.mit.edu/2020/shell-tools/
- shellcheck: handy tool to check your
bash
scripts (source)- install via
sudo apt-get install shellcheck
- install via
Assignment and string delimiters
=
: assignment; n.b. no spaces$ foo=bar $ echo $foo bar
- ` `: space character performs argument splitting
$ foo = bar Command 'foo' not found
$foo
: access value of variablefoo
'
is for literal strings; bash performs no interpretation"
: bash will substitute variable values$ foo=bar $ echo "Value is $foo" Value is bar $ echo 'Value is $foo' $ Value is $foo
;
: separate multiple commands on a single line
Functions
bash allows you to define functions
mcd () {
mkdir -p "$1"
cd "$1"
}
$1
: first argument to script/function
Special Variables
bash has many special variables to refer to arguments, error codes etc. Reference list.
$0
: script name$1
to$9
: script arguments.$1
is the first argument, …$@
: all arguments$#
: number of arguments$?
: return code of the previous command$$
: process id for the current script!!
: entire last command with arguments- when execution fails due to lack of permissions, quickly execute last command with sudo by doing
sudo !!
- when execution fails due to lack of permissions, quickly execute last command with sudo by doing
$_
: last argument from last command. If you are in an interactive shell, you can also quickly get this value by typingEsc
followed by.
Streams
STDIN
: standard input streamSTDOUT
: standard output streamSTDERR
: standard error output stream
Stream redirection
1> foo
: redirectSTDOUT
tofoo
2> foo
: redirectSTDERR
tofoo
&> foo
: redirect bothSTDOUT
andSTDERR
tofoo
Return codes and conditional execution
0
: everything executed correctly- otherwise: an error occurred
true
program always has 0 return codefalse
programs always has 1 return code- Exit codes can be used to conditionally execute commands:
&&
: and||
: or
$ false || echo "Oops, fail"
Oops, fail
As false returns 1, echo
is executed
$ true || echo "Will not be printed"
$
As true returns 0, echo will not be executed
$ true && echo "Things went well"
Things went well
The second command executes as the first runs without errors
$ false && echo "Will not be printed"
$
The second command doesn’t execute as the first returns an error state
$ false ; echo "This will always run"
This will always run
Command and process substitution
$ foo=$(pwd)
$ echo $foo
/home/user/
Here we are getting the output of command pwd
and storing it as a variable.
- Command substitution:
$(CMD)
executesCMD
and substitutes output of command in-place. -
for file in $(ls)
: shell callsls
, then iterates over those values - process substitution:
<(CMD)
executesCMD
and places output in a temporary file, and substitutes with that file name. This is useful if command expects values to be passed by file instead of STDIN diff <(ls foo) <(ls bar)
: show differences between files in dirsfoo
andbar
by first creating files listing foo and bar, and thendiff
ing them
Here’s an example that iterates through provided arguments grep
s for string foobar
and appends to the file as a comment if it isn’t found
#!/bin/bash
echo "Starting program at $(date)" # Date will be substituted
echo "Running program $0 with $# arguments with pid $$"
for file in $@; do
grep foobar $file > /dev/null 2> /dev/null
# When pattern is not found, grep has exit status 1
# We redirect STDOUT and STDERR to a null register since we do not care about them
if [[ $? -ne 0 ]]; then # if return code of last command is not equal to 0
echo "File $file does not have any foobar, adding one"
echo "# foobar" >> "$file"
fi
done
/dev/null
is a special device you can write to and input will be discarded- Comparisons reference: manpage for
test
-f
: if a file exists
- Use
[[ ]]
over[ ]
as chances of mistakes are reduced, but this is not portable tosh
(Explanation)
Shell globbing
Shell globbing: expanding expression e.g. filename expansion
- wildcards
?
: match one character*
: match any number of chars
{}
: common substring expansion- e.g.
convert image.{png,jpg}
expands toconvert image.png image.jpg
- e.g.
More examples:
cp /path/to/project/{foo,bar,baz}.sh /newpath
expands tocp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath
- Combinations:
mv *{.py,.sh} folder
moves all*.py
and*.sh
files to folder
This creates files foo/a, foo/b, … foo/h, bar/a, bar/b, … bar/h:
$ mkdir foo bar
$ touch {foo,bar}/{a..j}
$ touch foo/x bar/y
Show differences between files in foo and bar
$ diff <(ls foo) <(ls bar)
# Outputs
# < x
# ---
# > y
|
: pipes are also import in scripting; putting output of one program as input to next programbash
scripting
Shebang
e.g. at start of script you will see line #!/bin/bash
#!
: shebang/hashbang indicates this is to be interpreted as an executable file/bin/bash
: indicates the program loader should run this file, with the path to the current script as the first argument- best practice: write shebang lines using (
env
)[http://man7.org/linux/man-pages/man1/env.1.html] command to resolve to wherever the command lives in the system, maximising script portability. This will usePATH
environment variable- e.g.
#!/usr/bin/env python
- e.g.
Shell functions and scripts: the differences
- functions need to be in shell language; scripts can be in any language, hence the shebang
- functions are loaded when the definition is read; scripts are loaded at time of execution
- functions are executed in current shell environment; scripts execute in their own process
- functions can modify environment variables e.g. change current directory, while scripts cannot
- scripts are passed by value environment variables that have been exported using
export
Shell Tools
Finding how to use commands
Given a command how do you go about finding out what it does and its different options?
- Run with
-h
or--help
flags - Look at
man
page for more detail - Interactive tools (e.g. ncurses):
:help
or?
- TLDR pages focuses on giving example use cases of a command so you can quickly figure out which options to use.
Finding files
find
find
recursively searches for files matching some criteria. Some examples:
# Find all directories named src
find . -name src -type d
# Find all python files that have a folder named test in their path
find . -path '**/test/**/*.py' -type f
# Find all files modified in the last day
find . -mtime -1
# Find all zip files with size in range 500k to 10M
find . -size +500k -size -10M -name '*.tar.gz'
Find can also perform actions on the stuff it finds, which helps simplify monotonous tasks.
# Delete all files with .tmp extension
find . -name '*.tmp' -exec rm {} \;
# Find all PNG files and convert them to JPG
find . -name '*.png' -exec convert {} {.}.jpg \;
fd
find
has tricky syntax:
e.g. to find files that match some pattern PATTERN
you have to execute find -name '*PATTERN*'
(or -iname
if you want the pattern matching to be case insensitive).
- you can start building aliases for those scenarios but as part of the shell philosophy is good to explore using alternatives.
- you can find (or even write yourself) replacements for some.
e.g.
fd
is a simple, fast and user-friendly alternative tofind
.- colorized output, default regex matching, Unicode support, more intuitive syntax
- syntax to find a pattern
PATTERN
isfd PATTERN
.
locate
- uses a compiled index/database for quickly searching
- database that is updated using
updatedb
. - in most systems
updatedb
is updated daily viacron
- trade-off compared to find/fd vs locate is between speed vs freshness.
find
etc. can files using attributes (e.g. file size, modification time or file permissions) whilelocate
just uses the name.- in depth comparison
Finding code
- You want to search for all files that contain some pattern, along with where pattern occurs in those files.
grep
: generic tool for matching patterns from input text- many flags, very versatile
-C
for getting Context around the matching line. e.g.grep -C 5
will print 5 lines before and after the match.-v
for inverting the match, i.e. print all lines that do not match the pattern-R
Recursively goes into directories and look for text files for the matching string
But grep -R
can be improved in many ways, such as ignoring .git
folders, using multi CPU support, etc.
Various alternatives, but pretty much cover same use case:
Some examples:
# Find all python files where I used the requests library
rg -t py 'import requests'
# Find all files (including hidden files) without a shebang line
rg -u --files-without-match "^#!"
# Find all matches of foo and print the following 5 lines
rg foo -A 5
# Print statistics of matches (# of matched lines and files )
rg --stats PATTERN
History: Finding shell commands
-
Problem: you want to find specific commands you typed at some point.
- up arrow: gives you back your last command, slowly goes through your shell history
history
: lets you access your shell history programmatically; print your shell history to the standard output- we can pipe that output to
grep
and search for patterns- e.g.
history | grep find
will print commands with the substring “find”.
- e.g.
- we can pipe that output to
- if you start a command with a leading space it won’t be added to you shell history. useful when you are typing commands with passwords or other bits of sensitive information.
- If you make the mistake of not adding the leading space you can always manually remove the entry by editing your
.bash_history
or.zhistory
. Ctrl+R
: backwards search through your history.- After pressing
Ctrl+R
you can type a substring you want to match for commands in your history. - As you keep pressing it you will cycle through the matches in your history.
- This can also be enabled with the UP/DOWN arrows in zsh.
- A nice addition: fzf bindings.
fzf
is a general purpose fuzzy finder that can used with many commands. used to fuzzily match through your history and present results in a convenient and visually pleasing manner.
- After pressing
History-based autosuggestions
- First introduced in fish shell, this feature dynamically autocompletes your current shell command with the most recent command that you typed that shares a common prefix with it
- can be enabled in zsh and it is a great quality of life trick for your shell
Directory Navigation
- Problem: how to quickly navigate directories
- writing shell aliases
- creating symlinks with ln -s
fasd
: Find frequent and/or recent files and directories- Fasd ranks files and directories by frecency (both frequency and recency)
- most straightforward use is autojump which adds a
z
command that you can use to quicklycd
using a substring of a frecent directory. E.g. if you often go to/home/user/files/cool_project
you can simplyz cool
to jump there.
More complex tools to get an overview of a directory structure
Alternative Shells
zsh
: Z shell is an extended version of the Bourne Shell (sh
), based on bash
zsh cheatsheet
Pros: ref
- automatic
cd
: just type name of directory - recursive path expansion: e.g.
/u/lo/b
expands to/user/local/bin
- spelling correction and approx. completion
- plugin and theme
Installation guide Install Oh My Zsh To get fonts working correctly using Ubuntu via WSL, had to install powerline fonts on Windows by cloning the repo and installing (ref):
>git clone https://github.com/powerline/fonts.git
>cd fonts
>.\install.ps1
Manually select a powerline font on the Ubuntu window for special characters to work. Change directory colours (select from here)
# using dircolors.ansi-dark
curl https://raw.githubusercontent.com/seebi/dircolors-solarized/master/dircolors.ansi-dark --output ~/.dircolors
## set colors for LS_COLORS
eval `dircolors ~/.dircolors`
NB configuration menu accessed by
$ autoload -Uz zsh-newuser-install
$ zsh-newuser-install -f
Exercises
-
Read
man ls
and write anls
command that lists files in the following manner- Includes all files, including hidden files
- Sizes are listed in human readable format (e.g. 454M instead of 454279954)
- Files are ordered by recency
- Output is colorized
A sample output would look like this
-rw-r--r-- 1 user group 1.1M Jan 14 09:53 baz drwxr-xr-x 5 user group 160 Jan 14 09:53 . -rw-r--r-- 1 user group 514 Jan 14 06:42 bar -rw-r--r-- 1 user group 106M Jan 13 12:12 foo drwx------+ 47 user group 1.5K Jan 12 18:08 ..
-
Write bash functions
marco
andpolo
that do the following. Whenever you executemarco
the current working directory should be saved in some manner, then when you executepolo
, no matter what directory you are in,polo
shouldcd
you back to the directory where you executedmarco
. For ease of debugging you can write the code in a filemarco.sh
and (re)load the definitions to your shell by executingsource marco.sh
. -
Say you have a command that fails rarely. In order to debug it you need to capture its output but it can be time consuming to get a failure run. Write a bash script that runs the following script until it fails and captures its standard output and error streams to files and prints everything at the end. Bonus points if you can also report how many runs it took for the script to fail.
#!/usr/bin/env bash
n=$(( RANDOM % 100 ))
if [[ n -eq 42 ]]; then
echo "Something went wrong"
>&2 echo "The error was using magic numbers"
exit 1
fi
echo "Everything went according to plan"
-
As we covered in lecture
find
’s-exec
can be very powerful for performing operations over the files we are searching for. However, what if we want to do something with all the files, like creating a zip file? As you have seen so far commands will take input from both arguments and STDIN. When piping commands, we are connecting STDOUT to STDIN, but some commands liketar
take inputs from arguments. To bridge this disconnect there’s thexargs
command which will execute a command using STDIN as arguments. For examplels | xargs rm
will delete the files in the current directory.Your task is to write a command that recursively finds all HTML files in the folder and makes a zip with them. Note that your command should work even if the files have spaces (hint: check
-d
flag forxargs
)find . -type f -name "*.html" | xargs -d '\n' tar -cvzf archive.tar.gz
-
(Advanced) Write a command or script to recursively find the most recently modified file in a directory. More generally, can you list all files by recency?
Solutions
$ ls -laht --color
-
marco() { export MARCO=$(pwd) } polo() { cd "$MARCO" }
-
#!/usr/bin/env bash n=$(( RANDOM % 100 )) if [[ n -eq 42 ]]; then echo "Something went wrong" >&2 echo "The error was using magic numbers" exit 1 fi echo "Everything went according to plan"
- First create some html files
$ mkdir foo bar $ touch {foo,bar}/{a..g}.html $ touch {a..g}.html $ find . -path "*.html" ./a.html ./b.html ./bar/a.html ./bar/b.html ./bar/c.html ./bar/d.html ./bar/e.html ./bar/f.html ./bar/g.html ./c.html ./d.html ./e.html ./f.html ./foo/a.html ./foo/b.html ./foo/c.html ./foo/d.html ./foo/e.html ./foo/f.html ./foo/g.html ./g.html $ find . -name "*.html" | xargs -d '\n' tar -cvzf test.tar.gz