Week 3 Discussion - Bash Programming#
Intro#
What is your favorite text/source-code editor/s? Why?
Solution to
My favorite editor is Vim, because I love minimalism and simplicity. Compared to intuitive graphical editors like gedit, Visual Studio Code and PyCharm Vim takes rather a longer time to configure it for your personal use and use it efficiently.
I am using Vim for more than then years now. When I was studying in 2006, I used Visual Studio which turned out to be very useful for writing more complex programs because of its autocompletion features. While coding you do not have to remember the exact syntax for a command, or the name of the function which can remove an item from a list. This is a situation where your computer can help you. Autocompletion is nowadays a standard feature in integrated development environments (IDE), which are more focused on coding then editing text.
Math#
What is the problem with the following Bash expressions?
expr 24 * 7 * 52
expr 10+5/3
Solution to
In Bash, the
*
character is used for filename expansion, also called globbing.*
will be replaced with the files in the current directory, and the command will fail.Note that quoting the argument does not work either:
expr '24 * 7 * 52'
expr
requires a space between different components of the operation, in other words there should be a space between each number and operator.
Carry out the following operations using Bash:
24*7*52 # 8736
1+5*3 # 16
(1+5)*3 # 18
3.333*3 # 9.999
3/2 # 1.5
Solution to
expr 24 \* 7 \* 52
# we can also use arithmetic expansion:
echo $(( 24*7*52 ))
expr 1 + 5 \* 3
expr \( 1 + 5 \) \* 3
echo '3.3333*3' | bc
echo '3/2' | bc -l
What does the
-l
option exactly do inbc
?When is the option
-l
useful?
Solution to
According to the
bc
manual - section Math Library:… a math library is preloaded and the default scale is set to 20. …
and additional functions, e.g., sinus, cosinus, etc are defined
-l
should be used when integer results are not useful in your Bash script or when you need functions defined in thebc
math library.
Variables#
What is the problem with the following?
JOKE_LIMIT = 2
JOKE_TERM = student
curl -H "Accept: text/plain" "https://icanhazdadjoke.com/search?term=$JOKE_TERM&limit=$JOKE_LIMIT"
Solution to
There should be no spaces around the equal sign. Otherwise Bash interprets JOKE_LIMIT
as a command, =
and JOKE_TERM
as arguments.
Correct:
JOKE_LIMIT=2
JOKE_TERM=student
curl -H "Accept: text/plain" "https://icanhazdadjoke.com/search?term=$JOKE_TERM&limit=$JOKE_LIMIT"
What is the difference between these two examples?
FILE_ID=5
FILE_ID=$FILE_ID+1
FILE_ID=5
let FILE_ID=$FILE_ID+1
Solution to
The former will store the result as a string, but the latter one will evaluate (i.e., interpret) the expression.
FILE_ID=5
FILE_ID=$FILE_ID+1
echo $FILE_ID
FILE_ID=5
let FILE_ID=$FILE_ID+1
echo $FILE_ID
How can you store the output of a command in a variable? Complete the line beginning with
TEMP_CELSIUS
How is the syntax which you used called?
What is wrong with the Fahrenheit conversion?
# use wttr.in API to get the temperature in Celsius using the output of
# `curl wttr.in/\?format=%f | cut -c -3`
TEMP_CELSIUS=
echo Temperature in Celsius: $TEMP_CELSIUS
# Convert to Fahrenheit
let TEMP_FAHRENHEIT=$TEMP_CELSIUS\*1.8+32
echo Temperature in Fahrenheit: $TEMP_CELSIUS
Solution to
TEMP_CELSIUS=$(curl wttr.in/\?format=%f | cut -c -3)
echo Temperature in Celsius: $TEMP_CELSIUS
# Bash does not support calculation with floating numbers
# you will get `... syntax error: invalid arithmetic operator ...`
# let TEMP_FAHRENHEIT=$TEMP_CELSIUS*1.8+32
# add a zero before $TEMP_CELSIUS, otherwise bc could error out (syntax error
# if +10 instead of 0+10)
TEMP_FAHRENHEIT=$(echo 0$TEMP_CELSIUS*1.8+32 | bc)
# Replace $TEMP_CELSIUS with $TEMP_FAHRENHEIT
echo Temperature in Fahrenheit: $TEMP_FAHRENHEIT
This syntax is called command substitution which is one of the seven shell expansions in Bash. You can substitue a command also by using backquotes (`), which is old-style though.
Bash can do arithmetics using expr 5 + 4
. But Bash supports a shorter syntax to expand arithmetic expressions. How does this syntax look like and how it is called?
You may take a look at Shell Expansions (Bash Reference Manual)
Solution to
echo $((5+3))
What do the following variables refer to in a shell script?
You may take a peek at Shell Parameters (Bash Reference Manual)
$2
$15
${15}
$@
$#
$0
Solution to
$2 # second parameter passed to the script
$15 # first parameter and the string `5`
${15} # fifteenth parameter
$@ # all positional parameters
$# # the number of parameters
$0 # the name of the script
More info on $15
vs ${15}
: Positional Parameters (Bash Reference Manual)
DAYS_UNTIL_BDAY=5
echo "You have $DAYS_UNTIL_BDAY days until your birthday, haven't you?"
echo Today\'s date is $(date -I).
In the former script the strings preceded by $
or enclosed by $(...)
are replaced with another string. What is this concept called in Bash?
Solution to
This concept is called in general Shell expansion. The first example is a parameter expansion, the second command substitution.
To expand something means to interpret a string and replace it with another string. Writing a dollar-sign ($
) is similar to marking a word on a text which should be reviewed and replaced.
User Input#
What is the problem with the following script?
echo "Which city's weather forecast do you need?"
read $CITY
echo "Ok! Showing the weather forecast for $CITY":
curl wttr.in/$CITY
Solution to
read
should be used with the name of the variable. $CITY
expands the variable CITY
, so the script will become:
...
read
...
Correct way to use read
is:
...
read CITY
...
Logic and If/Else#
What is the output of the following lines?
false
echo $?
false && true
echo $?
false && true && true
echo $?
true || false
echo $?
Solution to
1
1
1
0
What is the output?
true || echo Hello!
Solution to
The output will be empty, because echo Hello!
will not be run. A conditional expression chained with OR will always be true if one of the components is true. The first component is in our example true
, so Bash won’t bother looking further and thus won’t execute echo Hello!
.
In other words (last paragraph in Conditional Constructs (Bash Reference Manual)):
The && and || operators do not evaluate expression2 if the value of expression1 is sufficient to determine the return value of the entire conditional expression.
Note that true
itself is also a shell command.
You are writing code which checks if the right amount of arguments are supplied to your script. What would be the output if
$#
(number of arguments) is3
?Why?
[[ $# -eq 3 ]] || echo 😒 || exit 1 && echo arguments: $1 $2 $3 || echo 🤷
Solution to
arguments: ...
Lists (Bash Reference Manual) states:
… Of these list operators, ‘&&’ and ‘||’ have equal precedence …
and
AND and OR lists are sequences of one or more pipelines separated by the control operators ‘&&’ and ‘||’, respectively. AND and OR lists are executed with left associativity.
Associativity in programming languages defines which operators will be executed first when they have the same precedence and when parantheses are not used. For example,
+
and-
have the same precedence, but*
and/
have a higher precedence.Left associativity means that we enclose the operations from the left. Parantheses are introduced in incremental steps as follows:
a || b || c && d || e
(a || b) || c && d || e
( (a || b) || c) && d || e
( ( (a || b) || c) && d) || e
Now the line is executed like this:
a || b
:a
is true, so the whole expression will be true regardless ofb
. So there is no need to executeb
.true || c
: same as previous.true && d
: This expression will be false ifd
is false, but true ifd
is true. So Bash will executed
to get its output. In our cased
is theecho
command which always returns true, so the whole expression is true.true || e
: same as 1.
Can
echo 🤷
be ever executed? No. This expression is only there for teaching purposes.You may have had the intuition (like me) that nothing will be executed because Bash won’t bother after the first OR (
||
) operator because the first expression is true. This is not the case due to the left-associativity rules.
x
,y
andz
are variables.x or y and z
is called a … .or
,and
are … .In Bash syntax
or
andand
are written as … and …, respectively.If the last command was successful, its … will be
0
.If the last command was unsuccessful, its … will be … .
Solution to
logical expression
logical operators
||
and&&
.exit status
non-zero
It is counter-intuitive that the exit-code 0
resembles success, and 1
(and other non-zero values) resemble an error. An excerpt from Exit Status (Bash Reference Manual)
… This seemingly counter-intuitive scheme is used so there is one well-defined way to indicate success and a variety of ways to indicate various failure modes. …
Note that a logical expression is not replaced with its exit code, e.g., 0
, but is executed and their exit codes are not printed. So [[ 1 -ne 2 ]] && true && ls
will execute all of the commands one after another starting from the left, will only print directory contents, and exit with 0.
Also note that the arithmetic expressions ($((EXPRESSION))
) also support conditional expressions, and their output is 1
if true and 0
otherwise. For example, echo $((42 % 2 == 0))
will output 1
.
You want to impress your friend with your fresh shell hacking skills by writing a lyrics retriever script. Your script expects exactly two arguments, artist
and title
. Your script should exit with an error message if these arguments are missing. The error message could be “Usage: ./script.sh ARTIST TITLE” Complete the following script:
## YOUR CODE HERE
ARTIST=$1
TITLE=$2
# encode space chars and
# get the lyrics using lyrics.ovh API
curl -L api.lyrics.ovh/v1/${ARTIST// /%20}/${TITLE// /%20}
Solution to
[[ $# -ne 2 ]] && echo Usage: $0 ARTIST TITLE && exit 1
ARTIST=$1
TITLE=$2
# encode space chars and
# get the lyrics using lyrics.ovh API
curl -L api.lyrics.ovh/v1/${ARTIST// /%20}/${TITLE// /%20}
[[…]]
is one of the conditional constructs that Bash features:
Return a status of 0 or 1 depending on the evaluation of the conditional expression …
Give two command examples which use an unary expression:
Solution to
[[ -s prog.conf ]] || echo Configuration file does not exist or is empty.
[[ -z $(ls) ]] && echo Working directory is empty.
For a comprehensive list: Bash Conditional Expressions
The following script shortens URLs using the API of the is.gd URL shortening service.
It accepts a single parameter, the URL which should be shortened. Implement two checks for the parameter:
Exactly one single parameter is allowed.
The parameter should be a valid URL. Implement a simple check which tests if the URL starts with a valid domain (domain.suffix), e.g.,
aydos.de
.
### YOUR CODE HERE
URL=$1
### YOUR CODE HERE
curl "https://is.gd/create.php?format=simple&url=$URL"
Solution to
[[…]]
is very handy for validating a parameter or a variable using a single line of code:
[[ $# -ne 1 ]] && echo Usage: $0 URL && exit 1
URL=$1
[[ ! $URL =~ .+\..+ ]] && echo The provided parameter is not a valid URL && exit 1
curl "https://is.gd/create.php?format=simple&url=$URL"
Rewrite the following script using if/else.
[[ $# -ne 1 ]] && echo Usage: $0 URL && exit 1
URL=$1
[[ ! $URL =~ .+\..+ ]] && echo The provided parameter is not a valid URL && exit 1
curl "https://is.gd/create.php?format=simple&url=$URL"
Solution to
if [[ $# -ne 1 ]]; then
echo Usage: $0 URL
exit 1
elif [[ ! $URL =~ .+\..+ ]]; then
echo The provided parameter is not a valid URL
exit 1
else
curl "https://is.gd/create.php?format=simple&url=$URL"
fi
More info on Conditional Constructs (Bash Reference Manual)
Arrays#
What is wrong with the following code?
DIRS_TO_BACKUP=home etc var
FIRST_PRIORITY=${DIRS_TO_BACKUP[0]}
LAST_PRIORITY=${DIRS_TO_BACKUP[-1]}
echo First directory to backup: $FIRST_PRIORITY
echo Last directory to backup: $LAST_PRIORITY
echo There are ${#DIRS_TO_BACKUP} directories to backup.
Solution to
Arrays must be enclosed using parantheses
To get the number of elements in the array we need to append the variable name with
[*]
.
DIRS_TO_BACKUP=(home etc var)
FIRST_PRIORITY=${DIRS_TO_BACKUP[0]}
LAST_PRIORITY=${DIRS_TO_BACKUP[-1]}
echo First directory to backup: $FIRST_PRIORITY
echo Last directory to backup: $LAST_PRIORITY
echo There are ${#DIRS_TO_BACKUP[*]} directories to backup.
You have some files with .intro
, .body
and .conclusion
extensions. These contain introduction, body and conclusion paragraphs.
The following block creates some example files.
tee $(mktemp XXX.intro) <<< intro1
tee $(mktemp XXXX.intro) <<< intro2
tee $(mktemp XXXXX.intro) <<< intro3
tee $(mktemp XXX.body) <<< body1
tee $(mktemp XXXX.body) <<< body2
tee $(mktemp XXX.conclusion) <<< conclusion1
tee $(mktemp XXXX.conclusion) <<< conclusion2
tee $(mktemp XXXXX.conclusion) <<< conclusion3
tee $(mktemp XXXXXX.conclusion) <<< conclusion4
Write a program which generates all possible stories consisting of these paragraphs, where every story follow the introduction-body-conclusion structure. The filenames of the stories have the following format: NAME1-NAME2-NAME3.txt
. Do not use the first intro, body and conclusion that you encounter (the first item in your array).
You can get rid of the file extension by using parameter expansion as follows:
file=filename.extension
echo ${file%.*} # outputs: filename
Solution to
By enumerating every possible combination:
intros=(*.intro)
bodies=(*.body)
conclusions=(*.conclusion)
cat ${intros[1]} ${bodies[1]} ${conclusions[1]} > ${intros[1]%.*}-${bodies[1]%.*}-${conclusions[1]%.*}.txt
cat ${intros[1]} ${bodies[1]} ${conclusions[2]} > ${intros[1]%.*}-${bodies[1]%.*}-${conclusions[2]%.*}.txt
cat ${intros[1]} ${bodies[1]} ${conclusions[3]} > ${intros[1]%.*}-${bodies[1]%.*}-${conclusions[3]%.*}.txt
cat ${intros[2]} ${bodies[1]} ${conclusions[1]} > ${intros[2]%.*}-${bodies[1]%.*}-${conclusions[1]%.*}.txt
cat ${intros[2]} ${bodies[1]} ${conclusions[2]} > ${intros[2]%.*}-${bodies[1]%.*}-${conclusions[2]%.*}.txt
cat ${intros[2]} ${bodies[1]} ${conclusions[3]} > ${intros[2]%.*}-${bodies[1]%.*}-${conclusions[3]%.*}.txt
Solution to
Using loops and slicing array items using substring expansion (${parameter:offset}
) which is one of the parameter expansions. :
intros=(*.intro)
bodies=(*.body)
conclusions=(*.conclusion)
for intro in ${intros[*]:1}; do
for body in ${bodies[*]:1}; do
for conclusion in ${conclusions[*]:1}; do
cat $intro $body $conclusion > ${intro%.*}-${body%.*}-${conclusion%.*}.txt
done
done
done
Array variables vs variables that contain a sequence of strings#
Note that the following does not create an array but a normal variable which consists of a sequence of filenames:
mktemp XXX.intro
mktemp XXXX.intro
intros=*intro
echo ${intros[1]} # outputs nothing
echo ${intros[0]} # outputs all .intro files
In this example we see that even we can use indexing (like intros[1]
) on normal variables, but they will output nothing. This behavior is partly documented in Arrays (Bash Reference Manual):
… Any reference to a variable using a valid subscript is legal, …
All the items are stored in the index 0, because everything is stored as a single item but not partitioned as in an array.
The manual additionally states:
… and
bash
will create an array if necessary.
Example:
var='1 2 3'
declare -p var # outputs: declare -- var="1 2 3"
echo ${var[1]} # outputs nothing
declare -p var # outputs: declare -- var="1 2 3"
var[1]=new
echo ${var[1]} # outputs: new
declare -p var # outputs: declare -a var=([0]="1 2 3" [1]="new")
But an array is not converted to a normal variable if we access it as a normal variable. As documented in Arrays (Bash Reference Manual):
Referencing an array variable without a subscript is equivalent to referencing with a subscript of 0.
var[0]=1
var[1]=2
declare -p var # outputs: declare -a var=([0]="1" [1]="2")
echo $var # outputs: 1 instead of 1 2
In the previous example we converted a normal variable to an array by accessing the variable using an index, but the existing items were assigned to the index 0. If every space separated string must be assigned to a separate index, the following conversion approach is better:
unset var
# make sure that var is not defined, otherwise we may be overwriting an
# existing array variable
var='1 2 3'
declare -p var # outputs: declare -- var="1 2 3"
echo ${var[1]} # outputs nothing
var=($var)
declare -p var # outputs: declare -a var=([0]="1" [1]="2" [2]="3")
echo ${var[1]} # outputs the second item
Braces#
Use brace expansion to generate following commands/strings:
0 1 2 3 4 5
22 23 24 32 33 34 42 43 44
141 142 14x 14y 151 152 15x 15y 161 162 16x 16y
AGGGA AGGGG AGGGC AGGGT GGGGA GGGGG GGGGC GGGGT CGGGA CGGGG CGGGC CGGGT TGGGA TGGGG TGGGC TGGGT
echo cp IMG_1934.jpg IMG_1934-backup.jpg
Solution to
echo {0..5}
echo {2..4}{2..4}
echo 1{4..6}{{1,2},{x,y}}
echo {A,G,C,T}GGG{A,G,C,T}
echo cp IMG_1934{,-backup}.jpg
If we want to use variables to generate a sequence — for example in a script — we need to use the eval
command:
a=0
b=5
echo {$a..$b}
eval echo {$a..$b}
This is strange, isn’t it? What could be the reason? Take a look into Brace Expansion (Bash Reference Manual) and find the reason.
Solution to
The manual states:
Brace expansion is performed before any other expansions, and any characters special to other expansions are preserved in the result. …
This means that Bash would expand brace expansions and then process with parameter expansions like $a
{.bash} and $b
{.bash}. Before $a
{.bash} and $b
{.bash} get expanded there is no brace expression to expand, because {$a..$b}
{.bash} is not a valid brace expression (but {0..5}
{.bash} is). Then Bash proceeds with parameter expansion and expands {$a..$b}
{.bash} to {0..5}
{.bash}. After that echo {0..5}
{.bash} is executed, so the third line will output {0..5}
{.bash}.
Similarly the fourth line becomes eval echo {0..5}
{.bash}, and then the eval
{.bash} command executes the command echo {0..5}
{.bash}. This time {0..5}
{.bash} is a valid brace expression and it is expanded to 0 1 2 3 4 5
. Finally echo 0 1 2 3 4 5
{.bash} is executed which leads to the intended output.
Write a script which accepts
FILE_COUNT
as a single parameter and generatesFILE_COUNT
empty files with the namesFILE_0 ... FILE_{FILE_COUNT-1}
, e.g.,./file_gen.sh 2
should generateFILE_0
andFILE_1
.(Optional) Pay attention that
./file_gen.sh 11
generatesFILE_00
toFILE_10
, and notFILE_0
toFILE_10
.Hint: There is a special syntax for generating terms with the same number of digits. Brace Expansion (Bash Reference Manual) may help you.
Solution to
[[ $# -ne 1 ]] && echo Usage: $0 FILE_COUNT && exit 1 FILE_COUNT=$1 eval touch FILE_{0..$(( $FILE_COUNT-1 ))}
Brace Expansion (Bash Reference Manual):
… When either x or y begins with a zero, the shell attempts to force all generated terms to contain the same number of digits, zero-padding where necessary.
FILE_{00..$(( ...
solves our problem.
Loops#
Write a script which accepts FILE_COUNT
as a single parameter and generates FILE_COUNT
files with the names FILE0 ... FILE_{FILE_COUNT-1}
and their id as their content, e.g., ./file_gen_using_loops.sh 2
should generate FILE_0
and FILE_1
with the contents 0
and 1
, respectively.
Solution to
[[ $# -ne 1 ]] && echo Usage: $0 FILE_COUNT && exit 1
FILE_COUNT=$1
for i in $(eval {0..$(( $FILE_COUNT-1 ))} ); do
echo $i > FILE_$i
done
Write a command which loops over all the jpg files in the working directory and prepends today’s date to their filenames.
Solution to
When you are writing a for loop it is a good practice to prepend the commands which should be repeated with echo
so that these commands are not executed but only printed, in other words disarmed:
for f in $(ls *.jpg); do
echo mv {,$(date -I)}$f
done
Another advice: Looping over the files in the current directory is used very often, so may want to shorten it in a single line for convenience.
for f in $(ls *.jpg); do echo mv {,$(date -I)}$f; done
After you checked that your loop generates what you intend, then you can arm the loop again by removing the echo
:
for f in $(ls *.jpg); do mv {,$(date -I)}$f; done
Write a command which generates new files with random names in working directory unless the number of files in the working directory has reached 100. Use a while
loop.
You can use the Bash variables RANDOM
or SRANDOM
to generate random numbers.
Solution to
while [[ $(ls | wc -l) -lt 100 ]]; do touch $RANDOM; done
You have a file which contains lines in the following format: name score email
, for example:
database.csv
Artayi 34 artayi@posteo.de
Kettar 87 kettar@jfa.cn
Akaca 34 email@aka.eu
Write a script which reads a file like this and generates for every line a file with the filename name.txt
and the score as the content. For example ./your_script database.csv
should generate the following files:
Artayi.txt
Kettar.txt
Akaca.txt
Their content should correspond to their individual scores.
Hint: use while
and read
. The example in read(1p) manual could help.
Solution to
read
reads from the standard input
[[ $# -ne 1 ]] && echo Usage: $0 DATABASE.csv && exit 1
DB=$1
while read name score mail
do
echo $score > $name.txt
done < $DB
Write a command which removes every file which only contains the string NA
.
Solution to
for f in $(ls)
do
if [[ $(< $f) = NA ]]
then
echo removing $f
rm $f
fi
done
$(< $f)
is the same as $(cat $f)
.
Functions#
What is the role of the keyword local
in functions?
Solution to
If we assign a variable var
in a script (especially in a function), then this change is global for all the var
s in our script. This behavior may be undesirable when we use var
in the script. We should mark the local variables in a function with local
to avoid any side effects.
You have the following file which contains a function:
function countext {
local extensions=$@
local sum=0
for extension in $extensions
do
let sum=sum+$(ls *.$extension | wc -l)
done
echo $sum
}
This function counts the number files with given extensions, e.g., if your working directory contains a.jpg b.jpg c.txt k.odt
, then countext jpg txt
would output 3
.
Before using this function in your shell you have to make it available in your current shell. How would you do that? Try with bash count_extensions.sh
, and then using source
. What is the difference? What is the purpose of the source
command in general?
Solution to
source FILE
or in short . FILE
reads the file as if we are executing the lines in the given file in our current shell. bash FILE
will create a new bash shell (in other words: a new environment for variables and execution) and run the command in this separate shell. After it has been executed, the shell environment will be deleted and the names in this shell won’t be available in our current shell.
In a project you have downloaded numerous files in your directory, but the filenames do not have any file extensions, e.g., database
instead of database.csv
and you want to fix that. If the file contains at least ten lines then you decide to add the .csv
extension to the filename, .txt
otherwise. Additionally you want to log every rename operation in a log file.
What would be the rough structure of your program? Try to use functions.
What would be the advantage and disadvantage of using functions?
Implement your program
Hint: if your script is in the same directory as the working directory, pay attention that you do not rename your script.
You can use the following script to create dummy files:
# Creates ten random CSV files in current working directory.
function random_true_or_false {
if (( $RANDOM %2 == 0 ))
then
true
else
false
fi
}
function generate_file {
local number_of_lines=$1
local filename=$(mktemp XXX)
for i in $(seq $number_of_lines)
do
echo $RANDOM, $RANDOM, $RANDOM >> db-$filename.csv
done
echo generated the file db-$filename
}
for i in $(seq 10)
do
if random_true_or_false
then
generate_file $(( $RANDOM + 10 ))
else
generate_file $(( $RANDOM % 10 ))
fi
done
Solution to
Our program should be able to:
determine if a file has more than ten lines or not
rename a file by appending
.txt
or.csv
to the filenamelog the rename operation
for every file in the working directory.
We could implement all these functionality using a sequence of commands, which would in our case involve less code (disadvantage of using functions). If we use functions, our code would be more readable, because a programmer who wants to understand your code using the function names. For example capitalize_first_letter_in_each_sentence
would be more readable than sed -i -e 's/^\s*./\U&\E/g' -e 's/[\.!?]\s*./\U&\E/g'
. Another advantage is the modularity. When you define the function capitalize_first_letter_in_each_sentence
, then you can also use it on other files.
function file_has_ten_or_more_lines {
local file=$1
if [[ $(cat $file | wc -l) -ge 10 ]]
then
true
else
false
fi
}
function apply_extension {
local filename=$1
local extension=$2 # without `.`, e.g., `csv`
mv $1{,.$2}
}
function log {
# appends messages to $0.log
local msg=$@
echo $msg >> $0.log
}
# ignore all files which already have an extension
for f in $(ls --ignore='*.*')
do
if file_has_ten_or_more_lines $f
then
log $f has $(cat $f | wc -l) lines => renaming to $f.csv
apply_extension $f csv
else
log $f has $(cat $f | wc -l) lines => renaming to $f.txt
apply_extension $f txt
fi
done
What is the difference between return
and echo
in functions?
Solution to
return
only affects a functions exit status, echo
is generally used to output the results of a function. Still, using a function which returns true
or false
is useful in a conditional expression, e.g.:
function got_heads_after_flipping_the_coin {
if (( $RANDOM %2 == 0 ))
then
true
else
false
fi
}
if got_heads_after_flipping_the_coin
then
echo heads
else
echo tails
fi
Version with echo
:
function got_heads_after_flipping_the_coin {
if (( $RANDOM %2 == 0 ))
then
echo true
else
echo false
fi
}
if [[ $(got_heads_after_flipping_the_coin) == true ]]
then
echo heads
else
echo tails
fi
We see that if got_heads_after_flipping_the_coin
is more readable than the version with echo
.
Writing Programs#
What makes the Unix shell highly composable?
Solution to
Many Unix shell commands can be easily chained together using piping (|
), as long as the commands can read and write to standard input. This gives the user an efficient interface. Without piping the user would have had to write the output to a file and read it in the next program.
How can we make a file executable, and what is the role of the shebang in this context?
Solution to
chmod +x FILE
. If we then execute the file using ./FILE
, then the shell looks at the first two bytes of a file. These numbers indicate:
file is a binary program (no script like a Bash script). Then this file is executed.
file begins with
#!
. Then this file is executed using the program given after#!
. For example if we have#!/usr/bin/python
then the script will be run by the Python interpreter.else: Bash will execute the lines as shell commands
More info on #!
and the first two bytes of a file called magic numbers: Unix FAQ - Why do some scripts start with #!
How can we run a Bash script without making it executable?
Solution to
bash FILE
Give three examples of environment variables defined on your current shell environment. What do these three variables stand for and why could they be defined at all?
Solution to
We can use env
to list the currently defined environment variables.
Examples:
OLD_PWD
stores the previous directory we have been in. Usingcd -
we can change to our old working directory._
contains the last command that we used. We can reference this command using$_
LANG
contains the language that we are using. Some programs read this variable to set the interface language.
What is the role of the environment variable
PATH
?
Solution to
When we execute a command
in our shell, then Bash guesses which program we really mean with our command
. If the command
is not a builtin command like true
or for
, then Bash searches for matching executable files in the directories defined by PATH
. We can use which command
to see which path is actually used to execute command
.
If we have a script that we frequently use, then we can add the directory where our script resides in to the PATH
. Then we do not have to type the long path to run our script.
Summary and reflection#
Did you reach the following learning objectives for this week?
Create a program using Bash
Use Bash to perform basic arithmetic
Store data in variables using Bash
Use Bash to collect user input
Create arrays in Bash
Use brace expansion to create strings out of sequences
Use loops to repeat lines of codes based on logical conditions or sequence