====================
Key terminology (simplified):
- OS: operating system, what actually does the work under the hood, manages running tasks
- Terminal: A text input/output environment, translates keyboard strokes into text, handles text display on the screen
- Shell: a command-line interpreter, handles text input from the terminal and converts it into code to be run by the OS
The Bourne-Again SHell (BASH) is a particular shell environment, developed as a successor to the Bourne shell (SH) written by Steven Bourne. BASH is one of the most popular flavors of shell, though many alternatives exist (zsh, tcsh, ksh, ...). All of them have slightly different syntax, but they function the same: serve as an mediator between the user acting on a terminal and the OS.
- Prevalence: It's absolutely everywhere, if you're interested in scientific computing you're pretty much guaranteed to need to work with a Unix or Linux system at some point, and BASH is your portal to working with these systems.
- Utility: BASH offers a wide range of features and functions to interact with file systems and the OS
- Scriptable: can automate tasks that would be difficult or tedious to do with only a graphical interface
To access a BASH environment, we need to use some kind of terminal application. Everyone has their favorite (I like iTerm2 on OSX and Terminator on Linux), but on Linux/OSX there will be a default terminal:
If you're on Windows, you'll need a terminal program (I've been told there's a bash app now in Windows 10). Back before then, there were a few different options, you could dual boot Linux on your machine, or you could use PuTTY, a terminal app, to connect to a remote Linux machine. For those on an older version of Windows, you can use PuTTY to access the campus engineering workstations (EWS) while on campus (or through VPN):
Hostname: <NetID>@remlnx.ews.illinois.edu
If other folks running Linux/OSX like to work on the EWS system, you can run the following command in your own terminals:
ssh <NetID>@remlnx.ews.illinois.edu
The ssh command standards for secure shell, a network protocol for connecting your terminal to a remote shell application. Just a note: I find it super slow to run on the EWS system. If you can do things locally/on another system then it might be a better option.
Opening a new terminal window will start a new instance of your default shell. We can type commands or arbitrary text into this terminal, and send them to the BASH interpreter for processing. Because every language (yes, BASH is a fully functioning language!) needs their own 'Hello World' example, here's the simple way in BASH. Note: the $ symbol is how we establish that we are currently sending commands to the shell from a terminal. I've also seen other terminal characters like >, but for the purposes of this tutorial, $ commands will indicate sending bash shell commands directly in the terminal.
$ echo Hello, Bash!
Hello, Bash!
While this quick test shows how easy it is, there's a lot going on under the hood. First, the terminal window takes your keyboard input and displays it to the screen. Upon pressing a carriage return, the full line of text is passed from the terminal to the shell, which parses that text input. The shell parses out the echo command, which says take the block of characters following this command, and pass it directly back to the terminal window (echoing the input).
pwd- get the path for the (p)resent (w)orking (d)irectoryls- list contents of current directorycd <path>- change directory to <path>
Often one of the first things you'd like to know when you start a new instance of BASH is what directory are you currently working in. To do this, we'll use the pwd command. This will give the full path to the current working directory.
A directory is similar to the notion of a folder in a graphical desktop environment, it contains a list of files and subdirectories. The file system can be conceptualized as a tree of directories.

A path is a location of a directory or file from the root directory
/, you can think of it as the names of the folders you traversed to get to that directory/file. Relative paths are listed from the current directory, and are not started with a/symbol. Absolute paths are given from the root directory/.
If we'd like to look at the contents of our current directory, we can use the ls command. This will return a lexicographically sorted list of all files and subdirectories present. Under the hood, ls is actually equivalent to ls ., calling for a list of files in the . current directory. If we'd like the files in an alternate directory, we can look at them directly using ls <path>. There are tons of extra flags we can pass to ls, but I'll list only the two that I think are useful to know (see: http://linuxcommand.org/man_pages/ls1.html):
ls -lh- list contents of directory in long (-l), human-readable (-h) form (includes data on file size/access date/permissions)
Knowing the contents of a directory, it's time we were able to move between directories. To do this, we will use the cd command. We hand cd a path we would like to move to, either relative to our current directory or absolute. Using the example directory tree above, if pwd returns /users/john, we could cd work or cd /users/john/work/ to access our work folder.
.- the current directory..- the directory above.in the tree~- a user's home directory (usually/home/<UserName>/or/users/<UserName>/)/- root directory of the file system
cp <source> <destination>- copy file from source to destinationmv <source> <destination>- move file from source to destinationrm <filepath>- delete file specificied by pathmkdir <path>- make a new directory at pathrmdir <path>- remove the directory at path
Examples:
cp Readme.md ../Readme.old.md takes the current Readme.md file and copies it to the previous directory in the tree, modifying the name of the file to Readme.old.md
mv AllMyData.csv Backup/ will move the file to the folder Backup in the current working directory, maintaining the same file name.
rm ~/Downloads/UnhelpfulReviewersComments.pdf will remove the file from the Downloads folder in the user's home directory.
mkdir ~/Backups/DailyBackup_090716 will create a new folder in the Backups directory inside of home
rmdir Projects/WhatWasIThinking will remove the folder WhatWasIThinking from the current directories Projects folder. NOTE: this folder must be empty otherwise you will get an error (there are ways around this but it's safer until you are more comfortable with bash scripting to only remove empty folders, otherwise you risk destroying your system)
cat <filepath1> <filepath2> ...- concatenate all of the contents of all passed files and print them to the screenless <filepath>- interactive file browser, allows you to scroll through a file without opening it/dumping the full contents to screenhead <filepath>- grab the first 10 lines of a filetail <filepath>- grab the last 10 lines of a fileheadandtailcan be passed a-n #to print the first/last # lines
grep <pattern> <filepath>- search for pattern in file and print lines that contain the pattern
Two other important operators to know, redirect to file:
>- redirect data that would be printed to the terminal into a file, this function OVERWRITES the file if it exists- Example:
head -n 12 dataFile.dat > dataHeader.dat
- Example:
>>- redirect data but APPEND it to the file if it already exists
An example from molecular simulation: we have a data file that contains the positions of a large number of L3K peptides (L=LEU=Leucine, K=LYS=Lysine, column 1).
$ cat after_nvt.gro
Martini system from L3K.pdb
21923
1LEU BB 1 1.213 1.987 1.343 0.2643 0.0428 -0.1789
1LEU SC1 2 1.272 1.746 1.562 0.2976 0.1963 -0.0951
2LEU BB 3 1.499 2.231 1.279 -0.0785 -0.1063 -0.3216
2LEU SC1 4 1.479 2.473 1.052 -0.1779 0.2060 0.0329
3LEU BB 5 1.796 2.187 1.456 0.2418 0.1456 0.2937
3LEU SC1 6 1.757 1.860 1.596 -0.1554 -0.1429 -0.1564
4LYS BB 7 2.091 2.225 1.579 -0.0849 0.0039 0.1769
4LYS SC1 8 2.242 1.917 1.541 -0.1881 -0.1178 -0.2775
4LYS SC2 9 2.190 1.638 1.403 0.0037 0.0082 -0.1669
1LEU BB 10 1.034 2.095 5.004 0.2386 -0.2658 -0.1759
...
Luckily this file has a very static description. I'd like to know the coordinates of all of the LYS backbone groups (BB, column 2), with x/y/z coordinates stored between characters 21 and 44 (inclusive). I'd like to write these coordinates to a separate file, LYS_BB_Coords.dat. We can do this pretty easily by passing data between bash commands. To do this, we use a pipe |, which takes output from one command and passes it to another
$ cat after_nvt.gro | grep 'LYS' | grep 'BB' | cut -c21-44 >> LYS_BB_Coords.dat'
$ head LYS_BB_Coords.dat
2.091 2.225 1.579
1.917 1.997 5.165
2.164 1.502 8.733
1.412 2.512 12.128
1.944 4.958 1.097
2.091 5.480 4.924
1.366 5.163 9.381
1.273 5.310 12.140
1.916 9.112 1.614
2.121 8.803 4.956
Note: awk is a powerful, powerful tool, that I do NOT have time to go into sadly (this entire thing could've been a one-liner in awk). If you need to parse columns out of files, do yourself a favor and spend some time looking into it. (http://www.tutorialspoint.com/awk/)
In lieu of starting a war, I will only suggest that there are a wide variety of possible file editors that you might pick from:
- vi/vim
- emacs
- gedit
- nano
- pico
- atom
- textedit
- jed
- ...
Each has their own pros and cons, and each of these could be their own separate talk so I'll leave it there.
So far we've learned how to use BASH to move around and look at a file system, and extract information from files. The nice part about bash is that we can write small scripts, or sequences of bash commands stored in a file, and use those to do our work instead of having to use the same sets of commands over and over again. Running a script opens up a new BASH environment, and runs the scripted commands in that environment.
There are multiple different options for running a script. The easiest is to just pass your script file to the bash interpreter and let it run:
bash myScript.sh
Typically people use the file extension .sh for shell scripts to be clear, though this is not required. An alternative technique is to give the script executable (+x) permissions using the change file modes (chmod) command. Once the file has been given execute permissions, it can be called from the terminal without invoking the bash command directly.
chmod +x myScript.sh
./myScript.sh
In this case, it can be useful to have the following line at the beginning of your script file to ensure that the interpreter correctly parses your shell script for bash:
#!/bin/bash
$?- what was the return call of the last command executed in the shell$#- how many command line arguments were passed to the script$1,$2, ... - the values of the command line arguments, in their given order ($1is the first argument, etc)$0- the name of the currently run shell script$@- array containing all of the($1, $2, ...)variables
Let's say I would like to run some python analysis code on my data, and I'd like to make it easy on myself so that I can make changes quickly or be uniform with my variables across different pools of data. We can write a small bash script to run our code for us, handling things like taking in the data file from the command line arguments, setting up analysis parameters, passing those to a python function call, and pushing the data to output files. All with only a few lines of bash scripting!
#!/bin/bash
# set variables
datafile=$1 # passed argument parameter
script="analyzeSim.py" # string
columns=(4 5 6) # array
starttime=1000 # integers
difftime=100
# run script
python $script $datafile $starttime ${columns[*]] ${columns[0]} >> outStart.txt
#
endtime=$((starttime+difftime))
python $script $datafile $endtime ${columns[*]] ${columns[0]} >> outEnd.txt
There are a number of things here to note
varname=...sets the value of a variable, this works for strings, integers, and arrays. The lack of whitespace is important (won't work if there are spaces)- We call these variables with
$varname - For an array,
$varnamewill give the first element (index 0) in the array.- Single element access is given by
${varname[number]} - All elements are given by
${varname[*]}
- Single element access is given by
- We can do (integer) arithmetic with the
$(( ))construct on our set variables.- The standard
+ - * / %operators all work in this way - Bitwise operators also work:
& | ^ << >> ! ~
- The standard
BASH conditionals take a bit of getting used to if you're coming from most other languages. Here's the basic structure of an if statement,
if CONDITIONAL_STATEMENT
then
# perform true action
else
# perform false action
fi
Conditional operators are slightly different than in other languages, but luckily we can take advantage of the extended test command [[ ]] which will take our condition statement and parse it like pretty much any other language. There are a variety of other options here, including (( )) and [ ], but we'll stick with the extended test command for this tutorial.
NOTE: In BASH, return values of 0 are considered true, return values of non-zero are considered false. This is related to the idea of program return codes: a 0 indicates everything finished as expected, all other values indicate some form of error/extraneous result. This is very different from most other languages.
With that caveat in place, you can use most of your favorite conditional statements in BASH inside [[ ]]. Note the extra spacing below, it is necessary so that the bash shell can correctly parse the statement.
- Equality checking:
[[ $a == $b ]]or[[ $a != $b ]] - Greater than/less than:
[[ $a > $b ]]or[[ $a < $b ]] - Logical AND:
[[ $a > $b && $c > $b ]] - Logical OR:
[[ $a > $b || $c > $b ]] - Single command versions of greater than or equal/less or equal can be done via the
-geor-leoperators,[[ $a -ge $b ]]
Some extra fun ones for strings/file paths:
- Is string empty/null (zero length):
[[ -z $str ]] - Is string NOT empty:
[[ -n $str ]] - Does a file/directory exist:
[[ -e $path ]] - Is the string a valid directory:
[[ -d $path ]] - Is the string a valid file:
[[ -f $path ]]
The standard for loop in bash follows the same convention as in C, you define a loop counter that starts at some value, you have a test condition for when to end the loop, and you define some adjustment to your loop counter on each iteration. Here's a simple example to print numbers, skipping by 5, from START to END:
START=0
END= 100
for ((counter = $START; counter <= $END; counter+=5))
do
echo $counter
done
Similarly, while loops follow a very normal structure, and here's how you'd do the printing statement from above:
counter=$START
while [[ $counter -le $END ]]
do
echo $counter
counter=$((counter+5))
done
If you've got an array, you can also do a loop through each element, similar to a for each loop:
arr=`seq $START 5 $END` # defining an array
for i in $arr
do
echo $i
done
Sometimes you'll find that you end up writing the same set of operations in your script, and you'd like to condense your code for readability/make things simpler on your self. Well you can define functions in a bash file like any other language! A simple examples that prints out all input arguments to a function would be:
printArgs()
{
for i in $@
do
echo $i
done
}
Let's work through an example script that takes in a number argument from the command line and prints out the prime factorization of that number. This script will demonstrate the use of functions, loops, conditional statements, and recursion.
First, we need to know if a number N is prime. A simple means of testing this is to check if any number i, where i <= sqrt(N), divides evenly into N. For this, we'll use the modulo operator N % i (this operator returns the remainder from performing division N/i, if the result is 0 then N is perfectly divisible by i).
Let's write up a function for deciding if a number is prime based on this idea:
isPrime()
{
# local modifies variable scope so we don't overwrite elsewhere
local N=$1
local i
for ((i = 2; $((i*i)) <= $N; i++))
do
if [[ $((N%i)) == 0 ]]
then
return 1
fi
done
return 0
}
Next we'll hunt for the prime factors of our number. The idea is to iterate from a starting number until we find a prime number p. Once we find one, we should check to see if it's a factor of N:
- If
pis a prime factor ofN, we should outputpand then start the search process over again, now looking at the numberN/pand starting atp. - If
pis NOT a prime factor ofN, we should continue searching for prime factors ofNstarting atp+1
Let's try to do this then with recursion:
findPrimeFactor()
{
local N=$1
START=$2
if isPrime $N # takes in the return code of the function
then
printf "%d" $N
return
fi
local p
for ((p = $START; $((p*p)) <= $N; p++))
do
if isPrime $p
then
if [[ $((N%p)) == 0 ]]
then
printf "%dx" $p
N=$((N/p))
findPrimeFactor $N $p
return
fi
fi
done
}
And that's it! We now have a function that can take in a number and get out the prime factorization, let's generate out the rest of the script to handle user inputs. In particular, I'd like to handle the case where the user hands multiple numbers to the program to factorize:
for i in $@
do
printf "Prime Factors of %d: " $i
findPrimeFactor $i 2 # start with the smallest prime value
printf "\n"
done
And just for a little bit of fun:
$ time ./primeFactorize.sh 210745829136131
Prime Factors of 210745829136131:601x350658617531
real 0m7.830s
user 0m7.372s
sys 0m0.457s
There are more efficient techniques out there but our little bash script could factor a 14-digit number in ~8 seconds. Not too shabby!
