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.
#!/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
#!/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
#!/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
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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
#!/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
- 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