more shell with 'if', script arguments, arithmetic
script arguments
see shell scripts from the software carpentry
command-line arguments stored in variables $1, $2, etc.
$0: name of script
$#: number of arguments
example: script to show both the beginning and
the end of a file.
argument: file name
create a new file headtail.sh, such as with touch headtail.sh,
then edit your file (e.g. with VS Code) so that it contains this:
echo "script name: $0"
echo "first argument: $1"
echo "number of arguments: $#"
(head -n 2; tail -n 2) < "$1"
by the way: < redirect the standard input, and
subshell between ( ): both head and tail get the same standard input.
execute the script like this (for now):
bash headtail.sh Mus_musculus.GRCm38.75_chr1.bed
bash headtail.sh Mus_musculus.GRCm38.75_chr1.bed | column -t
- even if you use
zshby default, your system hasbash, most likely, and yourzshknows where to findbash: so thebashcommand above should work. - now might be a good time to comment out the first 3 lines
(or delete them from your script): these
echolines were for the sake of learning and demystifying the default variables like$0and$1.
safe script options, file permissions
I recommend you start your script with this:
#!/bin/bash
set -e # script terminates if any command exits with non-zero status
set -u # terminates if any variable is unset
set -o pipefail # terminates if command within a pipes exits unsuccessfully
#! “shebang”: tells how to run the script.
Would be #!/usr/bin/perl for a perl script.
do which bash or which perl to know what to put on this line:
path to bash or to perl.
Here you could replace /bin/bash by /bin/zsh if you want your script
to be run by zsh instead of bash. I recommend that you avoid commands that
work with one but don’t work with the other: it would hamper reproducibility
and could bother collaborators
(or yourself if you decide to switch from one shell to another in a few years).
With the first line and with the execute permission, we can run the script
with ./myscript.sh filename instead of bash myscript.sh filename.
To change permission:
ls -l
chmod u+x headtail.sh
ls -l
./headtail.sh Mus_musculus.GRCm38.75_chr1.bed
./headtail.sh Mus_musculus.GRCm38.75_chr1.bed | column -t
u, g, o: user, group, other; a for all
+ or - to add or remove permissions
r, w, x: read, write, execute
~/bin directory:
- create one if you don’t already have one, put your own programs there, to call them from anywhere.
- shell = programming language: it has variables, and the value of a
variable is accessed with
$in front of the variable name. PATHis a variable: tryecho $PATH.PATHlists a bunch of directories. When you type of command name, such aslsorecho, the shell looks for an executable file with that name in all the directories inPATH. If executes the first one that it finds.- Do you see
~/binsomewhere when you doecho $PATH? - if yes: any executable file inside
~/binwill be found when you type its name, and can be executed from wherever you are. - if not: add it!
- with the
bashshell: open your file~/.bash_profileto edit it, and add the lineexport PATH="$PATH:~/bin". If you don’t even have a file~/.bash_profile, create it (usetouch) then edit it to add the line. - with the
zshshell: open your file~/.zshrc(create it if it doesn’t exist) and add theexport path+=('~/bin'). - run
source ~/.bash_profileorsource ~/.zshrcdepending on your shell, or simply exit your terminal and re-open it.
- with the
With that, I can move my script headtail.sh into ~/bin, and run
it from anywhere I would like, as headtail.sh filename:
mv headtail.sh ~/bin/
headtail.sh Mus_musculus.GRCm38.75_chr1.bed
headtail.sh Mus_musculus.GRCm38.75_chr1.bed | column -t
arithmetic expansion
use (( )). integers only.
It’s good to know about arithmetic expansion,
but I don’t encourage using it.
If you need anything elaborate, it means that you should use a
Julia, Python, Perl or R script, not a shell script.
Try this on your own, to see the output:
i=3678 # no spaces!!
echo "my variables is: i=$i"
((i = i+6))
echo "I incremented i by 6: now i=$i"
((i--))
echo "I decremented i by 1: now i=$i"
((i++)); echo "I incremented i by 1: now i=$i"
((i+=1)); echo "I incremented i by 1 again: now i=$i" # 3685
((i/=5)); echo "finally, I divided i by 5: now i=$i"
echo $((i++)) # i++ executes the command "echo" and increments i after
echo $i # we see 738, not 737 like earlier
echo $((++i)) # ++i increments i first, then executes the command
echo $i # we see 739, like earlier
if statements and checks
examples:
if [ $i -lt 800 ] # the spaces after `[` and before `]` are REQUIRED
then
echo "i is less than 800"
else
echo "i is not less than 800"
fi
if [ $i -lt 800 -a $i -ge 790 ] # -a = "and"
then
echo "790 <= i < 800"
else
echo "i<790 or i>=800"
fi
headtail script: let’s test and check for at least one argument (file name), and if so, test that this file is readable:
if [ $# -lt 1 ] || [ ! -f $1 ] || [ ! -r $1 ]
then
echo "error: no argument, or argument is not a file, or file not readable"
exit 1 # exit script with error code (1). 0 = successful exit
fi
exit code: 0 if successful, 1 if unsuccessful (for the shell, 0=true, 1=false!!)
| test expressions | |
|---|---|
-z str |
string str is empty |
str1 = str2 |
strings str1 and str2 are identical. different: str1 != str2 |
int1 -eq int2 |
integers int1 and int2 are equal. not equal: int1 -ne int2 |
int1 -lt int2 |
integer int1 is less than int2. greater: int1 -gt int2 |
int1 -le int2 |
integer int1 is less than or equal to int2. greater or equal: int1 -ge int2 |
-d thing |
thing is a directory. file: -f, link: -h |
-e thing |
thing exists |
-r file |
file is readable. writable: -w, executable: -x |
! |
negation |
-o, -a, ! |
or, and: to separate expressions within a test [...] (not short-circuit) |
( ) |
to group tests |
||, && |
or, and: to separate different tests (short circuit) |
short-circuit evaluation with || and &&: convenient, and the order is important!
- “A or B”: B is not evaluated if A is true, because
the result would be true anyway.
We can do “zero arguments or argument 1 is a file” without causing an error, but there could be an error if we did “argument 1 is a file or zero arguments”. - “A and B”: B is not evaluated if A is false: the result would be false anyway.
Let’s add a second, optional argument to our headtail.sh script:
number of lines to show at each end. default: 2
nl=2 # number of lines to show, on each end
if [ $# -ge 2 ]
then
nl=$2
fi
(head -n $nl; tail -n $nl) < "$1"
and now use our script with (or without) its new option:
headtail.sh Mus_musculus.GRCm38.75_chr1.bed 5 | column -t
headtail.sh Mus_musculus.GRCm38.75_chr1.bed | column -t
changing your shell prompt
variable PS1 contains your shell prompt (prompt string):
echo $PS1 # save this output, to go back to original prompt in same session
PS1="hiCecile% "
PS1="hiCecile$ "
PS1="$ "
parse_git_branch() { # defines a shell function: run "git branch" and extract branch name
git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
PS1="\$(parse_git_branch)$ "
PS1="\[\033[33m\]\$(parse_git_branch)$ "
PS1="\[\033[33m\]\$(parse_git_branch)\[\033[00m\]$ "
last one: shows if in git repository, and if so,
name of current checked out branch
to affect future sessions: pick the one you like best and add this at
the end of your ~/.bash_profile file:
export PS1=preferred_choice_here