Skip to content

Instantly share code, notes, and snippets.

@harrietty
Last active November 6, 2018 18:01
Show Gist options
  • Save harrietty/d2a8a5e8fda67cad98be8a365f483d92 to your computer and use it in GitHub Desktop.
Save harrietty/d2a8a5e8fda67cad98be8a365f483d92 to your computer and use it in GitHub Desktop.

Here are some posible solutions with explanations for Bash Practice Exercises. They are not necessarily the only or the best solutions - there is a preference for using a series of simple commands piped to one another.

1. A program that tells you whether the argument is a file, directory or doesn't exist

#!/bin/bash

# Using an error exit code to make sure the program does not continue.
# Exiting with anything other than 0 signifies an error.

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
fi

if [[ -d $1 ]]; then
  echo "${1} is a directory"
elif [[ -e $1 ]]; then
  echo "${1} is a file"
else
  echo "${1} does not exist"
fi

# Exit with a success code
exit 0

2. A program that takes two numbers as arguments (check they are numbers) and tells you which is largest

#!/bin/bash

if [[ ! $1 || ! $2 ]]; then
  echo "Please provide 2 arguments"
  exit 1
fi

num1=$1
num2=$2

# Using the =~ operator and a regular expression we can check whether the argument looks like a number
# Remember everything is a string in bash!

if [[ ! $num1 =~ [0-9]+ ]]; then
  echo "${num1} is not a number"
  exit 1
elif [[ ! $num2 =~ [0-9]+ ]]; then
  echo "${num2} is not a number"
  exit 1
fi

if [[ $num1 -gt $num2 ]]; then
  echo "${num1} is larger than ${num2}"
elif [[ $num2 -gt $num1 ]]; then
  echo "${num2} is larger than ${num1}"
else
  echo "${num1} and ${num2} are the same"
fi

exit 0

3. A program that takes a filename as an argument and tells you whether it is executable.

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ -d $1 ]]; then
  echo "${1} is a directory"
  exit 1
elif [[ ! -e $1 ]]; then
  echo "${1} does not exist"
  exit 1
fi

file=$1

if [[ -x $file ]]; then
  echo "${file} is executable"
else
  echo "${file} is not executable"
fi

exit 0

4. A program that takes two strings as arguments and tells you which is longer

#!/bin/bash

if [[ ! $1 || ! $2 ]]; then
  echo "Please provide 2 arguments"
  exit 1
fi

str1=$1
str2=$2

# We can put a # sign in front of a variable when it's in brackets to find out its length. 
# E.g. $str1 becomes ${#str1} to get its length

if [[ ${#str1} -gt ${#str2} ]]; then
  echo "${str1} is longer than ${str2}"
elif [[ ${#str2} -gt ${#str1} ]]; then
  echo "${str2} is longer than ${str1}"
else
  echo "${str1} and ${str2} are the same length"
fi

exit 0

5. A program that takes the name of a file and tells you whether it includes the date in the filename in the format DD-MM-YYYY.

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
fi

filename=$1

# This regular expression checks for crazy dates, but it's true that someone could get away with '39-01-2008'
# But I didn't want to make this too complicated!

if [[ $filename =~ [0-3][0-9]-[0-1][0-9]-[0-2][0-9][0-9][0-9] ]]; then
  echo "${filename} contains a valid date"
else
  echo "${filename} does not contain a valid date"
fi

exit 0

Harder ones

1. A script that takes a file full of space-separated words and prints the words separated by line, ordered alphabetically

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -f $1 ]]; then
  echo $1 is not a file
  exit 1
fi

filename=$1

new_string=$(cat "${filename}" | tr ' ' '\n' | sort)

echo "${new_string}"
exit 0

2. A program that takes a file full of comma-separated numbers and prints the largest number

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -f $1 ]]; then
  echo "${1} is not a file"
  exit 1
fi

filename=$1

result=$(cat "${filename}" | tr ',' '\n\' | sort -n | tail -n1)

echo $result

exit 0

3. A program which takes a file of comma-separated first names and delivers a report of the top 5 names by frequency.

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -f $1 ]]; then
  echo "${1} is not a file"
  exit 1
fi

filename=$1

result=$(cat "${filename}" | tr ',' '\n' | sort | uniq -c | sort -nr | head -n5)

# In order to preserve the linebreaks in the $result variable we must wrap it in double quotes
echo "${result}"

exit 0

4. A program that counts the number of files and directories (one level only) in a directory

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide a directory"
  exit 1
fi

dirname="$1"

if [[ ! -d $dirname ]]; then
  echo "${dirname} is not a directory"
  exit 1
fi

# the top line of ls -l is the total count of files, so we cut this off with tail, telling tail to start at the 2nd line of input
# we use wc -l for a wordcount of lines (each line from ls was a filename).

# In Mac I get irritating whitespace around the number outputted by wc,
# so finally I use the tr (translate) command with the -d flat to delete extra spaces

count=$(ls -l "${dirname}" | tail -n+2 | wc -l | tr -d ' ')

echo "${dirname} contains ${count} files/directories"
exit 0

5. A program which counts the number of files in 2 directories and tells you which has the most

#!/bin/bash

# We can use the logical or operator (||) to evaluate two expressions in our if statement
if [[ ! $1 || ! $2 ]]; then
  echo "You must provide 2 directories as arguments"
  exit 1
fi

dir1="$1"
dir2="$2"

if [[ ! -d $dir1 ]]; then
  echo "${dir1} is not a directory"
  exit 1
elif [[ ! -d $dir2 ]]; then
  echo "${dir2} is not a directory"
  exit 1
fi

# See challenge 1 for an explanation of these commands
count1=$(ls -l "${dir1}" | tail -n+2 |  wc -l | tr -d ' ')
count2=$(ls -l "${dir2}" | tail -n+2 | wc -l | tr -d ' ')

# We cannot compare numbers in bash with > or <. We use the -gt, -lt, -eq flags instead
# (bear in mind that the variables $count1 and $count2 are not actually numbers, they are strings. Everything is a string in bash

if [[ $count1 -gt $count2 ]]; then
  echo "${dir1} has the most files (${count1})"
  exit 0
elif [[ $count2 -gt $count2 ]]; then
  echo "${dir2} has the most files (${count2})"
  exit 0
else
  echo "${dir1} and ${dir2} both have ${count1} files"
  exit 0
fi

6. A program that creates a new executable file with the given name

#!/bin/bash

if [[ ! $1  ]]; then
  echo "Please provide a filename to create"
  exit 1
fi

filename="$1"

if [[ -e "${filename}.sh" ]]; then
  echo "${filename}.sh already exists"
  exit 1
fi

# Here we're executing a command and then running some more code based on its completion code (0 or an error)
# Note that we don't need [[ ]] around the touch or chmod commands, [[ ]] is only for bash expressions such as comparing two things

if touch "${filename}.sh"; then
  if chmod u=rwx "${filename}.sh"; then
    echo "#!/bin/bash" >> "${filename}.sh"
    echo "Success! Executable file ${filename}.sh created"
    exit 0
  else
    echo  "Something went wrong!"
    exit 1
  fi
else
  echo "Something went wrong!"
  exit 1
fi

7. A script that tells you the number of characters in a file

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide a file to count"
  exit 1
fi

file="$1"

if [[ ! -f $file ]]; then
  echo "${file} is not a file"
  exit 1
fi

# After cat-ing the file we use tr (translate) to remove any newline or carriage returns, which would be counted by wc
# wc with the -m flag counts characters
# I use the tr again to remove spaces from the output of wc

count=$(cat $file | tr -d '\n\r' | wc -m | tr -d ' ')

echo "${file} contains ${count} characters"
exit 0

8. A script that tells you the difference in length (of total characters) between two files

#!/bin/bash

if [[ ! $1 || ! $2 ]]; then
  echo "Please provide 2 files to check"
  exit 1
fi

file1="$1"
file2="$2"

if [[ ! -f $file1 ]]; then
  echo "${file1} is not a file"
  exit 1
elif [[ ! -f $file2 ]]; then
  echo "${file2} is not a file"
  exit 1
fi

# Same as previous exercise, but repeated on 2 files
count1=$(cat $file1 | tr -d '\n\r' | wc -m | tr -d ' ')
count2=$(cat $file2 | tr -d '\n\r' | wc -m | tr -d ' ')

if [[ $count1 -gt $count2 ]]; then
  echo "${file1} has $(expr $count1 - $count2) more characters than ${file2}"
elif [[ $count2 -gt $count1 ]]; then
  echo "${file2} has $(expr $count2 - $count1) more characters than ${file1}"
else
  echo "${file1} and ${file2} both have ${count1} characters"
fi

exit 0

9. A script that tells you how many files and how many directories are in a given directory.

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -d $1 ]]; then
  echo "${1} is not a directory"
fi

dir=$1

# Tree lists the file structure of a given directory. It can be asked with the -d flag to just list directories

# On a Mac, I use ghead which I installed as part of the coreutils package, becuase the regular head command on Mac doesn't
# let you pass a negative number to the -n flag. This -n-2 tells head to get everything from the 2nd bottom line of the file

dirs=$(tree -L 1 -d "${dir}" | tail -n+2 | ghead -n-2 | wc -l | tr -d ' ')
all=$(tree -L 1 "${dir}" | tail -n+2 | ghead -n-2 | wc -l | tr -d ' ')

# maths can be done using the expr command
files=$(expr $all - $dirs)

echo "${dir} contains ${files} files and ${dirs} directories"
exit 0

10. A script that tells you which file/directory in a given directory is the largest in bytes

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -d $1 ]]; then
  echo "${1} is not a directory"
  exit 1
fi

dir=$1

# Find searches for and lists items in a directory
# -depth 1 tells us to only go 1 level deep, no subdirectories
# -maxdepth 1 would be another option, but it would also list the main directory itself,
# inflating our total by 1

count="$(find "./${dir}" -depth 1 | wc -l)"

if [[ $count -eq 0 ]]; then
  echo "${dir} does not contain any files"
  exit 1
fi

# We use the gdu version of du on Mac for access to the --apparent-size flag
# which tells us specifically how much data the file contains, rather than disk usage

# Reverse numberical sort to get the largest at the top of the list
# Head command gives us the first line of the sorted output
# cut takes the second field (column) from the line - the filename

largest=$(gdu --apparent-size -b "${dir}"/* | sort -nr | head -n1 | cut -f2)

echo "${largest} is the largest file"
exit 0

11. A script that takes a directory and a number (n) and prints the n largest files/directories by bytes in the directory

#!/bin/bash

if [[ ! $1 || ! $2 ]]; then
  echo "Please provide two arguments"
  exit 1
elif [[ ! -d $1 ]]; then
  echo $1 is not a directory
  exit 1

# We test the second argument against a regular expression with the =~ operator

elif [[ ! $2 =~ [0-9]+ ]]; then
  echo Incorrect argument: $2 is not a number
  exit 1
fi

dir=$1
count=$2

list=$(gdu --apparent-size -b "${dir}"/* | sort -nr | head -n $count)

# Double quotes are needed around $list to preserve the newlines in the string

echo "${list}"

exit 0

12. A script that tells you which .js file in a directory is the largest in bytes, and tells you the bytes

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -d $1 ]]; then
  echo "${1} is not a directory"
fi

dir=$1

count=$(find "./${dir}" -depth 1 | wc -l)

if [[ $count -eq 0 ]]; then
  echo "${dir} does not contains any files"
  exit 1
fi

# if the first gdu command fails, we enter the else block and print our own error message
# &>/dev/null redirects any errors or output to a black hole-like directory where they disappear!
# So we don't see the output of the command on our screen

# Note the wildcard * character for saying "Any file ending in .js"

if gdu --apparent-size -b "${dir}"/*.js &>/dev/null; then
  largest=$(gdu --apparent-size -b "${dir}"/*.js | sort -nr | head -n1)
  size=$(echo $largest | cut -f1 -d ' ')
  filename=$(echo $largest | cut -f2 -d ' ')
  echo "${filename} is the largest file at ${size} bytes"
  exit 0
else
  echo "No .js files found"
  exit 1
fi

13. A script that takes 2 directories and tells you how many files/directories of the same name exist in both directories

#!/bin/bash

if [[ ! $1 || ! $2 ]]; then
  echo "Please provide two arguments"
  exit 1
elif [[ ! -d $1 ]]; then
  echo "${1} is not a directory"
elif [[ ! -d $2 ]]; then
   echo "${2} is not a directory"
fi

dir1=$1
dir2=$2

files_1=$(ls -1 "${dir1}")

files_2=$(ls -1 "${dir2}")

# Concatenating two variables
# To add a newline character in a double-quoted string, we can literally just include a linebreak

all_files="${files_1}
${files_2}"

# We must wrap the variable $all_files in double quotes to preserve the newlines when echoing it

total=$(echo "$all_files" | wc -l)
total_without_dupes=$(echo "$all_files" | sort | uniq | wc -l)

diff=$(expr $total - $total_without_dupes)

echo $dir1 and $dir2 have $diff file/directory names in common

exit 0

14. A script that counts how many sub-directories (and further nested directories) are inside a given directory

#!/bin/bash

if [[ ! $1 ]]; then
  echo "Please provide an argument"
  exit 1
elif [[ ! -d $1 ]]; then
  echo $1 is not a directory
  exit 1
fi

dir=$1

count=$(tree -d "${dir}" | ghead -n-2 | tail -n+2 | wc -l)

# wc usually creates an annoying whitespace at the beginnig of its output
# But if we don't quote the argument we pass to echo, then the whitepace in the variable $count
# is not preserved so we don't need to remove it with tr

echo ${dir} contains ${count} nested directories
exit 0

## Input and Output

  1. Create a script that asks a user for a filename and creates a file with this name. It should also ask whether the user wants to have write and execute permission, and update the permissions accordingly. If you ask a user for a Y/N answer to the question "Do you want to have write permissions? Y/N" and they respond with unexpected input, your program should keep asking until it receives input it understands.
#!/bin/bash

read -r -p "Choose a filename: " filename

# Basic checks that file doesn't already exist

if [[ ! $filename ]]; then
  echo "Must provide a filename"
  exit 1
elif [[ -e $filename ]]; then
  echo "${filename} already exists"
  exit 1
fi

touch ${filename}

until [[ $write_permission = 'Y' || $write_permission = 'N' ]]; do
  read -r -p "Would you like the user ($USER) to have write permission? Y/N " write_permission
done

until [[ $exec_permission = 'Y' || $exec_permission = 'N' ]]; do
  read -r -p "Would you like the user ($USER) to have execute permission? Y/N " exec_permission
done

if [[ $write_permission = 'Y' && $exec_permission = 'Y' ]]; then
  chmod u=rwx ${filename}
elif [[ $write_permission = 'Y' && $exec_permission = 'N' ]]; then
  chmod u=rw ${filename}
elif [[ $write_permission = 'N' && $exec_permission = 'Y' ]]; then
  chmod u=rx ${filename}
else
  chmod u=r ${filename}
fi

echo "File ${filename} created and permissions granted"
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment