Shell scripting in macOS – Part 8: Arrays & Dictionaries

This article is a continuation of the previous article. We will be taking the previous script and using it to build on the concepts we will learning in this article.

So far we have been working with a single piece of data. This was stored in a single variable. For each new piece of information we created a new variable. However, we often come across situations where there is more than one value for a given item. This is where collections like arrays and dictionaries come in.

Arrays

An array is a sequential collection of data. It allows us to store more than one value in a variable. This is good for situations like the contents of a folder, list of users, list of applications. Creating, reading, modifying, and iterating over an array is very easy. Let us have a look.

Creating

To create an array simply declare a variable followed by the '=' operator followed by values within round brackets.

# Declaring an array of items 
items=("ABC" "DEF" "GHI" "JKL" "MNO" "PQR")

Note that if you assign another set of values to the items variable it will replace the original values. We will see how to add values to an existing array a little later in the article.

Reading

We need to use the ${ } to read an array. This expands the array and allows us to read different values. There are different operations possible. I have listed some of those in the code snipper below.

# Getting a specific element from the array
echo "\${items[0]} 		= ${items[0]}"

# Getting all the elements of the array
echo "\${items[@]} 		= ${items[@]}"

# Getting the count of the elements in the array
echo "\${#items[@]} 	= ${#items[@]}"

# Getting a range of values
echo "\${items[@]:3:2} 	= ${items[@]:3:2}"

Modifying

To modify we simply need to use the '+' operator before the equals. This will add the value to the existing array without disturbing the other values.

# Pushing a value into the array
items+=("STU")

# Remove a specific item
unset items[2]

A small point to note. The unset is available with /bin/sh interpreter.

Iterating

for entry in "${items[@]}"
do 
	echo "-> $entry"
done

Here is a nice example of the user of arrays. The output of the list command is stored as an array in the variable named ‘directories’. Then using the for loop we can step through each folder and in turn printing its contents out.

directories=($(ls $HOME))

for folder in "${directories[@]}"
do 
	echo $folder
	eval "ls -l $HOME/$folder"
done

Dictionaries

Dictionaries are also collections just like arrays. However, there is one major difference. While arrays are indexed using integers, dictionaries are indexed using strings.

There is one small thing to note about dictionaries. They only work with bash version 4.0 or later. So if you face issues, make sure you are running bash 4.0 or later. In my example I am using zsh version 5.8.1. To find out which version you are running simply run the following command in terminal:

bash --version

or

zsh --version

Let us look at how to create and use dictionaries.

Creating

Creating a dictionary is very easy. We simply declare an associative array and give the dictionary variable a name.

declare -A contactDetails

Modifying

Editing or adding values to a dictionary is easy too. We use the variable name followed by the '[]' index brackets with the key value inside the brackets. This is followed by the '=' operator and the value to be assigned for that key after that.

contactDetails[name]="Arun"
contactDetails[email]="arun@amaranthine.co.in"
contactDetails[website]="www.amaranthine.in"
contactDetails[blog]="www.arunpatwardhan.com"
contactDetails[phone]="+91-9821000000"
contactDetails[dob]="$(date)"

Reading

We use the ‘@{ }’ operator to expand and read values from the dictionary, just as we did with an array. The only additional detail here is that we are using the key in order to get the specific value.

# Getting the value for a specific key
echo ${contactDetails[name]}

# Getting all the values
echo ${contactDetails[@]}

# Getting all the keys
echo ${(k)contactDetails[@]}

# Getting all the values
echo ${(v)contactDetails[@]}

# Getting all the keys and values
echo ${(kv)contactDetails[@]}

# Getting number of entries
echo ${#contactDetails[@]}

Iterating

There are several different ways of iterating over a dictionary. In the example below, the for loop is iterating over all the keys from the dictionary. Inside the loop we are using each key to extract the corresponding value.

for item in "${(k)contactDetails[@]}"
do 
	printf "%-10s \t%-40s" $item ${contactDetails[$item]}
	echo " "
done

Script

Let us update out script to use arrays.

#!/bin/zsh

#-------------------------------------------------------------------------------------------------
#NAME:		Folder creator
#AUTHOR:	Arun Patwardhan
#CONTACT:	arun@amaranthine.co.in
#DATE:		15th September 2022
#WEBSITE:	https://github.com/AmaranthineTech/ShellScripts
#-------------------------------------------------------------------------------------------------

#LEGAL DISCLAIMER --------------------------------------------------------------------------------
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
#-------------------------------------------------------------------------------------------------

#LICENSE/TERMS AND CONDITIONS --------------------------------------------------------------------
#MIT License

#Copyright (c) Amaranthine 2021.

#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:

#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#-------------------------------------------------------------------------------------------------

#ABOUT -------------------------------------------------------------------------------------------
# fileCreator.zsh
# 1.7
#-------------------------------------------------------------------------------------------------

#DESCRIPTION ------------------------------------------------------------------------------------- 
# - THis script is intended for creating the custom folders that are required on all corporate computers. 
# - Run this script on a new computer or a computer being reassigned to another employee.
# - This script can run on all computers.
#-------------------------------------------------------------------------------------------------

#USAGE -------------------------------------------------------------------------------------------
# - To create folders with default names run the command: ./folderCreator.zsh
# - To define your own folder names: ./folderCreator.zsh <folder1> <folder2> <folder3>
# - Available options  : Only the help option is available
# - Getting help       : Use the -h or the -help options to get more information. Or you can use the man command to view the man page.
#-------------------------------------------------------------------------------------------------

#WARNING/CAUTION ---------------------------------------------------------------------------------
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
# This script doesn't perform any validation of the folder names being passed in by the user. 
# If the script does not see the -h or the -help options then it will assume that the data being passed in is the name of the folder.
# The user of the script must ensure that the desired folder names are passed in.
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#-------------------------------------------------------------------------------------------------

#INSTALLATION ------------------------------------------------------------------------------------
# Instructions for placing the script in the correct place are listed here. 
# Location:		/Library/Scripts/
# Permissions:	rwx r-x r-x
#-------------------------------------------------------------------------------------------------

#REQUIREMENTS ------------------------------------------------------------------------------------
# Shell:		/bin/zsh
# OS:			macOS Big Sur 11.4 or later
# Dependencies:	None
#-------------------------------------------------------------------------------------------------

#HELP/SUPPORT ------------------------------------------------------------------------------------
# You can get help by running the following commands.
# ./folderCreator.zsh -h
# ./folderCreator.zsh -help
# OR
# man folderCreator.zsh
# You can also view the log file for the same at: ~/Library/Logs/folderCreator_log_v1-7.log
#-------------------------------------------------------------------------------------------------

#HISTORY -----------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------
# Version 1.0: Basic script which creates the folders
# Version 1.1: Gives user the ability to specify the folder names at run time.
# Version 1.2: Adds safety checks to the scripts
# Version 1.3: Includes documentation as well as ability to get help.
# Version 1.4: Includes optimisation using for loop
# Version 1.5: Prompts the user in the GUI for names for the different folders.
# Version 1.6: Updated the log mechanism with the help of a function and here document.
# Version 1.7: Replaced the folder variables with an array
#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# ------------------------------ SCRIPT STARTS HERE ----------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------

#These are the default values used for the folder names incase the user doesn't provide any.
FOLDERS=("Tools" "Reports" "Help")

#Script version number
VERSION_NUMBER="1.7"

#Command name
COMMAND_NAME="folderCreator.zsh"

#1. Check to see if the user is asking for help. In which case we will have to provide information about the command.
if [[ $1 == "-h" ]] || [[ $1 == "-help" ]]; then
	echo "ABOUT 
-----
fileCreator_v1-7.zsh
Version $VERSION_NUMBER

NAME 
----
$COMMAND_NAME — Folder creation utility SYNOPSIS
$COMMAND_NAME folder names [ verbs ]

DESCRIPTION 
-----------
$COMMAND_NAME creates 3 folders in the home folder. In case the folder names are not provided then the command will create folders with default names 'Tools', 'Reports', \"Help\".

There is also the option of getting help via the help verb.
- This script is intended for creating the custom folders that are required on all corporate computers. 
- Run this script on a new computer or a computer being reassigned to another employee.
- This script can run on all computers.

VERBS 
-----
[ −h −help] Both the options are used to invoke the help documentation.
[ −v −version] Both the options are used to get the version number of the folderCreator command.

REQUIREMENTS 
------------
The following are the minimum requirements to get the script running.
Shell:\t\t zsh
OS:\t\t macOS Big Sur 11.4 or later
Dependencies:\t None

INSTALLATION 
------------
$COMMAND_NAME can be installed anywhere you wish. However, there are certain locations that are recommended.
Location:\t /Library/Scripts/ 
Permissions: \t rwxr-xr-x

USAGE  
-----
$COMMAND_NAME folder1 folder2 folder3 
Will create folders with your own names. 

$COMMAND_NAME -h OR $COMMAND_NAME -help 
Will invoke the help utility.

$COMMAND_NAME -v OR $COMMAND_NAME -version 
will print the version number in stdout.

WARNING/CAUTION  
---------------
$COMMAND_NAME does not perform any validation of names. The only options that folderCreator accepts are -h and -help verbs or the -v and 
-version verbs. If the script does not see the -h , -help or the -v , -version options then it will assume that the data being passed in is 
the name of the folder. The user of the folderCreator command must ensure that the desired folder names are passed in. The user will also be 
prompted, via the graphical user interface, if he/she wishes to provide the names for the folders. If yes, then there will be subsequent 
prompts asking for the folder names.

EXAMPLES 
--------
$COMMAND_NAME Resources Results Assistant
This will create 3 folders Resources , Results , Assistant , in the user’s home folder. 

$COMMAND_NAME
This will create 3 folders with the default names

$COMMAND_NAME Apps
This will use the Apps name for the first folder but the default names for the last 2 folders. 

NOTE
----
The user will be asked if he/she wishes to provide custom names in all the examples mentioned above. The user's value will always override 
whatever is being provided to the script or defaults.

DIAGNOSTICS 
-----------
The script produces a log file called ~/Library/Logs/folderCreator_log_v1-x.log
This file is typically located in the user’s home folder log folder. The x represents the version number of $COMMAND_NAME
You can view the logs for each respective version.

COPYRIGHT  
---------
Copyright (c) Amaranthine 2015-2021. All rights reserved. https://amaranthine.in

EXIT STATUS  
-----------
In most situations, $COMMAND_NAME exits 0 on success"
	exit 0
fi

PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-7.log"

# Function to log activity
function recordActivity() {
	cat << EOF >> $PATH_TO_LOG
[$(date)] $1
EOF
}


echo "$(date) Running script $0 to create folders."
echo ""

TODAY=$(date)

recordActivity "Starting"

#2. Check to see if the version number is 
if [[ $1 == "-version" ]] || [[ $1 == "-v" ]]; then
	echo "Version: $VERSION_NUMBER"
	exit 0
fi

#3. The following if statements check to see if the script is receiving any arguments. It then picks those arguments and assigns them to the respective variables for use in the script.
if [[ $1 != "" ]]; then
	FOLDERS[0]=$1
fi

if [[ $2 != "" ]]; then
	FOLDERS[1]=$2
fi

if [[ $3 != "" ]]; then
	FOLDERS[2]=$3
fi

#4. We can prompt the user to see if they wish to provide folder names themselves. This will override any values provided as arguments.
userClicked=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to provide names for the folders or use the defaults instead?" buttons {"Custom", "Default"} default button 2 with icon POSIX file "/System/Library/CoreServices/HelpViewer.app/Contents/Resources/AppIcon.icns")')
	
# if the user decides to provide custom names then go ahead and ask the user via GUI prompts. Otherwise use the values sent as arguments or defaults.	
if [[ $userClicked == "Custom" ]]; then
	recordActivity "The user decided to provide custom names."
	
	FOLDERS[0]=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 1" default answer "Utilities" buttons {"OK"} default button 1 with title "Folder that will hold the utilities" with icon POSIX file "/Users/Shared/Finder.icns")')
	
	FOLDERS[1]=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 2" default answer "Tools" buttons {"OK"} default button 1 with title "Folder that will hold the tools" with icon POSIX file "/Users/Shared/Finder.icns")')
	
	FOLDERS[2]=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 3" default answer "Help" buttons {"OK"} default button 1 with title "Folder that will hold the support documents" with icon POSIX file "/Users/Shared/Finder.icns")')
		
	recordActivity "User provided: ${FOLDER[@]}"
else
	recordActivity "User decided to use default values: ${FOLDER[@]}"
fi

#5. Go to the home folder.
cd $HOME

#6. Check to see if each of the folders exists. If it exists then do not create it. Else create the folder. 
recordActivity "Creating folders: ${FOLDER[@]}"

for item in ${FOLDER[@]}; do
	if [[ -d $item ]]; then
		recordActivity "Not creating $item as it already exists."
	else
		recordActivity "Creating $item"
		mkdir $item
	fi
	
	#7. Create the task completion file inside each folder.
	recordActivity "Creating hidden file for $item folder."
	cd $item
	#8. Generate the file names based on the folder names.
	touch ".$item-FolderCreated"
	cd ..
done

echo "$(date) Task completed. Have a nice day!"
	
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ END OF SCRIPT ---------------------------------------------------

Summary

Both arrays and dictionaries now allow us to store collections of data in a single variable. This enables us to write compact scripts and deal with complex data.

Download

You can download the completed script from here.

Advertisement

Shell scripting in macOS – Part 7: Miscellaneous

This article is a continuation of the previous article. We will be taking the previous script and using it to build on the concepts we will learning in this article.

We will be covering a few different features, available in shell scripting, in this article.

Functions

Often times, you will find that there are some operations that you perform repeatedly across different points in the script. It would be extremely useful to write this logic once and reuse it over and over in a quick and efficient manner. Functions allow us to do just that.

#!/bin/bash

#function syntx ----------
repeat() {
	echo "Function without the function keyword"
}

repeat 

#function with function keyword ----------
function message() {
	echo "The argument is $1"
}

message "Arun"

#function with a local variable
#--------------------------------------------------
function localVar() {
	local value="ABC"
	echo $value
}

localVar 

#function with an argument being passed in
#--------------------------------------------------
function report() {
	echo "Argument passed in: $1"
}

report "Value 1"

function argsParameters() {
	echo "\$# -> Number of arguments"
	echo "\$* -> All positional arguments as a single word"
	echo "\$@ -> All positional arguments as separate strings"
	echo "\$1 -> First argument"
	echo "\$_ -> last argument of previous command"
}

argsParameters 

#function returning value
#--------------------------------------------------
function operation() {
	echo "XYZ"
}

answer="$(operation)"
echo $answer

function retCode() {
	echo "Return code"
	return 10
}

retCode 
echo $?

Environment

#!/bin/bash

#list environment variables
echo "Print environment variables"
echo "--------------------------------------------------"
printenv 
echo ""


#print specific environment variable value
echo "Print specific environment variables"
echo "--------------------------------------------------"
printenv SHELL
printenv USER
printenv LOGNAME
printenv HOME
echo ""

#path to the printenv command
echo "Print path to printenv command"
echo "--------------------------------------------------"
which printenv

The output would look like:

Print environment variables
--------------------------------------------------
CR_RUNID=19455
TERM_PROGRAM=CodeRunner
CR_SANDBOXED=1
TERM=dumb
SHELL=/bin/zsh
TMPDIR=/var/folders/ts/gm470rbx4xv0t507dt7xmj7c0000gn/T/com.coderunnerapp.CodeRunner/
CR_INPUT=
CR_SCRIPTS_DIR=/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data/Library/Application Support/CodeRunner/Languages/Shell Script.crLanguage/Scripts
USER=arunpatwardhan
COMMAND_MODE=unix2003
SSH_AUTH_SOCK=/private/tmp/com.apple.launchd.P9J71uVoN9/Listeners
filename=envDemo.sh
__CF_USER_TEXT_ENCODING=0x1F5:0x0:0x0
CR_DEVELOPER_DIR=/Applications/CodeRunner.app/Contents/SharedSupport/Developer
CR_UNSAVED_DIR=/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data/Library/Application Support/CodeRunner/Unsaved
CR_LANGUAGE_DIR=/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data/Library/Application Support/CodeRunner/Languages/Shell Script.crLanguage
CR_ENCODING_NAME=utf-8
CR_FILENAME=envDemo.sh
PATH=/Applications/Xcode.app/Contents/Developer:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/Library/Apple/usr/bin:/Applications/CodeRunner.app/Contents/SharedSupport/Developer/bin
__CFBundleIdentifier=com.coderunnerapp.CodeRunner
PWD=/Users/arunpatwardhan/Developer
APP_SANDBOX_CONTAINER_ID=com.coderunnerapp.CodeRunner
CFFIXED_USER_HOME=/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data
CR_FILE=/Users/arunpatwardhan/Developer/envDemo.sh
XPC_FLAGS=0x0
CR_TMPDIR=/var/folders/ts/gm470rbx4xv0t507dt7xmj7c0000gn/T/com.coderunnerapp.CodeRunner/CodeRunner
XPC_SERVICE_NAME=0
SHLVL=2
HOME=/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data
CR_SUGGESTED_OUTPUT_FILE=/var/folders/ts/gm470rbx4xv0t507dt7xmj7c0000gn/T/com.coderunnerapp.CodeRunner/CodeRunner/envDemo
CR_VERSION=62959
LOGNAME=arunpatwardhan
LC_CTYPE=UTF-8
CR_RUN_COMMAND=bash "$filename"
compiler=
CR_ENCODING=4
_=/usr/bin/printenv

Print specific environment variables
--------------------------------------------------
/bin/zsh
arunpatwardhan
arunpatwardhan
/Users/arunpatwardhan/Library/Containers/com.coderunnerapp.CodeRunner/Data

Print path to printenv command
--------------------------------------------------
/usr/bin/printenv

Redirection

We have already covered a little bit of redirection in an earlier article. There are some more redirection options available that we will look at out here.

OperatorDescriptionExample
>Writes the output of the preceding command to the fileecho "ABC" > file
>>Appends information to the file being pointed to another fileecho "ABC" >> file
|Passes the output of the preceding command to the next commandls -l | grep "*.sh"

Using the above redirections there are some interesting actions that we can perform.

ActionDescription
command >> /dev/nullThis will completely discard the output of the command.
command 2>&1This will redirect stderr to stdout and show both together on stdout.
command 1>&2This will redirect stdout to stderr and show both together on stderr.

Here document

One interesting application fo the redirection operator is the concept of here documents. A here document is used to send multiple lines of input to a command. The general structure is:

command << endOfMessageFlag
message
message
message
endOfMessageFlag

In this case the endOfMessageFlag is used to inform the command that the message has come to an end. A popular example is ‘EOF’ but any text can be used. Here are some examples of here documents.

#Writing to a file
cat << EOF >> /Users/Shared/temp.log
"This is a demo "
$(date)
EOF

The above script write the message within the ‘EOF’ to the file: /Users/Shared/temp.log. The message being:

This is a demo. 
Mon Sep 25 12:31:07 IST 2022

Here is another example:

#Multiple statements to a command
osascript << EOF
display dialog "Would you like to provide names for the folders or use the defaults instead?" buttons {"Custom", "Default"} default button 2 with icon POSIX file "/System/Library/CoreServices/HelpViewer.app/Contents/Resources/AppIcon.icns"
text returned of (display dialog "Enter the name of folder 1" default answer "Utilities" buttons {"OK"} default button 1 with title "Folder that will hold the utilities" with icon POSIX file "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/AlertStopIcon.icns")
EOF

The ‘here’ document allows us to send multiple AppleScript statements to ‘osascript‘.

Folder creator script update

Let us try to use some of these features in our folder creator script.

#!/bin/zsh

#-------------------------------------------------------------------------------------------------
#NAME:		Folder creator
#AUTHOR:	Arun Patwardhan
#CONTACT:	arun@amaranthine.co.in
#DATE:		15th September 2022
#WEBSITE:	https://github.com/AmaranthineTech/ShellScripts
#-------------------------------------------------------------------------------------------------

#LEGAL DISCLAIMER --------------------------------------------------------------------------------
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
#SOFTWARE.
#-------------------------------------------------------------------------------------------------

#LICENSE/TERMS AND CONDITIONS --------------------------------------------------------------------
#MIT License

#Copyright (c) Amaranthine 2021.

#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is
#furnished to do so, subject to the following conditions:

#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#-------------------------------------------------------------------------------------------------

#ABOUT -------------------------------------------------------------------------------------------
# fileCreator.zsh
# 1.6
#-------------------------------------------------------------------------------------------------

#DESCRIPTION ------------------------------------------------------------------------------------- 
# - THis script is intended for creating the custom folders that are required on all corporate computers. 
# - Run this script on a new computer or a computer being reassigned to another employee.
# - This script can run on all computers.
#-------------------------------------------------------------------------------------------------

#USAGE -------------------------------------------------------------------------------------------
# - To create folders with default names run the command: ./folderCreator.zsh
# - To define your own folder names: ./folderCreator.zsh <folder1> <folder2> <folder3>
# - Available options  : Only the help option is available
# - Getting help       : Use the -h or the -help options to get more information. Or you can use the man command to view the man page.
#-------------------------------------------------------------------------------------------------

#WARNING/CAUTION ---------------------------------------------------------------------------------
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
# This script doesn't perform any validation of the folder names being passed in by the user. 
# If the script does not see the -h or the -help options then it will assume that the data being passed in is the name of the folder.
# The user of the script must ensure that the desired folder names are passed in.
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#-------------------------------------------------------------------------------------------------

#INSTALLATION ------------------------------------------------------------------------------------
# Instructions for placing the script in the correct place are listed here. 
# Location:		/Library/Scripts/
# Permissions:	rwx r-x r-x
#-------------------------------------------------------------------------------------------------

#REQUIREMENTS ------------------------------------------------------------------------------------
# Shell:		/bin/zsh
# OS:			macOS Big Sur 11.4 or later
# Dependencies:	None
#-------------------------------------------------------------------------------------------------

#HELP/SUPPORT ------------------------------------------------------------------------------------
# You can get help by running the following commands.
# ./folderCreator.zsh -h
# ./folderCreator.zsh -help
# OR
# man folderCreator.zsh
# You can also view the log file for the same at: ~/Library/Logs/folderCreator_log_v1-6.log
#-------------------------------------------------------------------------------------------------

#HISTORY -----------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------
# Version 1.0: Basic script which creates the folders
# Version 1.1: Gives user the ability to specify the folder names at run time.
# Version 1.2: Adds safety checks to the scripts
# Version 1.3: Includes documentation as well as ability to get help.
# Version 1.4: Includes optimisation using for loop
# Version 1.5: Prompts the user in the GUI for names for the different folders.
# Version 1.6: Updated the log mechanism with the help of a function and here document.
#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# ------------------------------ SCRIPT STARTS HERE ----------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------

#These are the default values used for the folder names incase the user doesn't provide any.
TOOLS_FOLDER="Tools"
REPORTS_FOLDER="Reports"
HELP_FOLDER="Help"

#Script version number
VERSION_NUMBER="1.6"

#Command name
COMMAND_NAME="folderCreator.zsh"

#1. Check to see if the user is asking for help. In which case we will have to provide information about the command.
if [[ $1 == "-h" ]] || [[ $1 == "-help" ]]; then
	echo "ABOUT 
-----
fileCreator_v1-6.zsh
Version $VERSION_NUMBER

NAME 
----
$COMMAND_NAME — Folder creation utility SYNOPSIS
$COMMAND_NAME folder names [ verbs ]

DESCRIPTION 
-----------
$COMMAND_NAME creates 3 folders in the home folder. In case the folder names are not provided then the command will create folders with default names 'Tools', 'Reports', \"Help\".

There is also the option of getting help via the help verb.
- This script is intended for creating the custom folders that are required on all corporate computers. 
- Run this script on a new computer or a computer being reassigned to another employee.
- This script can run on all computers.

VERBS 
-----
[ −h −help] Both the options are used to invoke the help documentation.
[ −v −version] Both the options are used to get the version number of the folderCreator command.

REQUIREMENTS 
------------
The following are the minimum requirements to get the script running.
Shell:\t\t zsh
OS:\t\t macOS Big Sur 11.4 or later
Dependencies:\t None

INSTALLATION 
------------
$COMMAND_NAME can be installed anywhere you wish. However, there are certain locations that are recommended.
Location:\t /Library/Scripts/ 
Permissions: \t rwxr-xr-x

USAGE  
-----
$COMMAND_NAME folder1 folder2 folder3 
Will create folders with your own names. 

$COMMAND_NAME -h OR $COMMAND_NAME -help 
Will invoke the help utility.

$COMMAND_NAME -v OR $COMMAND_NAME -version 
will print the version number in stdout.

WARNING/CAUTION  
---------------
$COMMAND_NAME does not perform any validation of names. The only options that folderCreator accepts are -h and -help verbs or the -v and 
-version verbs. If the script does not see the -h , -help or the -v , -version options then it will assume that the data being passed in is 
the name of the folder. The user of the folderCreator command must ensure that the desired folder names are passed in. The user will also be 
prompted, via the graphical user interface, if he/she wishes to provide the names for the folders. If yes, then there will be subsequent 
prompts asking for the folder names.

EXAMPLES 
--------
$COMMAND_NAME Resources Results Assistant
This will create 3 folders Resources , Results , Assistant , in the user’s home folder. 

$COMMAND_NAME
This will create 3 folders with the default names

$COMMAND_NAME Apps
This will use the Apps name for the first folder but the default names for the last 2 folders. 

NOTE
----
The user will be asked if he/she wishes to provide custom names in all the examples mentioned above. The user's value will always override 
whatever is being provided to the script or defaults.

DIAGNOSTICS 
-----------
The script produces a log file called ~/Library/Logs/folderCreator_log_v1-x.log
This file is typically located in the user’s home folder log folder. The x represents the version number of $COMMAND_NAME
You can view the logs for each respective version.

COPYRIGHT  
---------
Copyright (c) Amaranthine 2015-2021. All rights reserved. https://amaranthine.in

EXIT STATUS  
-----------
In most situations, $COMMAND_NAME exits 0 on success"
	exit 0
fi

PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-6.log"

# Function to log activity
function recordActivity() {
	cat << EOF >> $PATH_TO_LOG
[$(date)] $1
EOF
}


echo "$(date) Running script $0 to create folders."
echo ""

TODAY=$(date)

recordActivity "Starting"

#2. Check to see if the version number is 
if [[ $1 == "-version" ]] || [[ $1 == "-v" ]]; then
	echo "Version: $VERSION_NUMBER"
	exit 0
fi

#3. The following if statements check to see if the script is receiving any arguments. It then picks those arguments and assigns them to the respective variables for use in the script.
if [[ $1 != "" ]]; then
	TOOLS_FOLDER=$1
fi

if [[ $2 != "" ]]; then
	REPORTS_FOLDER=$2
fi

if [[ $3 != "" ]]; then
	HELP_FOLDER=$3
fi

#4. We can prompt the user to see if they wish to provide folder names themselves. This will override any values provided as arguments.
userClicked=$(/usr/bin/osascript -e 'button returned of (display dialog "Would you like to provide names for the folders or use the defaults instead?" buttons {"Custom", "Default"} default button 2 with icon POSIX file "/System/Library/CoreServices/HelpViewer.app/Contents/Resources/AppIcon.icns")')
	
# if the user decides to provide custom names then go ahead and ask the user via GUI prompts. Otherwise use the values sent as arguments or defaults.	
if [[ $userClicked == "Custom" ]]; then
	recordActivity "The user decided to provide custom names."
	
	TOOLS_FOLDER=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 1" default answer "Utilities" buttons {"OK"} default button 1 with title "Folder that will hold the utilities" with icon POSIX file "/Users/Shared/Finder.icns")')
	
	REPORTS_FOLDER=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 2" default answer "Tools" buttons {"OK"} default button 1 with title "Folder that will hold the tools" with icon POSIX file "/Users/Shared/Finder.icns")')
	
	HELP_FOLDER=$(/usr/bin/osascript -e 'text returned of (display dialog "Enter the name of folder 3" default answer "Help" buttons {"OK"} default button 1 with title "Folder that will hold the support documents" with icon POSIX file "/Users/Shared/Finder.icns")')
		
	recordActivity "User provided: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER"
else
	recordActivity "User decided to use default values: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER"
fi

#5. Go to the home folder.
cd $HOME

#6. Check to see if each of the folders exists. If it exists then do not create it. Else create the folder. 
recordActivity "Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER"

for item in $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER; do
	if [[ -d $item ]]; then
		recordActivity "Not creating $item as it already exists."
	else
		recordActivity "Creating $item"
		mkdir $item
	fi
	
	#7. Create the task completion file inside each folder.
	recordActivity "Creating hidden file for $item folder."
	cd $item
	#8. Generate the file names based on the folder names.
	touch ".$item-FolderCreated"
	cd ..
done

echo "$(date) Task completed. Have a nice day!"
	
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ END OF SCRIPT ---------------------------------------------------

One of the big advantages with using a function and a here document to generate log files is that we can change the format and structure simply by modifying the function. The message itself remains unique.

We have seen some really interesting features in this article. In the next article we will take scripting a little further by exploring Arrays and dictionaries

Download

You can download the completed script from here.

Shell scripting in macOS – Part 2: Managing information

This article is a continuation of the previous article. We will be taking the previous script and using it to build on the concepts we will learning in this article.

Using Variables to store information

First up we will look at variable. Variables are containers that can hold information. The biggest advantage of this is the fact that we can use information in our tasks simply by reusing the variable it is stored in. This means if there is any change at a later date, then we only have to change the value in the variable. 

So, in the future, if there is a need to modify the information, we only have a single point of change to make. This greatly aids  in the ease of maintenance of the code.

It also makes the script more readable.

NOTE: The value of a variable can be changed at a later point of time within the script. 

Creating variables is very easy. You simply declare a name and assign it a value using the = operator. For example, if we are going to be using the path to the logs folder then storing it in a variable called PATH_TO_LOGS makes sense. We would then follow it up with the = sign and follow that up with the path in quotes. 

PATH_TO_LOGS=“/Library/Logs/“

To use this variable in a command we would simple callout the name with the $ symbol prefixed before it. 

echo $PATH_TO_LOGS

The $ symbol is necessary to access the value being held by the container.

While declaring variables try to use names which explain the purpose of the variable.

Built in variables

We can see that it is very easy to define our own variables. However, we are not restricted to creating our own variables. The system provides us with predefined variables. These give us access to useful information such as:

  • Path to the current user’s home folder.
  • The shell interpreter being used.
  • The currently logged in user name. 

We can get the complete list of commands with the help of the printenv command.

printenv

How about using these variables? Well, we will use it the same way we would use our own variables. Just prefix the $ symbol before the variable name. 

echo "The path to the home folder is $HOME"

Let us update the script from the previous article.

#!/bin/zsh

echo "Running script to create folders."

TOOLS_FOLDER="Tools"
REPORTS_FOLDER="Reports"
HELP_FOLDER="Help"

TOOLS_FOLDER_CREATED=".$TOOLS_FOLDER-FolderCreated"
REPORTS_FOLDER_CREATED=".$REPORTS_FOLDER-FolderCreated"
HELP_FOLDER_CREATED=".$HELP_FOLDER-FolderCreated"

cd $HOME

echo "Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER"
mkdir $TOOLS_FOLDER
mkdir $REPORTS_FOLDER
mkdir $HELP_FOLDER

echo "Creating hidden file for $TOOLS_FOLDER folder."
cd $TOOLS_FOLDER
touch $TOOLS_FOLDER_CREATED
cd ..

echo "Creating hidden file for $REPORTS_FOLDER folder."
cd $REPORTS_FOLDER
touch $REPORTS_FOLDER_CREATED
cd ..

echo "Creating hidden file for $HELP_FOLDER folder."
cd $HELP_FOLDER
touch $HELP_FOLDER_CREATED
cd ..

echo "Task completed. Have a nice day!"

Capturing command output

Now that we have seen how variables can be created and used, then next logical step is to use them to store the outcome of a command. Why would we need to do this? Let us suppose that a command returns the path to a folder and we would like to perform multiple tasks on this folder. We can simply save the path in a variable and then use the variable across the script. 

If storing the result of the command in a variable wasn’t possible then we would have to execute the command over and over again every time we needed the result.

But before we store the outcome of the command we first need to understand how we can capture the output of a command itself. This is done with the help of command substitution. The command to be executed is placed within the $ symbol followed by parentheses.

So to store it in a variable we would just place the command we would just place this on he right hand side of the = sign. For example, if we wanted to store today’s date we would use the date command placed within the $() on the right hand side of the = sign. On the left hand side of the = sign would be the name of our variable.

TODAY=$(date)

There is an older way of doing the same thing, instead of using the $() the command would be placed within 2 back ticks.

TODAY=`date`

Writing to files

While it is useful to store information within variables there are some limitation with this. Sometimes we would like to store our data outside the script for example on some other file. The advantage with this approach is that it allows us to access the information across multiple invocations of the script. 

The way we write to a file is by redirecting the output of the command from standard output to a file. There are 2 operators that help us with this.

The redirect operator with a single angle bracket will write the contents to a file. This will replace the existing content fo the file.

echo "Hello, World!" > /Users/Shared/message.txt

The redirect operator with 2 angle brackets will also write contents to a file. But this will append or add the existing content. 

echo "Hello, World!" >> /Users/Shared/message.txt

Depending on what you want you can use one of the 2 approaches. 

Logging events taking place in the script

A log file is used to note done certain events being performed by an app, script, process, or any task. It is a very useful troubleshooting tool. This would be a nice feature to add to our script. We can log the different events that are taking place. To do this we will use the same redirect operator to write to a file.

Log files are typically stored in one of two locations in macOS:

  • ~/Library/Logs/
  • /Library/Logs

For our demo we will store it in the ~/Library/Logs/ folder. This makes sense because our script will be making changes to a user’s home folder. So ideally, the log file should also stay in the user’s home folder.

The way we will generate our log file is by redirecting the output of the echo command to our file.

echo "Hello, World!" >> ~/Library/Logs/folderCreator_log_v1-1.log

So all the echo statements we have will be modified to redirect to the log. Additionally, we will use command substitution to include the date and time in out message. Let us modify the script above to reflect these new changes.

#!/bin/zsh

echo "$(date) Running script to create folders."

TOOLS_FOLDER="Tools"
REPORTS_FOLDER="Reports"
HELP_FOLDER="Help"

TOOLS_FOLDER_CREATED=".$TOOLS_FOLDER-FolderCreated"
REPORTS_FOLDER_CREATED=".$REPORTS_FOLDER-FolderCreated"
HELP_FOLDER_CREATED=".$HELP_FOLDER-FolderCreated"

TODAY=$(date)
PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-1.log"

echo "$(date) Starting" >> $PATH_TO_LOG

cd $HOME

echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG
mkdir $TOOLS_FOLDER
mkdir $REPORTS_FOLDER
mkdir $HELP_FOLDER

echo "$(date) Creating hidden file for $TOOLS_FOLDER folder." >> $PATH_TO_LOG
cd $TOOLS_FOLDER
touch $TOOLS_FOLDER_CREATED
cd ..

echo "$(date) Creating hidden file for $REPORTS_FOLDER folder." >> $PATH_TO_LOG
cd $REPORTS_FOLDER
touch $REPORTS_FOLDER_CREATED
cd ..

echo "$(date) Creating hidden file for $HELP_FOLDER folder." >> $PATH_TO_LOG
cd $HELP_FOLDER
touch $HELP_FOLDER_CREATED
cd ..

echo "$(date) Task completed. Have a nice day!"

Passing information to a script

While storing information and capturing information within a script is useful. It is also useful to have the ability to give information to a script at the time of running the script. This allows the user of the script to have greater control over the end result or outcome. 

The information that is passed into the script is store in predefined variables known as positional variables. They are named $0, $1, $2 and onwards. Let us modify the script to use these variables.

#!/bin/zsh

echo "$(date) Running script $0 to create folders."

TOOLS_FOLDER=$1
REPORTS_FOLDER=$2
HELP_FOLDER=$3

TOOLS_FOLDER_CREATED=".$TOOLS_FOLDER-FolderCreated"
REPORTS_FOLDER_CREATED=".$REPORTS_FOLDER-FolderCreated"
HELP_FOLDER_CREATED=".$HELP_FOLDER-FolderCreated"

TODAY=$(date)
PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-1.log"

echo "$(date) Starting" >> $PATH_TO_LOG

cd $HOME

echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG
mkdir $TOOLS_FOLDER
mkdir $REPORTS_FOLDER
mkdir $HELP_FOLDER

echo "$(date) Creating hidden file for $TOOLS_FOLDER folder." >> $PATH_TO_LOG
cd $TOOLS_FOLDER
touch $TOOLS_FOLDER_CREATED
cd ..

echo "$(date) Creating hidden file for $REPORTS_FOLDER folder." >> $PATH_TO_LOG
cd $REPORTS_FOLDER
touch $REPORTS_FOLDER_CREATED
cd ..

echo "$(date) Creating hidden file for $HELP_FOLDER folder." >> $PATH_TO_LOG
cd $HELP_FOLDER
touch $HELP_FOLDER_CREATED
cd ..

echo "$(date) Task completed. Have a nice day!"

The final script should look like:

#!/bin/zsh
echo "$(date) Running script $0 to create folders."
TOOLS_FOLDER=$1
REPORTS_FOLDER=$2
HELP_FOLDER=$3
TOOLS_FOLDER_CREATED=".$TOOLS_FOLDER-FolderCreated"
REPORTS_FOLDER_CREATED=".$REPORTS_FOLDER-FolderCreated"
HELP_FOLDER_CREATED=".$HELP_FOLDER-FolderCreated"
TODAY=$(date)
PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-1.log"
echo "$(date) Starting" >> $PATH_TO_LOG
cd $HOME
echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG
mkdir $TOOLS_FOLDER
mkdir $REPORTS_FOLDER
mkdir $HELP_FOLDER
echo "$(date) Creating hidden file for $TOOLS_FOLDER folder." >> $PATH_TO_LOG
cd $TOOLS_FOLDER
touch $TOOLS_FOLDER_CREATED
cd ..
echo "$(date) Creating hidden file for $REPORTS_FOLDER folder." >> $PATH_TO_LOG
cd $REPORTS_FOLDER
touch $REPORTS_FOLDER_CREATED
cd ..
echo "$(date) Creating hidden file for $HELP_FOLDER folder." >> $PATH_TO_LOG
cd $HELP_FOLDER
touch $HELP_FOLDER_CREATED
cd ..
echo "$(date) Task completed. Have a nice day!"

Script locations

One last thing to talk about now is script locations. So far we have been placing our scripts where ever we wish and running them from there. But it may be a good idea to use a consistent location for the same. There are several candidates for this:

  • ~/Library/Scripts/
  • /Library/Scripts/

These are the more standard locations.

The only decision that needs to be made is whether it is the Library folder in the user’s home folder or the library folder located at root. This affects if the script is available only for a specific user or for all users on a computer.

There are other locations possible too. Developers often have a folder in the home folder called “Developer”. This needs to be manually created, but once created the system recognises it as the folder where files related to development are kept. You can create a scripts folder and place it in there.

Another popular location is the Application Support folder within the library folder. You can create a folder that represents items related to your scripts and then place the script in that folder. Note that these folders will have to be created by manually.

  • ~/Developer/Scripts/
  • ~/Library/Application Support/<your folder>/

These 2 locations would need to be created.

Scripts are not typically exposed to the end user. There is typically some kind of scheduling mechanism that triggers them. However, if a script is designed to be used by the end user you could even place them in:

  • /Applications/Scripts/
  • ~/Applications/Scripts/

Like the developer folder the applications folder in the home folder needs to be created. But once created the system recognises what it is intended for and gives it special privileges. The scripts folder within it will have to be created manually.

While this may not seem like a big deal. Placing your scripts in the correct location can lead to more consistent experiences, make troubleshooting easy, and also hide potential complexity.

Conclusion

The ability to store data within a script, pass data to a script or store data on an external file from within a script has several advantages. This makes the script more power and compact at the same time. It also makes the script less susceptible to errors and mistakes.

Video

Download

You can download the script from the same git repository as the previous one. The script is named folderCreator_v1-1.zsh.