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
zsh
by default, your system hasbash
, most likely, and yourzsh
knows where to findbash
: so thebash
command above should work. - now might be a good time to comment out the first 3 lines
(or delete them from your script): these
echo
lines were for the sake of learning and demystifying the default variables like$0
and$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. PATH
is a variable: tryecho $PATH
.PATH
lists a bunch of directories. When you type of command name, such asls
orecho
, 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
~/bin
somewhere when you doecho $PATH
? - if yes: any executable file inside
~/bin
will be found when you type its name, and can be executed from wherever you are. - if not: add it!
- with the
bash
shell: open your file~/.bash_profile
to 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
zsh
shell: open your file~/.zshrc
(create it if it doesn’t exist) and add theexport path+=('~/bin')
. - run
source ~/.bash_profile
orsource ~/.zshrc
depending 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