MoreBash:Exercises - Control flow

From Juneday education
Jump to: navigation, search

if statement

Check if it is the first day of the month

Sometimes, e g when writing a backup script, you want to take some action one day ad some oter the other days. We're going to do that now.

Write a script that prints the current date (using the date command). If the number of the day (in the month) is the first the script shall also output "full backup needed" otherwise the script shall output "incremental backup only"

Expand using link to the right to see a hint.

Store the current day in month in a variable like this:

DAY_IN_MONTH=$(date +"%d")

Expand using link to the right to see a solution.

Create a (text) file, typically called backup-simple.sh with the following content.

#!/bin/bash

DAY_IN_MONTH=$(date +"%d")
date +"%Y-%m-%d %H:%m:%S"

if [ "$DAY_IN_MONTH" = "01" ]
then
    echo "full backup needed"
else
    echo "incremental backup only"
fi

Check if it is the first or fifteenth day of the month

Extend the script above to out "full backup needed" also on the fifteenth day of the month.

Expand using link to the right to see a hint.

You can do this either by using || in the original if statement or you can write an elifstatement.

Expand using link to the right to see a solution.

Create a (text) file, typically called backup-fifteen.sh with the following content.

#!/bin/bash

DAY_IN_MONTH=$(date +"%d")
date +"%Y-%m-%d %H:%m:%S"

# Suggested solution number 01
if [ "$DAY_IN_MONTH" = "01" ] || [ "$DAY_IN_MONTH" = "15" ]
then
    echo "full backup needed"
else
    echo "incremental backup only"
fi

# Suggested solution number 02
if [ "$DAY_IN_MONTH" = "01" ]
then
    echo "full backup needed"
elif [ "$DAY_IN_MONTH" = "15" ]
then
    echo "full backup needed"
else
    echo "incremental backup only"
fi

More flexible day checking

Extend the script above to use two vairables FULL_BACKUP_DAY1 and FULL_BACKUP_DAY2. The value of these variables shall be used instead of the hardcoded "01" and "15".

Expand using link to the right to see a hint.

FULL_BACKUP_DAY1="01"
.....
elif [ "$DAY_IN_MONTH" = "$FULL_BACKUP_DAY1" ]

Expand using link to the right to see a solution.

Create a (text) file, typically called backup-flexible.sh with the following content.

#!/bin/bash                                                                                                                                          

FULL_BACKUP_DAY1="01"
FULL_BACKUP_DAY2="15"

DAY_IN_MONTH=$(date +"%d")
date +"%Y-%m-%d %H:%m:%S"

# Suggested solution number 01
if [ "$DAY_IN_MONTH" = "$FULL_BACKUP_DAY1" ] || [ "$DAY_IN_MONTH" = "$FULL_BACKUP_DAY2" ]
then
    echo "full backup needed"
else
    echo "incremental backup only"
fi

Checking command line options

Date and Time script with option

Write a script that prints the current date and time (using the date command) in the defualt format. If the user invokes the script with the option --swedish the following format "yyyy-mm-hh hh:mm:ss" shall be used to format the output.

Expand using link to the right to see a hint.

The command line option passed by the user can be found in the variable $1. Check if that variable has the value --swedish.

Expand using link to the right to see a solution.

Create a (text) file, typically called date-time-option.sh with the following content.

#!/bin/bash

if [ "$1" = "--swedish" ]
then
    date +"%Y-%m-%d %H:%m:%S"
else
    date
fi

while

Date and Time script with options using a while loop

Extend the previous script to handle the following options:

--time - with this option only the time (not date) shall be printed

--date - with this option only the date (not time) shall be printed

--swedish - with this option the date shall use swedish format

Expand using link to the right to see a solution.

Create a (text) file, typically called date-time-options.sh with the following content.

#!/bin/bash

if [ "$1" = "--date" ]
then
    date +"%Y-%m-%d"
elif [ "$1" = "--time" ]
then
    date +"%H:%m:%S"
elif [ "$1" = "--swedish" ]
then
    date +"%Y-%m-%d %H:%m:%S"
else
    date
fi


Date and Time script with options using a loop

We should now change the previous script a bit. We shall now loop through the arguments to find out what the user wants (that is we shall parse the user options) and then, when done parsing, we shall take action based on that.

--time - print date

--date - print time

--swedish - with this option the date shall use swedish format

The script shall print (in swedish or normal format):

  • only time if only --time was used
  • only date if only --date was used
  • both date and time if both --date and --time was used.
  • both date and time if neither of --date and --time was used.

Expand using link to the right to see a solution.

Create a (text) file, typically called date-time-options-while.sh with the following content.

#!/bin/bash

#
# This is a suggested solution to an exercise in one of Juneday Education's bash books
#
# The solution is using only the techniques and tools known by the
# students. We (the authors) know that there are better ways of
# solving this problem. We're writing a solution that we believe the
# students will understand :)
#
#

while [ "$1" != "" ]
do
    
    if [ "$1" = "--date" ]
    then
        DATE=true
    elif [ "$1" = "--time" ]
    then
        TIME=true
    elif [ "$1" = "--swedish" ]
    then
        FORMAT=swedish
    fi
    shift
done          



if [ "$DATE" = "true" ] && [ "$TIME" = "true" ] 
then
    BOTH=true
fi

if [ "$DATE" = "" ] && [ "$TIME" = "" ] 
then
    BOTH=true
fi


if [ "$BOTH" = "true" ]
then
    if [ "$FORMAT" = "swedish" ]
    then
        date +"%Y-%m-%d %H:%m:%S"
    else
        date
    fi
elif [ "$DATE" = "true" ] 
then
    date +"%Y-%m-%d"
elif [ "$TIME" = "true" ]
then
    date +"%H:%m:%S"
else
    echo "Uh oh...."
    exit 1
fi

exit 0

Alternative version:

#!/bin/bash

#
# Uses the more modern [[ keyword ("new test")
# and slightly more compact code
#

while [[ $1 != "" ]]
do
    if [[ $1 == --date ]]
    then
        DATE=true
    elif [[ $1 == --time ]]
    then
        TIME=true
    elif [[ $1 == --swedish ]]
    then
        FORMAT=swedish
    fi
    shift
done



if [[ ($DATE && $TIME) || ($DATE == "" && $TIME == "") ]]
then
    BOTH=true
fi

if [[ $FORMAT == swedish ]]
then
    DF_BOTH="%Y-%m-%d %H:%m:%S"
    DF_TIME="%H:%m:%S"
    DF_DATE="%Y-%m-%d"
else
    DF_BOTH="%m/%d/%Y %r"
    DF_TIME="%r"
    DF_DATE="%m/%d/%Y"
fi

if [[ $BOTH ]]
then
    date +"$DF_BOTH"
elif [[ $DATE ]]
then
    date +"$DF_DATE"
elif [[ $TIME ]]
then
    date +"$DF_TIME"
fi

exit 0

Example runs of the alternative version (uses LC_TIME=en_US.UTF-8 to make the date formats different when the --swedish flag is present):

$ LC_TIME=en_US.UTF-8 ./date.sh
12/23/2018 08:55:53 PM

$ LC_TIME=en_US.UTF-8 ./date.sh --date
12/23/2018

$ LC_TIME=en_US.UTF-8 ./date.sh --time
08:56:30 PM

$ LC_TIME=en_US.UTF-8 ./date.sh --time --date
12/23/2018 08:56:49 PM

$ LC_TIME=en_US.UTF-8 ./date.sh --swedish
2018-12-23 20:12:06

$ LC_TIME=en_US.UTF-8 ./date.sh --swedish --date
2018-12-23

$ LC_TIME=en_US.UTF-8 ./date.sh --swedish --time
20:12:38

$ LC_TIME=en_US.UTF-8 ./date.sh --swedish --time --date
2018-12-23 20:12:58

for

Loop through a simple string

Write a script that defines a variable like this:

WORDS="This is a small sentence with some words"

Loop through the words in the variable using a for loop. The words shall be printed on separate lines.

Expand using link to the right to see a solution.

Create a (text) file, typically called simple-for.sh with the following content.

#!/bin/bash

WORDS="This is a small sentence with some words"

for word in $WORDS
do
    echo "$word"
done

Parse with a for loop

Rewrite the script date-time-options-while.sh to use for statement instead of while in the parser part.

Expand using link to the right to see a solution.

Create a (text) file, typically called date-time-options-for.sh with the following content.

#!/bin/bash

#
# This is a suggested solution to an exercise in one of Juneday Education's bash books
#
# The solution is using only the techniques and tools known by the
# students. We (the authors) know that there are better ways of
# solving this problem. We're writing a solution that we believe the
# students will understand :)
#
#

for option in $*
do
    if [ "$option" = "--date" ]
    then
        DATE=true
    elif [ "$option" = "--time" ]
    then
        TIME=true
    elif [ "$option" = "--swedish" ]
    then
        FORMAT=swedish
    fi
    shift
done          



if [ "$DATE" = "true" ] && [ "$TIME" = "true" ] 
then
    BOTH=true
fi

if [ "$DATE" = "" ] && [ "$TIME" = "" ] 
then
    BOTH=true
fi


if [ "$BOTH" = "true" ]
then
    if [ "$FORMAT" = "swedish" ]
    then
        date +"%Y-%m-%d %H:%m:%S"
    else
        date
    fi
elif [ "$DATE" = "true" ] 
then
    date +"%Y-%m-%d"
elif [ "$TIME" = "true" ]
then
    date +"%H:%m:%S"
else
    echo "Uh oh...."
    exit 1
fi

exit 0

case

Parse with case

Rewrite the script date-time-options-while.sh to use case statement instead of if in the parsers part.

Expand using link to the right to see a solution.

Create a (text) file, typically called date-time-options-case.sh with the following content.

#!/bin/bash

#
# This is a suggested solution to an exercise in one of Juneday Education's bash books
#
# The solution is using only the techniques and tools known by the
# students. We (the authors) know that there are better ways of
# solving this problem. We're writing a solution that we believe the
# students will understand :)
#
#

while [ "$1" != "" ]
do
    case "$1" in
        "--date")
            DATE=true
            ;;
        "--time" )
            TIME=true
            ;;
        "--swedish" )
            FORMAT=swedish
            ;;
    esac
    shift
done          



if [ "$DATE" = "true" ] && [ "$TIME" = "true" ] 
then
    BOTH=true
fi

if [ "$DATE" = "" ] && [ "$TIME" = "" ] 
then
    BOTH=true
fi


if [ "$BOTH" = "true" ]
then
    if [ "$FORMAT" = "swedish" ]
    then
        date +"%Y-%m-%d %H:%m:%S"
    else
        date
    fi
elif [ "$DATE" = "true" ] 
then
    date +"%Y-%m-%d"
elif [ "$TIME" = "true" ]
then
    date +"%H:%m:%S"
else
    echo "Uh oh...."
    exit 1
fi

exit 0


Challenge exercises

Countdown

Write a script that counts down from 10 to 0 and sleeps 1 second (sleep 1) and outputs the remaining seconds in between. The use shall be able to set the the start value to a value.

Expand using link to the right to see a solution.

Create a (text) file, typically called count-down.sh with the following content.

#!/bin/sh

SECS=$1
if [ "$SECS" = "" ]
then
    SECS=10
fi

while [ 1 -le $SECS ]
do
    echo "$SECS remaining"
    SECS=$(( $SECS - 1 ))
    sleep 1
done
echo awake

Countdown with better format

The text about the remaning seconds shall be printed on one line and when done the entire output from the program shall be "erased".

Expand using link to the right to see a hint.

Chek out: printf "\r"

Expand using link to the right to see a solution.

Create a (text) file, typically called count-down-clear.sh with the following content.

#!/bin/sh

SECS=$1
if [ "$SECS" = "" ]
then
    SECS=10
fi

while [ 1 -le $SECS ]
do
    printf "\r%.3d seconds remaining" $SECS
    SECS=$(( $SECS - 1 ))
    sleep 1
done
printf "\r"

Upper/lower case script

Write a script that outputs the text in a file.

If the user supplies --upper-case the text shall be converted to all upper case letters.

If the user supplies --lower-case the text shall be converted to all lower case letters.

Expand using link to the right to see a hint.

The command tr can be used to achieve this. Try the following in a terminal:

$ echo "Hi there" | tr [a-z] [A-Z]
$ echo "Hi there" | tr [A-Z] [a-z]

... and instead of echo:ing text you can simply cat the file.


Expand using link to the right to see a solution.

Create a (text) file, typically called fileprint.sh with the following content.

#!/bin/bash

while [ "$1" != "" ]
do
    if [ "$1" = "--upper-case" ]
    then
        UC=true
        LC=falce
    elif [ "$1" = "--lower-case" ]
    then
        UC=false
        LC=true
    else
        FILE=$1
    fi
    shift
done

if [ "$FILE" = "" ] || [ ! -f $FILE ]
then
    echo "File \"$FILE\" does not exist"
    exit 1
fi

if [ "$UC" = "true" ]
then
    cat $FILE | tr [a-z] [A-Z]
elif [ "$LC" = "true" ]
then
    cat $FILE | tr [A-Z] [a-z]
else
    cat $FILE
fi

Alternative solution:

#!/bin/bash

UC=false
LC=false

while [[ $1 != "" ]]
do
    if [[ $1 == --upper-case ]]
    then
        UC=true
        LC=false
    elif [[ $1 == --lower-case ]]
    then
        UC=false
        LC=true
    else
        FILE="$1"
    fi
    shift
done

if [[ $FILE == "" || ! -f "$FILE" ]]
then
    echo "File \"$FILE\" does not exist"
    exit 1
fi

if $UC
then
    cat "$FILE" | tr [a-z] [A-Z]
elif $LC
then
    cat "$FILE" | tr [A-Z] [a-z]
else
    cat "$FILE"
fi

Test-runs of the alternative solution:

$ ./fileprint_alternative.sh Test.java 
public class Test{
    public static void main(String[] args){
	int a = 10;
	int b = -a;
	System.out.println("a: " + a + " b: " + b);
    }
}

$ ./fileprint_alternative.sh Test.java --lower-case
public class test{
    public static void main(string[] args){
	int a = 10;
	int b = -a;
	system.out.println("a: " + a + " b: " + b);
    }
}

$ ./fileprint_alternative.sh Test.java --upper-case
PUBLIC CLASS TEST{
    PUBLIC STATIC VOID MAIN(STRING[] ARGS){
	INT A = 10;
	INT B = -A;
	SYSTEM.OUT.PRINTLN("A: " + A + " B: " + B);
    }
}

Here's yet another alternative solution to the parsing:

#!/bin/bash

UC=false
LC=false

while [[ $1 != "" ]]
do
    [[ $1 == --upper-case ]] && UC=true
    [[ $1 == --lower-case ]] && LC=true
    [[ $1 =~ ^[^-] ]] && FILE="$1"
    shift
done

if [[ $FILE == "" || ! -f "$FILE" ]]
then
    echo "File \"$FILE\" does not exist"
    exit 1
fi

if $UC
then
    cat "$FILE" | tr [a-z] [A-Z]
elif $LC
then
    cat "$FILE" | tr [A-Z] [a-z]
else
    cat "$FILE"
fi

The interesting part(?) is the arguments parsing:

while [[ $1 != "" ]]
do
    [[ $1 == --upper-case ]] && UC=true
    [[ $1 == --lower-case ]] && LC=true
    [[ $1 =~ ^[^-] ]] && FILE="$1"
    shift
done

The first statement in the loop, [[ $1 == --upper-case ]] && UC=true is a test of the current $1 against the literal string -upper-case. If the test is successful, then UC is set to true. The same thing for --lower-case and LC. The third statement, [ $1 =~ ^[^-] ]] && FILE="$1" is a test using a regular expression. The regex ^[^-] means "does not start with a dash", which means it is not an option but the file name.

Note that file names can start with a dash, but that's something we really, really don't recommend. Just don't do it. The script will ignore such arguments as if they were invalid options, meaning that the file name won't be set.

Find all bash shell scripts

Write a script that recursively

  • finds all shell scripts (suffix .sh) in a directory
  • directory shall default to pwd, otherwise a user specified

Write a script that recursively

  • finds (using find all shell scripts (suffix .sh) in a directory
  • directory shall default to pwd, otherwise a user specified

Expand using link to the right to see a hint.

Check out the following command:

$ find . -type f -name "*.sh"


Expand using link to the right to see a solution.

Create a (text) file, typically called find-sh.sh with the following content.

#!/bin/bash

DIR=$(pwd)
if [ "$1" != "" ]
then
    DIR=$1
fi

find . -name "*.sh"

Find all source code files

Extend the script above to use a user supplied suffix.

Expand using link to the right to see a solution.

Create a (text) file, typically called find-src.sh, with the following content.

#!/bin/bash

SUFFIX=""
DIR=$(pwd)
for option in $*
do
    if [ -d $option ]
    then
        DIR=$option
    else
        SUFFIX=$option
    fi
    shift
done

if [ "$SUFFIX" = "" ]
then
    echo "Missing suffix"
    exit 1
fi
find $DIR -name "*.$SUFFIX"

Find all source code files - multiple languages

Extend the script above to use multiple user supplied suffices.

Expand using link to the right to see a hint.

Build up a string with arguments to find and use eval to "evaluate" the command. Like this:

eval find $DIR -type f $SUFFIX


Expand using link to the right to see a solution.

Create a (text) file, typically called find-src-mult.sh, with the following content.

#!/bin/bash

SUFFIX=""
DIR=.
for option in $*
do
    if [ -d $option ]
    then
        DIR=$option
    else
        if [ "$SUFFIX" != "" ]
        then
            SUFFIX="$SUFFIX -o "
        fi
        SUFFIX="$SUFFIX -name \"*.$option\""
    fi
    shift
done

if [ "$SUFFIX" = "" ]
then
    echo "Missing suffix"
    exit 1
else
    SUFFIX="$SUFFIX"
fi
eval find $DIR -type f $SUFFIX

Alternative solution, using -regex, e.g. find -regex '.*\.\(sh\|txt\sql\)':

#!/bin/bash

EXP=""
DIR=.

for option in $*
do
    if [[ -d "$option" ]]
    then
        DIR=$option
    else
        if [[ $EXP != "" ]]
        then
            EXP="${EXP}\\|$1"
        else
            EXP=$1
        fi
    fi
    shift
done

if [[ $EXP == "" ]]
then
    echo "Missing suffix"
    exit 1
else
    EXP="'.*\\.\\($EXP\\)'"
fi
echo "$DIR" -type f -regex "$EXP" | xargs find

This solution builds up a regular expression. The argument suffices sh sql txt results in the following regular expression: . -type f -regex '.*\.\(sh\|sql\|txt\)' which means "any character, zero or more times, followed by a dot, followed by 'sh' or 'sql' or 'txt'".

The find command can process regular expressions, using the -regex flag.

Links

Chapter links

previous (Control flow) | next (Scripts - Introduction)