Difference between revisions of "CSC231 Bash Tutorial 8"
(→Bash Functions) |
(→References) |
||
(43 intermediate revisions by the same user not shown) | |||
Line 1: | Line 1: | ||
--[[User:Thiebaut|D. Thiebaut]] ([[User talk:Thiebaut|talk]]) 13:13, 1 November 2017 (EDT) | --[[User:Thiebaut|D. Thiebaut]] ([[User talk:Thiebaut|talk]]) 13:13, 1 November 2017 (EDT) | ||
---- | ---- | ||
+ | <bluebox> | ||
+ | Today's lab is about bash functions. Bash support functions, and they work similarly to functions in Python or Java, but with a twist. As is usual with bash, you will find that bash supports different syntaxes for writing functions, and restricted ways of passing parameters or returning values. | ||
+ | <br /> | ||
+ | The script you have to write for the last challenge should be submitted to Moodle. | ||
+ | <br /> | ||
+ | The solutions to the other challenges will be available at the end of the page around 11:40 a.m. on Friday. | ||
+ | <br /> | ||
+ | </bluebox> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | {| | ||
+ | | | ||
+ | __TOC__ | ||
+ | | | ||
+ | | ||
+ | | ||
+ | | ||
+ | | | ||
+ | <videoflash>brrAmOYO9Ks</videoflash> | ||
+ | |} | ||
+ | <br /> | ||
+ | <br /> | ||
=Bash Functions= | =Bash Functions= | ||
+ | <br /> | ||
+ | ==References == | ||
+ | <br /> | ||
+ | * [https://ryanstutorials.net/bash-scripting-tutorial/bash-functions.php Ryans Tutorials] are very easy to read tutorials on all aspects of the bash shell, including today's subject: '''bash functions.''' | ||
+ | * Also, this fairly extensive [https://likegeeks.com/bash-functions/ article] by Mokhtar Ebrahim on Bash functions. | ||
+ | <br /> | ||
+ | |||
+ | ==Introduction== | ||
<br /> | <br /> | ||
There are two ways of declaring functions in bash, illustrated in the code below: | There are two ways of declaring functions in bash, illustrated in the code below: | ||
Line 33: | Line 63: | ||
<br /> | <br /> | ||
* Inside a function, $1 will refer to the first parameter passed to the function, $2 will refer to the second argument, etc. | * Inside a function, $1 will refer to the first parameter passed to the function, $2 will refer to the second argument, etc. | ||
+ | * You '''do not''' put the parameters inside the parenthesis, when declaring the function. | ||
* Here is an example, with both style functions: | * Here is an example, with both style functions: | ||
<br /> | <br /> | ||
Line 48: | Line 79: | ||
} | } | ||
− | + | printAge() { | |
echo "Your age: $1" | echo "Your age: $1" | ||
} | } | ||
Line 59: | Line 90: | ||
</source> | </source> | ||
<br /> | <br /> | ||
+ | <br /> | ||
+ | <!-- ----------------------------------------------------------------------------------------------- --> | ||
+ | {| style="width:100%; background:silver" | ||
+ | |- | ||
+ | | | ||
+ | |||
+ | ==Challenge 1== | ||
+ | |} | ||
+ | [[Image:QuestionMark1.jpg|right|120px]] | ||
+ | <br /> | ||
+ | * Add a new function to '''func2.sh''' called ''printInfo()''. The new function takes 2 parameters and calls ''printName'' and ''printAge'' to print both. Here is an example of how to call it (that will be the only function call in the main part of the script): | ||
+ | |||
+ | printInfo "Kathleen" 61 | ||
+ | |||
+ | :and the output will be the same as the previous version of '''func2.sh''': | ||
+ | |||
+ | ------------------------ | ||
+ | Hello Kathleen | ||
+ | Your age: 61 | ||
+ | ------------------------ | ||
+ | |||
+ | |||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <!-- ----------------------------------------------------------------------------------------------- --> | ||
+ | {| style="width:100%; background:silver" | ||
+ | |- | ||
+ | | | ||
+ | |||
+ | ==Challenge 2== | ||
+ | |} | ||
+ | [[Image:QuestionMark2.jpg|right|120px]] | ||
+ | <br /> | ||
+ | * Below is an incomplete bash script that implements the teller machine script we saw earlier. It prompts the user for an integer, and takes the number as a dollar amount that is broken into a number of $20-bills, $10-bills, $5-bills and $1-bills. You need to replace the '''XXXXXX''' symbols by the appropriate expression(s)... | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcTeller.sh | ||
+ | # D. Thiebaut | ||
+ | # Gets a number from the user and breaks it down | ||
+ | # into a number of $20, $10, $5, and $1 | ||
+ | |||
+ | if [ "$#" -ne 1 ] ; then | ||
+ | echo "Syntax $0 nnnn" | ||
+ | echo "where nnnn is a positive dollar amount" | ||
+ | exit 0 | ||
+ | fi | ||
+ | |||
+ | |||
+ | amount=$1 | ||
+ | |||
+ | function printBills { | ||
+ | if [ XXXXX -ne "0" ]; then | ||
+ | echo "$1 $2-bill(s)" | ||
+ | fi | ||
+ | } | ||
+ | |||
+ | function breakAmount { | ||
+ | no20s=$( expr XXXXX / 20 ) | ||
+ | amount=$( expr $amount % 20 ) | ||
+ | no10s=$( expr $amount / 10 ) | ||
+ | amount=$( expr $amount % 10 ) | ||
+ | no5s=$( expr $amount / 5 ) | ||
+ | no1s=$( expr $amount % 5 ) | ||
+ | |||
+ | printBills $no20s XXXXX | ||
+ | printBills XXXXX "10" | ||
+ | printBills XXXXX "5" | ||
+ | printBills $no1s XXXXX | ||
+ | } | ||
+ | |||
+ | breakAmount $amount | ||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | * Here is an example of how it works: | ||
+ | <br /> | ||
+ | |||
+ | cs231a@aurora ~/handout $ '''./funcTeller.sh''' | ||
+ | Syntax ./funcTeller.sh nnnn | ||
+ | where nnnn is a positive dollar amount | ||
+ | |||
+ | cs231a@aurora ~/handout $ '''./funcTeller.sh 1234''' | ||
+ | 61 20-bill(s) | ||
+ | 1 10-bill(s) | ||
+ | 4 1-bill(s) | ||
+ | |||
+ | cs231a@aurora ~/handout $ | ||
+ | |||
+ | <br /> | ||
+ | =Bash Functions Returning Values= | ||
+ | <br /> | ||
+ | Bash has a strange (weird ?) way of implementing functions returning values. Let's observe the following example, that will print the following output: | ||
+ | |||
+ | 1 2 | ||
+ | 2 4 | ||
+ | 3 6 | ||
+ | 4 8 | ||
+ | 5 10 | ||
+ | |||
+ | Here's the code for this script: | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # func3.sh | ||
+ | # D. Thiebaut | ||
+ | # display 5 ints and their double. | ||
+ | |||
+ | function doubleIt { | ||
+ | return $( expr $1 \* 2 ) | ||
+ | } | ||
+ | |||
+ | for i in 1 2 3 4 5 ; do | ||
+ | echo -n $i " " | ||
+ | doubleIt $i | ||
+ | echo $? | ||
+ | done | ||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | * The way the code above works, is that you call the function doubleIt first, passing it $i, then on the next line, you use '''$?''' to access the returned value of the function. '''$?''' is the standard way bash accesses the ''status'' of the previous command that was executed, or the previous function that was called. We will always use '''$?''' to access the value returned by the function called on the previous line. That's the way bash works. | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <tanbox> | ||
+ | Bash function can only return integers! They cannot return strings or other quantities that could be useful... :-( | ||
+ | </tanbox> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | ==Another Example== | ||
+ | <br /> | ||
+ | The script below returns the number of ".asm" and ".sh" files contained in your current directory: | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # func4.sh | ||
+ | # D. Thiebaut | ||
+ | # return the number of files with a given extension | ||
+ | |||
+ | function countFiles { | ||
+ | num=`ls *.$1 2> /dev/null | wc -l ` | ||
+ | return $num | ||
+ | } | ||
+ | |||
+ | countFiles "asm" | ||
+ | echo "Number of asm files: " $? | ||
+ | |||
+ | countFiles "sh" | ||
+ | echo "Number of bash scripts: " $? | ||
+ | |||
+ | for ext in "o" "c" ; do | ||
+ | countFiles $ext | ||
+ | echo "Number of files with $ext extension: " $? | ||
+ | done | ||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | ; output: | ||
+ | <br /> | ||
+ | |||
+ | cs231a@aurora ~/handout $ '''./func4.sh''' | ||
+ | Number of asm files: 34 | ||
+ | Number of bash scripts: 10 | ||
+ | Number of files with o extension: 3 | ||
+ | Number of files with c extension: 0 | ||
+ | |||
+ | * Note the redirection to /dev/null in the function; this is something new. By using "2> /dev/null" we are sending all error messages generated by the '''ls''' command to '''/dev/null''', which on Linux systems is a file that has always length zero. You can copy or redirect huge outputs to /dev/null, and it will absorb everything without ever growing. Think of it as a trash can that incinerate everything you put in, making the trash can always empty. I used this trick because when there are no files with the given extension, the '''ls''' command generates an error message, like this one: | ||
+ | |||
+ | ls: cannot access *.c: No such file or directory | ||
+ | |||
+ | :Sending the error messages to /dev/null makes the output look neater. | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <!-- ----------------------------------------------------------------------------------------------- --> | ||
+ | {| style="width:100%; background:silver" | ||
+ | |- | ||
+ | | | ||
+ | |||
+ | ==Challenge 3== | ||
+ | |} | ||
+ | [[Image:QuestionMark3.jpg|right|120px]] | ||
+ | <br /> | ||
+ | <br /> | ||
+ | Create a new script with a function that will receive one argument, a string, and will return the number of lines in Ulysses.txt that contain the string. If you have removed your Ulysses.txt file, you can use ''getcopy'' to get a fresh copy of it. | ||
+ | <br /> | ||
+ | Examples of use: | ||
+ | <br /> | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge3.sh chocolate | ||
+ | The word chocolate appears 6 times in Ulysses.txt | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge3.sh Buck | ||
+ | The word Buck appears 167 times in Ulysses.txt | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge3.sh wine | ||
+ | The word wine appears 71 times in Ulysses.txt | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge3.sh water | ||
+ | The word water appears 224 times in Ulysses.txt | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge3.sh thiebaut | ||
+ | The word thiebaut appears 0 times in Ulysses.txt | ||
+ | |||
+ | <br /> | ||
+ | <br /> | ||
+ | =Returning Strings= | ||
+ | <br /> | ||
+ | * Bash functions can return only integers. | ||
+ | * What if you want a function that returns a string? The answer is that you can, but not using the '''return''' keyword. You simply store the string you want to return in a variable, and since in Bash all variables are '''global''', then that variable will be available in your main program. | ||
+ | * Example: | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # func5.sh | ||
+ | # | ||
+ | |||
+ | function pickOne { | ||
+ | if [ "$1" -eq "1" ] ; then | ||
+ | food="chocolate" | ||
+ | return 0 | ||
+ | fi | ||
+ | if [ "$1" -eq "2" ] ; then | ||
+ | food="liver" | ||
+ | return 0 | ||
+ | fi | ||
+ | if [ "$1" -eq "1" ] ; then | ||
+ | food="ice cream" | ||
+ | return 0 | ||
+ | fi | ||
+ | food="milk" | ||
+ | } | ||
+ | |||
+ | for i in 0 1 2 3 4 5 ; do | ||
+ | pickOne $i | ||
+ | echo "$i $food" | ||
+ | done | ||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | ; its output: | ||
+ | <br /> | ||
+ | |||
+ | 0 milk | ||
+ | 1 chocolate | ||
+ | 2 liver | ||
+ | 3 milk | ||
+ | 4 milk | ||
+ | 5 milk | ||
+ | |||
+ | <br /> | ||
+ | <br /> | ||
+ | ==Debugging Bash Scripts== | ||
+ | <br /> | ||
+ | * Writing a script is similar to writing a Python or Java program: sometimes the code does not behave as expected. In this case, echo-ing debugging information to the screen is a good way to get a sense of what is going on. | ||
+ | * Add the following '''echo''' statement as the first line of the function in the script above, and run it. See how you see better what is going on with the function: | ||
+ | <br /> | ||
+ | ::<source lang="bash" highlight="2"> | ||
+ | function pickOne { | ||
+ | echo "pickOne \$1 = $1" | ||
+ | if [ "$1" -eq "1" ] ; then | ||
+ | ... | ||
+ | </source> | ||
+ | <br/> | ||
+ | * Do not hesitate to use this method to visualize the parameters passed to your functions. If a function is not working properly, chances are it is not getting the information you think it's getting... | ||
+ | <br /> | ||
+ | <br /> | ||
+ | |||
+ | <!-- ----------------------------------------------------------------------------------------------- --> | ||
+ | {| style="width:100%; background:silver" | ||
+ | |- | ||
+ | | | ||
+ | |||
+ | ==Challenge 4== | ||
+ | |} | ||
+ | [[Image:QuestionMark4.jpg|right|120px]] | ||
+ | <br /> | ||
+ | * Write a new teller-machine program that takes an integer on the command line, breaks it into a number of $20-bills, $10-bills, $5-bills, and $1-bills, and that will use a function that returns the string "bill" or "bills" whether the number of a bills in a particular denomination is 1 or not. | ||
+ | * Here is an example of the output of the solution script: | ||
+ | <br /> | ||
+ | |||
+ | cs231a@aurora ~/handout $ ./funcChallenge4.sh 1234 | ||
+ | 61 20-bills | ||
+ | 1 10-bill | ||
+ | 4 1-bills | ||
+ | |||
+ | <br /> | ||
+ | <br /> | ||
+ | =Moodle Submission= | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | |||
+ | <!-- ----------------------------------------------------------------------------------------------- --> | ||
+ | {| style="width:100%; background:silver" | ||
+ | |- | ||
+ | | | ||
+ | |||
+ | ==Moodle Challenge 5== | ||
+ | |} | ||
+ | [[Image:QuestionMark5.jpg|right|120px]] | ||
+ | <br /> | ||
+ | * Before working on the script, please modify your copy of Ulysses.txt and add the following line at the top of the file: | ||
+ | |||
+ | (Used in CSC231, at Smith College, Massachusetts) | ||
+ | |||
+ | : This will allow us to grep for CSC231, which should occur only once. We'll find out that Massachusetts appears twice, once in the line just added, and another time, somewhere in Joyce's book. | ||
+ | * Write a script called '''funcChallenge5.sh''' that uses functions and that allows the user to enter a list of words on the command line, which the script will grep for in Ulysses.txt, and report on the number of times each word is found. | ||
+ | <br /> | ||
+ | * Here's an example | ||
+ | |||
+ | cs231a@aurora ~/handout $ '''./funcChallenge5.sh hello CSC231 Class 1123 Smith Massachusetts''' | ||
+ | hello appears 38 times in Ulysses.txt | ||
+ | CSC231 appears once in Ulysses.txt | ||
+ | Class appears 36 times in Ulysses.txt | ||
+ | 1123 appears no times in Ulysses.txt | ||
+ | Smith appears 17 times in Ulysses.txt | ||
+ | Massachusetts appears twice in Ulysses.txt | ||
+ | |||
+ | * Notice that the script will ouput "once" when the word appears one time, "twice" for two times, "no times" when the word is not present, and otherwise will output the integer followed by "times." | ||
+ | * Submit your script to Moodle. | ||
+ | ; Note 1 | ||
+ | : the grep search is not case sensitive. | ||
+ | ; Note 2 | ||
+ | : if you want to loop through all the words typed on the command line, you can use this type of for-loop: | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | for word in $@ ; do | ||
+ | echo $word | ||
+ | done | ||
+ | </source> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <showafterdate after="20171103 11:40" before="20171231 00:00"> | ||
+ | =Solutions= | ||
+ | <br /> | ||
+ | ==Challenge 1== | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcChallenge1.sh | ||
+ | # D. Thiebaut | ||
+ | |||
+ | printBar() { | ||
+ | echo "------------------------" | ||
+ | } | ||
+ | |||
+ | function printName { | ||
+ | echo "Hello $1" | ||
+ | } | ||
+ | |||
+ | function printAge { | ||
+ | echo "Your age: $1" | ||
+ | } | ||
+ | |||
+ | function printInfo { | ||
+ | printBar | ||
+ | printName $1 | ||
+ | printAge $2 | ||
+ | printBar | ||
+ | } | ||
+ | |||
+ | printInfo "Kathleen McCartney" 61 | ||
+ | |||
+ | |||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | ==Challenge 2== | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcTeller.sh | ||
+ | # D. Thiebaut | ||
+ | # Gets a number from the user and breaks it down | ||
+ | # into a number of $20, $10, $5, and $1 | ||
+ | |||
+ | if [ "$#" -ne 1 ] ; then | ||
+ | echo "Syntax $0 nnnn" | ||
+ | echo "where nnnn is a positive dollar amount" | ||
+ | exit 0 | ||
+ | fi | ||
+ | |||
+ | |||
+ | amount=$1 | ||
+ | |||
+ | function printBills { | ||
+ | if [ "$1" -ne "0" ]; then | ||
+ | echo "$1 $2-bill(s)" | ||
+ | fi | ||
+ | } | ||
+ | |||
+ | function breakAmount { | ||
+ | no20s=$( expr $1 / 20 ) | ||
+ | amount=$( expr $amount % 20 ) | ||
+ | no10s=$( expr $amount / 10 ) | ||
+ | amount=$( expr $amount % 10 ) | ||
+ | no5s=$( expr $amount / 5 ) | ||
+ | no1s=$( expr $amount % 5 ) | ||
+ | |||
+ | printBills $no20s "20" | ||
+ | printBills $no10s "10" | ||
+ | printBills $no5s "5" | ||
+ | printBills $no1s "1" | ||
+ | } | ||
+ | |||
+ | breakAmount $amount | ||
+ | </source> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | ==Challenge #3== | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcChallenge3.sh | ||
+ | # count the number of times a word appear in Ulysses.txt | ||
+ | # | ||
+ | |||
+ | function countWord { | ||
+ | num=`grep -i $1 Ulysses.txt | wc -l` | ||
+ | return $num | ||
+ | } | ||
+ | |||
+ | if [ "$#" -ne "1" ] ; then | ||
+ | echo "Syntax ./funcChallenge3.sh sssss" | ||
+ | echo "where sssss is a string (word) that will be " | ||
+ | echo "grepped in Ulysses.txt" | ||
+ | exit | ||
+ | fi | ||
+ | |||
+ | countWord $1 | ||
+ | echo "The word $1 appears $? times in Ulysses.txt" | ||
+ | |||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | ==Challenge #4== | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcChallenge4.sh | ||
+ | # D. Thiebaut | ||
+ | # Gets a number from the user and breaks it down | ||
+ | # into a number of $20, $10, $5, and $1 | ||
+ | |||
+ | if [ "$#" -ne 1 ] ; then | ||
+ | echo "Syntax $0 nnnn" | ||
+ | echo "where nnnn is a positive dollar amount" | ||
+ | exit 0 | ||
+ | fi | ||
+ | |||
+ | |||
+ | amount=$1 | ||
+ | |||
+ | function billOrBills { | ||
+ | if [ "$1" -eq "1" ] ; then | ||
+ | billString="bill" | ||
+ | else | ||
+ | billString="bills" | ||
+ | fi | ||
+ | } | ||
+ | |||
+ | function printBills { | ||
+ | if [ "$1" -ne "0" ]; then | ||
+ | billOrBills $1 | ||
+ | echo "$1 $2-$billString" | ||
+ | fi | ||
+ | } | ||
+ | |||
+ | function breakAmount { | ||
+ | no20s=$( expr $1 / 20 ) | ||
+ | amount=$( expr $amount % 20 ) | ||
+ | no10s=$( expr $amount / 10 ) | ||
+ | amount=$( expr $amount % 10 ) | ||
+ | no5s=$( expr $amount / 5 ) | ||
+ | no1s=$( expr $amount % 5 ) | ||
+ | |||
+ | printBills $no20s "20" | ||
+ | printBills $no10s "10" | ||
+ | printBills $no5s "5" | ||
+ | printBills $no1s "1" | ||
+ | } | ||
+ | |||
+ | breakAmount $amount | ||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | </showafterdate> | ||
+ | <!-- ======================================================= --> | ||
+ | <!-- ======================================================= --> | ||
+ | <!-- ======================================================= --> | ||
+ | <showafterdate after="20171103 11:40" before="20171231 00:00"> | ||
+ | ==Moodle Challenge== | ||
+ | <br /> | ||
+ | ::<source lang="bash"> | ||
+ | #! /bin/bash | ||
+ | # funcChallenge5.sh | ||
+ | # | ||
+ | |||
+ | function timesPlural { | ||
+ | #echo "timesPlural \$1 = $1 \$2 = $2" | ||
+ | if [ "$1" -eq "1" ] ; then | ||
+ | numTimes="once" | ||
+ | return 0 | ||
+ | fi | ||
+ | if [ "$1" -eq "2" ] ; then | ||
+ | numTimes="twice" | ||
+ | return 0 | ||
+ | fi | ||
+ | if [ "$1" -eq "0" ] ; then | ||
+ | numTimes="no times" | ||
+ | return 0 | ||
+ | fi | ||
+ | numTimes="$1 times" | ||
+ | } | ||
+ | |||
+ | function grepWordFile { | ||
+ | #echo "grepWordFile \$1 = $1 \$2 = $2" | ||
+ | num=` grep -i $1 $2 | wc -l` | ||
+ | #echo "num = " $num | ||
+ | timesPlural $num | ||
+ | echo "$1 appears $numTimes in $2" | ||
+ | } | ||
+ | |||
+ | for word in $@ ; do | ||
+ | #echo "word $word" | ||
+ | grepWordFile $word Ulysses.txt | ||
+ | done | ||
+ | |||
+ | |||
+ | </source> | ||
+ | <br /> | ||
+ | </showafterdate> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | <br /> | ||
+ | [[Category:CSC231]][[Category:Bash]][[Category:Labs]] |
Latest revision as of 13:57, 29 April 2018
--D. Thiebaut (talk) 13:13, 1 November 2017 (EDT)
Today's lab is about bash functions. Bash support functions, and they work similarly to functions in Python or Java, but with a twist. As is usual with bash, you will find that bash supports different syntaxes for writing functions, and restricted ways of passing parameters or returning values.
The script you have to write for the last challenge should be submitted to Moodle.
The solutions to the other challenges will be available at the end of the page around 11:40 a.m. on Friday.
|
|
Bash Functions
References
- Ryans Tutorials are very easy to read tutorials on all aspects of the bash shell, including today's subject: bash functions.
- Also, this fairly extensive article by Mokhtar Ebrahim on Bash functions.
Introduction
There are two ways of declaring functions in bash, illustrated in the code below:
#! /bin/bash # func1.sh # D. Thiebaut # prints some messages printSomething() { echo "Hello there!" } function printSomethingElse { echo "Hello again!" } printSomething printSomething printSomethingElse
- Create the script above, make it executable, and run it, to see how it works.
- Add a call to printSomething inside the printSomethingElse function, just to see if functions can actually call functions... Does bash accept nested calls?
Passing Arguments
- Inside a function, $1 will refer to the first parameter passed to the function, $2 will refer to the second argument, etc.
- You do not put the parameters inside the parenthesis, when declaring the function.
- Here is an example, with both style functions:
#! /bin/bash # func1.sh # D. Thiebaut printBar() { echo "------------------------" } function printName { echo "Hello $1" } printAge() { echo "Your age: $1" } printBar printName "Kathleen McCartney" printAge 61 printBar
Challenge 1 |
- Add a new function to func2.sh called printInfo(). The new function takes 2 parameters and calls printName and printAge to print both. Here is an example of how to call it (that will be the only function call in the main part of the script):
printInfo "Kathleen" 61
- and the output will be the same as the previous version of func2.sh:
------------------------ Hello Kathleen Your age: 61 ------------------------
Challenge 2 |
- Below is an incomplete bash script that implements the teller machine script we saw earlier. It prompts the user for an integer, and takes the number as a dollar amount that is broken into a number of $20-bills, $10-bills, $5-bills and $1-bills. You need to replace the XXXXXX symbols by the appropriate expression(s)...
#! /bin/bash # funcTeller.sh # D. Thiebaut # Gets a number from the user and breaks it down # into a number of $20, $10, $5, and $1 if [ "$#" -ne 1 ] ; then echo "Syntax $0 nnnn" echo "where nnnn is a positive dollar amount" exit 0 fi amount=$1 function printBills { if [ XXXXX -ne "0" ]; then echo "$1 $2-bill(s)" fi } function breakAmount { no20s=$( expr XXXXX / 20 ) amount=$( expr $amount % 20 ) no10s=$( expr $amount / 10 ) amount=$( expr $amount % 10 ) no5s=$( expr $amount / 5 ) no1s=$( expr $amount % 5 ) printBills $no20s XXXXX printBills XXXXX "10" printBills XXXXX "5" printBills $no1s XXXXX } breakAmount $amount
- Here is an example of how it works:
cs231a@aurora ~/handout $ ./funcTeller.sh Syntax ./funcTeller.sh nnnn where nnnn is a positive dollar amount cs231a@aurora ~/handout $ ./funcTeller.sh 1234 61 20-bill(s) 1 10-bill(s) 4 1-bill(s) cs231a@aurora ~/handout $
Bash Functions Returning Values
Bash has a strange (weird ?) way of implementing functions returning values. Let's observe the following example, that will print the following output:
1 2 2 4 3 6 4 8 5 10
Here's the code for this script:
#! /bin/bash # func3.sh # D. Thiebaut # display 5 ints and their double. function doubleIt { return $( expr $1 \* 2 ) } for i in 1 2 3 4 5 ; do echo -n $i " " doubleIt $i echo $? done
- The way the code above works, is that you call the function doubleIt first, passing it $i, then on the next line, you use $? to access the returned value of the function. $? is the standard way bash accesses the status of the previous command that was executed, or the previous function that was called. We will always use $? to access the value returned by the function called on the previous line. That's the way bash works.
Bash function can only return integers! They cannot return strings or other quantities that could be useful... :-(
Another Example
The script below returns the number of ".asm" and ".sh" files contained in your current directory:
#! /bin/bash # func4.sh # D. Thiebaut # return the number of files with a given extension function countFiles { num=`ls *.$1 2> /dev/null | wc -l ` return $num } countFiles "asm" echo "Number of asm files: " $? countFiles "sh" echo "Number of bash scripts: " $? for ext in "o" "c" ; do countFiles $ext echo "Number of files with $ext extension: " $? done
- output
cs231a@aurora ~/handout $ ./func4.sh Number of asm files: 34 Number of bash scripts: 10 Number of files with o extension: 3 Number of files with c extension: 0
- Note the redirection to /dev/null in the function; this is something new. By using "2> /dev/null" we are sending all error messages generated by the ls command to /dev/null, which on Linux systems is a file that has always length zero. You can copy or redirect huge outputs to /dev/null, and it will absorb everything without ever growing. Think of it as a trash can that incinerate everything you put in, making the trash can always empty. I used this trick because when there are no files with the given extension, the ls command generates an error message, like this one:
ls: cannot access *.c: No such file or directory
- Sending the error messages to /dev/null makes the output look neater.
Challenge 3 |
Create a new script with a function that will receive one argument, a string, and will return the number of lines in Ulysses.txt that contain the string. If you have removed your Ulysses.txt file, you can use getcopy to get a fresh copy of it.
Examples of use:
cs231a@aurora ~/handout $ ./funcChallenge3.sh chocolate The word chocolate appears 6 times in Ulysses.txt cs231a@aurora ~/handout $ ./funcChallenge3.sh Buck The word Buck appears 167 times in Ulysses.txt cs231a@aurora ~/handout $ ./funcChallenge3.sh wine The word wine appears 71 times in Ulysses.txt cs231a@aurora ~/handout $ ./funcChallenge3.sh water The word water appears 224 times in Ulysses.txt cs231a@aurora ~/handout $ ./funcChallenge3.sh thiebaut The word thiebaut appears 0 times in Ulysses.txt
Returning Strings
- Bash functions can return only integers.
- What if you want a function that returns a string? The answer is that you can, but not using the return keyword. You simply store the string you want to return in a variable, and since in Bash all variables are global, then that variable will be available in your main program.
- Example:
#! /bin/bash # func5.sh # function pickOne { if [ "$1" -eq "1" ] ; then food="chocolate" return 0 fi if [ "$1" -eq "2" ] ; then food="liver" return 0 fi if [ "$1" -eq "1" ] ; then food="ice cream" return 0 fi food="milk" } for i in 0 1 2 3 4 5 ; do pickOne $i echo "$i $food" done
- its output
0 milk 1 chocolate 2 liver 3 milk 4 milk 5 milk
Debugging Bash Scripts
- Writing a script is similar to writing a Python or Java program: sometimes the code does not behave as expected. In this case, echo-ing debugging information to the screen is a good way to get a sense of what is going on.
- Add the following echo statement as the first line of the function in the script above, and run it. See how you see better what is going on with the function:
function pickOne { echo "pickOne \$1 = $1" if [ "$1" -eq "1" ] ; then ...
- Do not hesitate to use this method to visualize the parameters passed to your functions. If a function is not working properly, chances are it is not getting the information you think it's getting...
Challenge 4 |
- Write a new teller-machine program that takes an integer on the command line, breaks it into a number of $20-bills, $10-bills, $5-bills, and $1-bills, and that will use a function that returns the string "bill" or "bills" whether the number of a bills in a particular denomination is 1 or not.
- Here is an example of the output of the solution script:
cs231a@aurora ~/handout $ ./funcChallenge4.sh 1234 61 20-bills 1 10-bill 4 1-bills
Moodle Submission
Moodle Challenge 5 |
- Before working on the script, please modify your copy of Ulysses.txt and add the following line at the top of the file:
(Used in CSC231, at Smith College, Massachusetts)
- This will allow us to grep for CSC231, which should occur only once. We'll find out that Massachusetts appears twice, once in the line just added, and another time, somewhere in Joyce's book.
- Write a script called funcChallenge5.sh that uses functions and that allows the user to enter a list of words on the command line, which the script will grep for in Ulysses.txt, and report on the number of times each word is found.
- Here's an example
cs231a@aurora ~/handout $ ./funcChallenge5.sh hello CSC231 Class 1123 Smith Massachusetts hello appears 38 times in Ulysses.txt CSC231 appears once in Ulysses.txt Class appears 36 times in Ulysses.txt 1123 appears no times in Ulysses.txt Smith appears 17 times in Ulysses.txt Massachusetts appears twice in Ulysses.txt
- Notice that the script will ouput "once" when the word appears one time, "twice" for two times, "no times" when the word is not present, and otherwise will output the integer followed by "times."
- Submit your script to Moodle.
- Note 1
- the grep search is not case sensitive.
- Note 2
- if you want to loop through all the words typed on the command line, you can use this type of for-loop:
for word in $@ ; do echo $word done
<showafterdate after="20171103 11:40" before="20171231 00:00">
Solutions
Challenge 1
#! /bin/bash # funcChallenge1.sh # D. Thiebaut printBar() { echo "------------------------" } function printName { echo "Hello $1" } function printAge { echo "Your age: $1" } function printInfo { printBar printName $1 printAge $2 printBar } printInfo "Kathleen McCartney" 61
Challenge 2
#! /bin/bash # funcTeller.sh # D. Thiebaut # Gets a number from the user and breaks it down # into a number of $20, $10, $5, and $1 if [ "$#" -ne 1 ] ; then echo "Syntax $0 nnnn" echo "where nnnn is a positive dollar amount" exit 0 fi amount=$1 function printBills { if [ "$1" -ne "0" ]; then echo "$1 $2-bill(s)" fi } function breakAmount { no20s=$( expr $1 / 20 ) amount=$( expr $amount % 20 ) no10s=$( expr $amount / 10 ) amount=$( expr $amount % 10 ) no5s=$( expr $amount / 5 ) no1s=$( expr $amount % 5 ) printBills $no20s "20" printBills $no10s "10" printBills $no5s "5" printBills $no1s "1" } breakAmount $amount
Challenge #3
#! /bin/bash # funcChallenge3.sh # count the number of times a word appear in Ulysses.txt # function countWord { num=`grep -i $1 Ulysses.txt | wc -l` return $num } if [ "$#" -ne "1" ] ; then echo "Syntax ./funcChallenge3.sh sssss" echo "where sssss is a string (word) that will be " echo "grepped in Ulysses.txt" exit fi countWord $1 echo "The word $1 appears $? times in Ulysses.txt"
Challenge #4
#! /bin/bash # funcChallenge4.sh # D. Thiebaut # Gets a number from the user and breaks it down # into a number of $20, $10, $5, and $1 if [ "$#" -ne 1 ] ; then echo "Syntax $0 nnnn" echo "where nnnn is a positive dollar amount" exit 0 fi amount=$1 function billOrBills { if [ "$1" -eq "1" ] ; then billString="bill" else billString="bills" fi } function printBills { if [ "$1" -ne "0" ]; then billOrBills $1 echo "$1 $2-$billString" fi } function breakAmount { no20s=$( expr $1 / 20 ) amount=$( expr $amount % 20 ) no10s=$( expr $amount / 10 ) amount=$( expr $amount % 10 ) no5s=$( expr $amount / 5 ) no1s=$( expr $amount % 5 ) printBills $no20s "20" printBills $no10s "10" printBills $no5s "5" printBills $no1s "1" } breakAmount $amount
</showafterdate>
<showafterdate after="20171103 11:40" before="20171231 00:00">
Moodle Challenge
#! /bin/bash # funcChallenge5.sh # function timesPlural { #echo "timesPlural \$1 = $1 \$2 = $2" if [ "$1" -eq "1" ] ; then numTimes="once" return 0 fi if [ "$1" -eq "2" ] ; then numTimes="twice" return 0 fi if [ "$1" -eq "0" ] ; then numTimes="no times" return 0 fi numTimes="$1 times" } function grepWordFile { #echo "grepWordFile \$1 = $1 \$2 = $2" num=` grep -i $1 $2 | wc -l` #echo "num = " $num timesPlural $num echo "$1 appears $numTimes in $2" } for word in $@ ; do #echo "word $word" grepWordFile $word Ulysses.txt done
</showafterdate>