Shell scripting in macOS – Part 10: Distribution

This is the last article in the series of scripts. We will take the script from Part 8 and continue with it. We will be taking our folderCreator script and using it in our distribution process.

Distribution is the process of placing the script along with any supporting resources on a destination computer making it easily available for use. There are many aspects to this process, but we are going to look at a few of them:

  • Where should our script be installed?
  • How will the end user be invoking the script?
  • Does the script have its own man page?

Creating our own commands from other shell scripts

You may have noticed that whenever we use commands like defaults, echo, ls, cd, … we do not have to specify the path to the command. There is a reason for that. When we run our script the shell interpreter ‘knows’ where to go and look for these commands. There are some predefined locations available. We can get these predefined locations from the environment.

printenv PATH

This gives us an output that looks like:

/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

The output you see will vary from computer to computer. As we can see it is a list of folders separated by a ‘:’. These are the folders the interpreter will go through to locate the commands we use.

While we do not specify the path to a command in our script it is actually considered good practice. In fact, we will be making the same change to our script.

So if we want our own commands to be located easily it should be placed in one of these folders.

Installing commands

While installing commands there is one feature of macOS that we need to keep in mind: SIP (System Integrity Protection). Introduced in OS X 10.11 El Capitan, SIP prevents the modification of certain system files or folders even if you have root privileges. So this means that we do not have a lot of choice when it comes to installing our command. We will place it in /usr/local/bin folder.

Here is the final version of the folderCreator script. I have added paths to some commands and have kept the file name as folderCreator. The version number has been removed from the file name.

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

#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-8.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
# Version 1.8: Added absolute path for commands. Final version ready for deployment.
#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# ------------------------------ 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.8"

#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.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:			zsh
OS:				macOS Big Sur 11.4 or later
Dependencies:	None

INSTALLATION 
------------
$COMMAND_NAME can be installed anywhere you wish. However, there are certain locations that are recommended.
Location:		/Library/Scripts/ 
Permissions:		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-8.log"

# Function to log activity
function recordActivity() {
	/bin/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"
		/bin/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.
	/usr/bin/touch ".$item-FolderCreated"
	cd ..
done

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

In order to run this command we will have to give it execute capabilities.

chmod 755 folderCreator

This should change the icon of the script to that of an executable. If it doesn’t then go ahead and remove the extension from the file.

Copy the file to the /usr/local/bin folder. You will need to authenticate as admin to do this. There may be other executables in this folder.

Using commands

Now let us test to see if this has worked. Run the command:

folderCreator -h

We can use the which command to confirm that the correct binary is being used.

which folderCreator

Adding path to the environment

Now, it is not necessary that the executables we create should be placed in one of the standard PATH folders. We could place it anywhere else. All we would have to do is to export this new folder path to the PATH environment variable. There are a few ways of doing this.

  1. Use the export command
  2. Source another script file into your script
  3. Automatically configure bash/zsh to source the export command
Using the export command

The export command temporarily adds a value to the PATH environment variable. We could do this at the start of the script. Let us look at this as an example.

  1. Create a script, message.bash and save it in the /Users/Shared/Scripts/.
#!/bin/bash

echo "$(date) This is a random script $RANDOM"
  1. Create another script called test.bash and save it where ever you want.
#!/bin/bash

export DEVELOPER_PATH=/Users/Shared/Scripts/
export PATH=$DEVELOPER_PATH:$PATH

bash welcome.bash

which welcome.bash
  1. Run the script with the command: bash test.bash. Don’t forget to put the path when using the script.

The 2 export commands are setting the value of the PATH environment variable to the new path. Note that while doing that we still keep the existing PATH value. So the environment will contain all the existing PATHS as well as the new path.

We can immediately see the benefit of adding another folder to our PATH component. The commands on line 6 and 8 do not require the path to the welcome.bash script to be explicitly mentioned. Even though they are not in the standard search paths. In fact, any script/executable that is placed in that folder will now be directly accessible without having to specify the absolute path. Having said that it is still a good idea to put the absolute path to a command.

An important thing to keep in mind is that this change only applies to the script that we are running. This will not impact other shells or other scripts running in the same shell. The next 2 options will show us how we could possibly do that.

Source the export commands from another script

If the export commands are needed in more than one script then it might be a good idea to source them instead of rewriting them over and over again.

Start off by creating a new file called newPath.bash. It should only have the 2 export commands in them.

#!/bin/bash

export DEVELOPER_PATH=/Users/Shared/Scripts/
export PATH=$DEVELOPER_PATH:$PATH

Now we will modify our test.bash script from the previous example as shown below.

#!/bin/bash
#Using source

source /Users/Shared/newPath.bash

bash welcome.bash

which welcome.bash

You can see that we simply source the original script in here. This is extremely useful if there are multiple paths that need to be added to multiple scripts. Any change in the path only has to be made in one place making this approach far more convenient and scalable. There is still the same catch. Any change to the path is only applicable to the scope of the script. Other scripts and the shell environment itself does not get affected.

Configure bash/zsh to source our export commands.

While the pervious 2 approaches are good, they have the limitation that the changes are only applicable to the script where the sourcing is done. All the other scripts and the shell itself are not affected by it. Now this maybe a desired outcome. There are situations where you would want this to be applicable globally to all the scripts and the shell itself without having to change the PATH value manually by ourselves. This can be done by changing the scripts that are invoked when the shell is loaded.

The file that is to be invoked is located in the /etc folder. It is called zshrc. If you want to make a change to bash shell then you need to modify bashrc. Copy this file to your desktop. Change it as shown below.

# System-wide profile for interactive zsh(1) shells.

# Setup user specific overrides for this in ~/.zshrc. See zshbuiltins(1)
# and zshoptions(1) for more details.

# Correctly display UTF-8 with combining characters.
if [[ "$(locale LC_CTYPE)" == "UTF-8" ]]; then
    setopt COMBINING_CHARS
fi

# Disable the log builtin, so we don't conflict with /usr/bin/log
disable log

# Save command history
HISTFILE=${ZDOTDIR:-$HOME}/.zsh_history
HISTSIZE=2000
SAVEHIST=1000

# Beep on error
setopt BEEP

# Use keycodes (generated via zkbd) if present, otherwise fallback on
# values from terminfo
if [[ -r ${ZDOTDIR:-$HOME}/.zkbd/${TERM}-${VENDOR} ]] ; then
    source ${ZDOTDIR:-$HOME}/.zkbd/${TERM}-${VENDOR}
else
    typeset -g -A key

    [[ -n "$terminfo[kf1]" ]] && key[F1]=$terminfo[kf1]
    [[ -n "$terminfo[kf2]" ]] && key[F2]=$terminfo[kf2]
    [[ -n "$terminfo[kf3]" ]] && key[F3]=$terminfo[kf3]
    [[ -n "$terminfo[kf4]" ]] && key[F4]=$terminfo[kf4]
    [[ -n "$terminfo[kf5]" ]] && key[F5]=$terminfo[kf5]
    [[ -n "$terminfo[kf6]" ]] && key[F6]=$terminfo[kf6]
    [[ -n "$terminfo[kf7]" ]] && key[F7]=$terminfo[kf7]
    [[ -n "$terminfo[kf8]" ]] && key[F8]=$terminfo[kf8]
    [[ -n "$terminfo[kf9]" ]] && key[F9]=$terminfo[kf9]
    [[ -n "$terminfo[kf10]" ]] && key[F10]=$terminfo[kf10]
    [[ -n "$terminfo[kf11]" ]] && key[F11]=$terminfo[kf11]
    [[ -n "$terminfo[kf12]" ]] && key[F12]=$terminfo[kf12]
    [[ -n "$terminfo[kf13]" ]] && key[F13]=$terminfo[kf13]
    [[ -n "$terminfo[kf14]" ]] && key[F14]=$terminfo[kf14]
    [[ -n "$terminfo[kf15]" ]] && key[F15]=$terminfo[kf15]
    [[ -n "$terminfo[kf16]" ]] && key[F16]=$terminfo[kf16]
    [[ -n "$terminfo[kf17]" ]] && key[F17]=$terminfo[kf17]
    [[ -n "$terminfo[kf18]" ]] && key[F18]=$terminfo[kf18]
    [[ -n "$terminfo[kf19]" ]] && key[F19]=$terminfo[kf19]
    [[ -n "$terminfo[kf20]" ]] && key[F20]=$terminfo[kf20]
    [[ -n "$terminfo[kbs]" ]] && key[Backspace]=$terminfo[kbs]
    [[ -n "$terminfo[kich1]" ]] && key[Insert]=$terminfo[kich1]
    [[ -n "$terminfo[kdch1]" ]] && key[Delete]=$terminfo[kdch1]
    [[ -n "$terminfo[khome]" ]] && key[Home]=$terminfo[khome]
    [[ -n "$terminfo[kend]" ]] && key[End]=$terminfo[kend]
    [[ -n "$terminfo[kpp]" ]] && key[PageUp]=$terminfo[kpp]
    [[ -n "$terminfo[knp]" ]] && key[PageDown]=$terminfo[knp]
    [[ -n "$terminfo[kcuu1]" ]] && key[Up]=$terminfo[kcuu1]
    [[ -n "$terminfo[kcub1]" ]] && key[Left]=$terminfo[kcub1]
    [[ -n "$terminfo[kcud1]" ]] && key[Down]=$terminfo[kcud1]
    [[ -n "$terminfo[kcuf1]" ]] && key[Right]=$terminfo[kcuf1]
fi

# Default key bindings
[[ -n ${key[Delete]} ]] && bindkey "${key[Delete]}" delete-char
[[ -n ${key[Home]} ]] && bindkey "${key[Home]}" beginning-of-line
[[ -n ${key[End]} ]] && bindkey "${key[End]}" end-of-line
[[ -n ${key[Up]} ]] && bindkey "${key[Up]}" up-line-or-search
[[ -n ${key[Down]} ]] && bindkey "${key[Down]}" down-line-or-search

# Default prompt
PS1="%n@%m %1~ %# "

# Useful support for interacting with Terminal.app or other terminal programs
[ -r "/etc/zshrc_$TERM_PROGRAM" ] && . "/etc/zshrc_$TERM_PROGRAM"

if [[ -f /Users/Shared/newPath.bash ]]; then
    source /Users/Shared/newPath.bash
fi

Save this file back to the etc folder. It might be a good idea to take a backup of the original file in case we need to restore it back to undo any errors we might introduce.

Open the terminal app. Run the welcome.bash script without providing the path. See if it works.

Run the command to print the path variable:

printenv PATH

As we can see, there is no need to run the export command repeatedly. There is no need to source the file that contains those commands either.

Standard location or custom path

This approach does give us a lot of flexibility. However, we can see that there are several things we need to do before we can get everything working well. It might be better for us to place our commands in the standard /usr/local folder. That would make the deployment a lot easier.


Man pages

What are man pages?

If you have used the command line interface on macOS/Unix/Linux then you would be familiar with the man command. In case you aren’t then the man command is the command that loads the manual for the binary specified. It is a quick easy way to access the documentation and help for the command. However, man pages aren’t restricted to only binaries. They could be applied on normal files too.

There is one thing to keep in mind. It is not necessary that a man page exists for a given command. Try running the command man folderCreator. What do you get?

No manual entry for folderCreator

We get a message saying that there is no manual entry for our binary. So we need to go ahead and create one.

Before I talk about how to create them I will first address the question of whether we need to create one in the first place. Especially since we are already providing help view the -h -help options. Actually we don’t have to. However, keep in mind that most users are already familiar with the man command and their instinctive reaction is to look for the man page of your command. It would be very nice to offer them that ability.

How do we create them?

In order to create our man page we need to use certain macros that render the document for us. More information can be available via the mandoc, groff, mdoc, and man commands. I would highly recommend going through the man pages of these commands.

man pages are simple files that contain information which is formatted with the help of different macros. A typical man page contains the following sections in the specified sequence:

  1. NAME
  2. SYNOPSIS
  3. DESCRIPTION
  4. VERBS
  5. REQUIREMENTS
  6. INSTALLATION
  7. USAGE
  8. WARNINGS
  9. EXIT STATUS
  10. EXAMPLES
  11. DIAGNOSTICS
  12. COPYRIGHT
  13. CONTACT DETAILS

There are other sections available too: the man page for groff command contains excellent information about that. Armed with the information about which sections are there within the man page we need to start gathering all our details together.

One piece that we need is the manual section.If we run the man command on man:

man man

It gives us some information about the manual sections. The sections describe the kinds of commands and potentially the actions they perform. Our command would fall under the user commands section or section 1.

Where are they located?

These files are located in the /usr/local/share/man/man1 folder. Where man1 represents the section number.

Let us try to create the file. Before that we will look at some macros that we would need.

MacroDescription
.DdThis is used to specify the date when the man page was created/published.
.DtThis is used to specify the title for the man page. Its value should always be in all caps.
.OsThe name of the operating system.
.ShSection header name,
.NmThe name of the command. This is the name that will be used throughout the document.
.NdThe description of the command.
.ArArguments being passed to a command
.OpOptions being passed to a command
.PpNew paragraph
.BlStart a list
.ElEnd the list
.Ev For environment variables
.ItItalics
.SsSubsection
.AnName of the author
.SySymbolic font mode
.\"Comment
Macros used to create the man page file

You can get more details and information about these macros by running the following command:

man mdoc

We will now use these macros to render our man page. The easiest way to do this would be to use an existing man page file as a template. The idea is to use the macros to do the rendering for us. Copy paste the contents of any existing man page into your file and start replacing the content with your own content. You can always test your page by using the man command directly on your file.

man /path/to/your/manpage/file.1

A good thing to do would be to add 1 item at a time and run the above command repeatedly till you get comfortable with how everything fits together.

If you need help correcting the formatting of the file run the following command:

mandoc -T lint folderCreator.1

This is how the man page file looks. Name it folderCreator.1 where the ‘1’ indicates the section number.

.\"Copyright (c) 2015-2022 Amaranthine. All Rights Reserved.
.\"
.\"
.Dd August 10, 2021
.Dt FOLDERCREATOR 1
.Os macOS 11
.Sh NAME
.Nm folderCreator
.Nd Folder creation utility
.\"
.\" ============================================================================
.\" ========================== BEGIN SYNOPSIS SECTION ==========================
.Sh SYNOPSIS
.Nm
.Ar "folder names"
.Op verbs
.\" =========================== END SYNOPSIS SECTION ===========================
.\" ============================================================================
.\"
.\" ============================================================================
.\" ======================== BEGIN DESCRIPTION SECTION =========================
.Sh DESCRIPTION
.Nm
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".
.Pp
The user is also prompted via the graphical user interface for names that should be used for the folders.
This is optional and the user can cancel it.
.Pp
There is also the option of getting help via the help verb.
.Pp
- This script is intended for creating the custom folders that are required on all corporate computers.
.Pp
- Run this script on a new computer or a computer being reassigned to another employee.
.Pp
- This script can run on all computers.
.\" ----------------------------------------------------------------------------
.\" ------------------------- BEGIN TERMINOLOGY LIST ---------------------------
.Sh VERBS
.Bl -hang
.It Op Fl h help
Both the options are used to invoke the help documentation.
.It Op Fl v version
Both the options are used to get the version number of the
.Nm
command.
.El
.\" --------------------------- END TERMINOLOGY LIST ---------------------------
.\" ----------------------------------------------------------------------------
.\" ============================================================================
.\" ======================== BEGIN REQUIREMENTS SECTION ========================
.Sh REQUIREMENTS
The following are the minimum requirements to get the script running.
.Bl -hang -offset 4n -width "xxxxxxxxxxxx" -compact
.It Shell:
zsh
.It OS:
macOS Big Sur 11.4 or later
.It Dependencies:
None
.El
.Ev HOME
.\" ============================================================================
.\" ======================== BEGIN INSTALLATION SECTION ========================
.Sh INSTALLATION
.Nm
can be installed anywhere you wish.
However, there are certain locations that are recommended.
.Bl -hang -offset 4n -width "xxxxxxxxxxxx" -compact
.It Location:
/Library/Scripts/
.It Permissions:
rwx r-x r-x
.El
.\" ============================================================================
.\" ======================== BEGIN USAGE SECTION ========================
.Sh USAGE
.Nm
.Ar folder1
.Ar folder2
.Ar folder3
.Pp
Will create folders with your own names.
.Pp
.Nm
.Ar -h
OR
.Nm
.Ar -help
.Pp
Will invoke the help utility.
.Pp
.Nm
.Ar -v
OR
.Nm
.Ar -version
.Pp
Will print the version number in stdout.
.Ss GUI Interaction
In all cases the user is always prompted for entering folder names via the graphical user interface.
Therefore this script triggers a gui popup.
In case this is not the desired behavior then the appropriate lines of code will need to be commented out.
.\" ============================================================================
.\" ======================== BEGIN WARNING/CAUTION SECTION ========================
.Sh WARNING/CAUTION
.Nm
does not perform any validation of names.
The only options that
.Nm
accepts are
.Ar -h
and
.Ar -help
verbs or the
.Ar -v
and
.Ar -version
verbs.
If the script does not see the
.Ar -h
,
.Ar -help
or the
.Ar -v
,
.Ar -version
options then it will assume that the data being passed in is the name of the folder.
The user of the
.Nm
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.
.\" ============================================================================
.\" ======================== BEGIN EXIT STATUS SECTION =========================
.Sh EXIT STATUS
In most situations,
.Nm
exits 0 on success
.\" ============================================================================
.\" ======================== BEGIN EXAMPLES SECTION ========================
.Sh EXAMPLES
.Nm
.Ar Resources
.Ar Results
.Ar Assistant
.Pp
This will create 3 folders
.Sy Resources
,
.Sy Results
,
.Sy Assistant
,
in the user's home folder.
.Pp
.Nm
.Pp
This will create 3 folders with the default names
.Pp
.Nm
.Ar Apps
.Pp
This will use the
.Sy Apps
name for the first folder but the default names for the last 2 folders.
.\" ============================================================================
.\" ======================== BEGIN DIAGNOSTICS SECTION ========================
.Sh DIAGNOSTICS
The script produces a log file called
.Sy ~/Library/Logs/folderCreator_log_v1-x.log
.Pp
This file is typically located in the user's home folder log folder.
The x represents the version number of
.Nm
.Pp
You can view the logs for each respective version.
.\" ============================================================================
.\" ======================== BEGIN COPYRIGHT SECTION ========================
.Sh COPYRIGHT
Copyright (c) Amaranthine 2015-2021.
All rights reserved.
https://amaranthine.in
.\" ============================================================================
.\" ======================== BEGIN CONTACT SECTION ========================
.Sh CONTACT DETAILS
.An Author: Arun Patwardhan
.Pp
Website: https://amaranthine.in
.Pp
Email: arun@amaranthine.co.in
view raw folderCreator.1 hosted with ❤ by GitHub

Place the man page in the following folder.

/usr/local/share/man/man1/folderCreator.1

This is how it would look if we run the man command on our folderCreator script.

This is how it would look like if we opened it in the Preview application.


Video

Download

You can download the final script from here.

You can download the man page file from here.

Summary

Our script has evolved quite a bit from the first blog. We started off with a very simple script and have ended with a larger script. It still does the same thing it did originally but is now a lot better. Here are some of the key points:

  • Writing event updates to log files
  • User interaction
  • Flexibility in terms of folder names via user interaction from the GUI as well as the command line arguments
  • Easy to maintain thanks to functions
  • Arrays make it scalable
  • Loops help make the script compact
  • Variables enhance scalability
  • Periodic checks ensure the script is safe and stable
  • User can get help using
    • the -h or the -help options
    • From the comments in the script
    • by viewing the man page for the script

Of course, there is room for improvement. Some of you might come up with ways of achieving this solution differently. Which is perfectly fine. There is no such thing as a universally perfect script. The point behind the script above was to illustrate how the different features could work together.

Final thoughts

Scripting is a continuous learning process. There are so many things in it. Over time you will find that you are faced with similar challenges. One thing that a lot of script writers and programmers do is to refer to perviously written scripts to get a head start. Every time you write a script it would be a good idea to archive it and keep a copy elsewhere. This will come in handy.

Happy scripting!

Advertisement

Shell scripting in macOS – Part 9: Automating scripts

This article is NOT a continuation of the previous article. The topics covered in this article won’t be used in our folder creation script as the features covered are not required or can’t be appropriately used. We will use a different script instead. This article is a continuation of the Shell scripting series though.

Why automate?

Most of the times we or someone else will be running the desired script. However there is one major drawback with this. It assumes that there is ‘someone’ who can run the script and they have access to the computer that the script is supposed to run on. This is not always a given.

The above approach also has the problem of efficiency. If the script is to be run periodically or at regular intervals it can get very inefficient and is likely to affect the workflows of the organisation.

Automation solves these problems for us by allowing scripts to run by themselves.

How to automate?

There are 2 options that we will look at in this article:

  • Expect utility
  • Launch Daemons/Launch Agents

Expect utility

The expect utility isn’t strictly an automation solution. It’s a tool that can answer stdout prompts from another script/program/application. Let us see how it works with an example.

The script below is asking a series of questions from the user and is expecting a response. The response is stored in a variable which is echo’d out. To make it interesting the last question is a choice based questions. Based on some random value the script will ask question ‘a’ or question ‘b’.

#!/bin/zsh

# Questions
# --------------------------------------------------
echo "1) What is your name?"
read -s NAME
echo $NAME 

echo "2) What is your email address?"
read -s EMAIL
echo $EMAIL 

echo "3) Could you tell me your company's website address?"
read -s WEBSITE
echo $WEBSITE 

echo "4) Please provide your mobile number?"
read -s MOBILE
echo $MOBILE 

# Choice
# --------------------------------------------------
if [[ $RANDOM -gt 90000 ]]; then
	echo "Do you like option A?"
else 
	echo "Would you prefer option B?"
fi

read -s OPTION
echo $OPTION 

Now if we just run the above script we would see prompts on stdout for the different questions and simply answer them. But what if this script is going to be called by another script? That is where the expect utility comes in. Let us have a look at out script and then analyse it

#!/bin/zsh
#!/usr/bin/expect 

#Variables
ANS1="Arun"
ANS2="arun@amaranthine.co.in"
ARG1=$1

export ANS1
export ANS2
export ARG1 

/usr/bin/expect << "EOF"
set myValue1 $env(ANS1);
set myValue2 $env(ANS2);
set arg1 $env(ARG1);

spawn /Users/arunpatwardhan/Developer/questions.zsh
expect "1) What is your name?\r" 
send -- "$myValue1\r"

expect "2) What is your email address?"
send -- "$myValue2\r"

expect "3) Could you tell me your company website address?"
send -- "www.amaranthine.in\r"

expect "4) Please provide your mobile number?"
send -- "$arg1\r"

expect {
	"Do you like option A?" { send -- "Of course!\r" }
	"Would you prefer option B?" { send -- "Definitely!\r" }
}

expect eof
EOF

Let us analyse this file line by line.

The first line is our interpreter declaration. The second line is where we specify which expect we will be using.

#!/bin/zsh
#!/usr/bin/expect 

Next we declare some variables. We will use them in our expect utility soon.

#Variables
ANS1="Arun"
ANS2="arun@amaranthine.co.in"
ARG1=$1

In order to use them in our utility we need to export these variables. We will then access them via the environment.

export ANS1
export ANS2
export ARG1 

This is where we start writing our expect command. Notice that we are using the ‘here’ document that we learnt in the previous script. This allows use to send multiple expect commands to the expect utility.

/usr/bin/expect << "EOF"

The first thing that we do is create variables for the expect utility from the variables that we already have.

set myValue1 $env(ANS1);
set myValue2 $env(ANS2);
set arg1 $env(ARG1);

Then we use the spawn command to start the script whose questions we wish to answer.

spawn /Users/arunpatwardhan/Developer/questions.zsh

Next we tell the expect utility to ‘expect’ a question. We specify the question to expect in the string after the expect command.

expect "1) What is your name?\r"

Once the expectation has been met, we send out our response to the question using the send command.

send -- "$myValue1\r"

We can answer the rest of the questions the same way. We can use variables to pass in data or directly write our answers in there.

In case of a choice we write an expect block command with the help of braces.

expect {
	"Do you like option A?" { send -- "Of course!\r" }
	"Would you prefer option B?" { send -- "Definitely!\r" }
}

Finally we end with an eof being passed to expect.

expect eof

The last EOF is for the ‘here’ document.

EOF

That’s it! It’s that simple to write expect scripts. Note that there are more commands available for the expect utility. I would strongly recommend going through the man page for details about expect.

man expect

There is also a utility called autoexpect that builds the expect script for you. As of this article (tested on macOS 12.6) it is not installed by default.

One last thing I would like to mention. The expect utility should ideally be used in a larger script which is invoking other scripts. It is not meant to replace human interaction. Use caution when invoking scripts/binaries/tools that you cannot read as you are not fully aware of everything that is done by these scripts.


Launch Daemons/Launch Agents

The other and option is to use launch daemons/launch agents. I will be referring to them as daemons through the rest of this article unless something specific has to be mentioned.

Daemons solve a different problem as compared to the expect utility.

The expect utility allows for a script/binary/tool/program to complete execution by answering questions expected from a human.

Daemons on the other hand allow us to schedule and automatically run scripts and custom intervals. These are background processes that are started on specific events. Daemons and agents are exactly the same, but they have some key differences:

Launch DaemonsLaunch Agents
These are owned by the systemThese are owned by the currently logged in user
Start as soon as the system boots upStarts only after the user logs in. Stops as soon as the user logs out
Is system wideCan apply to all user accounts or to a specific user account

These are some of the points that you have to keep in mind while deciding whether to create a daemon or an agent. The process for creating them is exactly the same. It is plist file that provides all the information that is necessary for the system to schedule and run the processes.

I will be talking about launch daemons and agents as well as plist files in separate articles at a later point in time.

Example

Let us look at how to create a daemon with the help of an example.

We would like to show an informational popup that appears as soon as the user logs in and at 5 minutes interval. We would also like to show a notification of the same.

Now this could also be achieved using other approaches but it gives us an opportunity to explore how to create agents. Also the scheduling is chosen so that we can see how it is done. It is not strictly required for this situation.

So why have we decided to create it as an agent?

The main reason is because it is to run only after the user logs in. Something that a Launch Agent is perfectly suited for.

We also want this to run for every user account on the computer. So this will need to be installed in the /Library folder. Specifically:

/Library/LaunchAgents/

Now that we have decided how and where let us focus first on the script that we plan to run.

#!/bin/zsh

/usr/bin/osascript << EOF 
display notification "By using this computer you agree to terms and conditions of use set down by the company." \
					  with title "Terms & Conditions" \
					  subtitle "Acknowledge" \
					  sound name "Alert"

set theAlertText to "Terms and conditions of use"
set theAlertMessage to "By using this computer you agree to terms and conditions of use set down by the company."
display alert theAlertText \
	    message theAlertMessage \
		as informational \
		buttons {"Accept"} default button "Accept"
EOF

The script simply uses the osascript command to run a few AppleScript commands. These commands show a notification as well as an alert message to the user. Let us place this script in the /Library/Scripts/ folder. This will make it easily accessible.

Next we will focus on creating the launch agent plist. Let us first list out the items we will need to put in the plist.

KeyDescription
LabelThis is the name of our Launch Agent
ProgramArgumentsThis is the action to be performed. In our case the action is to invoke the script.
StartIntervalThis is the time interval after which the program arguments must be executed.
RunAtLoadThis is a flag that indicates that the agent must be run as soon as it is loaded.

Armed with this information we will go ahead and create our plist. There are several tools available for this:

  • Xcode
  • TextEdit
  • defaults command
  • Third party tools like CodeRunner

We will use the defaults command for this. It is a built in command that allows us to work with plist files. Here is a little script that creates our plist.

#!/bin/zsh

defaults write ~/Desktop/in.amaranthine.welcomeMessage.plist Label -string "in.amaranthine.welcomeMessage"
defaults write ~/Desktop/in.amaranthine.welcomeMessage.plist ProgramArguments -array-add "/bin/zsh" "/Library/Scripts/welcome.zsh"
defaults write ~/Desktop/in.amaranthine.welcomeMessage.plist RunAtLoad -boolean true
defaults write ~/Desktop/in.amaranthine.welcomeMessage.plist StartInterval -integer 300

In the script, the defaults command is writing several key value pairs into a plist file at the specified path. Notice the key names: Label, ProgramArguments, RunAtLoad, StartInterval. These are the values we need to specify. The above commands need not be written in a specific sequence as they are being written to an XML file.

The resulting plist file should look like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>in.amaranthine.welcomeMessage</string>
	<key>ProgramArguments</key>
	<array>
		<string>/bin/zsh</string>
		<string>/Library/Scripts/welcome.zsh</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>300</integer>
</dict>
</plist>

Lets explore the different parts of this plist file. It contains a dictionary with several key value pairs.

<key>Label</key>
<string>in.amaranthine.welcomeMessage</string>

The first key value pair we encounter is ‘Label’. This is the name of our agent.

<key>ProgramArguments</key>
<array>
	<string>/bin/zsh</string>
	<string>/Library/Scripts/welcome.zsh</string>
</array>

The next key value pair is extremely important. It is telling our agent the task that needs to be performed. In this case use the zsh shell to run the welcome.zsh script. This is the same as running the following command in terminal:

/bin/zsh /Library/Scripts/welcome.zsh
<key>RunAtLoad</key>
<true/>

The second key value pair indicates that the agent must run as soon as it is loaded and not only when the user logs in. This is not strictly required but is very handy when we try to test our agent.

<key>StartInterval</key>
<integer>300</integer>

The last key value pair will schedule the agent to run at an interval of 5 minutes or 300 seconds.

You can check if the plist if properly formatted using the command:

plutil -lint /path/to/plist

Also, if you try to open the plist file in an editor it is likely that the formatting may not appear correctly. Run the following command to make it readable:

plutil -convert xml1 /path/to/plist

This does not affect the plist file in any way. So it is safe to perform.

The next bit is to place the file in correct location. Copy the plist file to the path:

/Library/LaunchAgents/

Next we will look at loading the agent

While the agent will be loaded automatically every time the user logs in. However, we would like to test the results immediately. We will use the launchctl command to load the agent.

launchctl bootstrap gui/501 /Library/LaunchAgents/in.amaranthine.welcomeMessage.plist

Let us explore the launchctl command.

The bootstrap option informs the system that an agent is being loaded.

The next bit tells launchctl that its for gui user 501. We can get the user number using the command id -u.

This is followed by the path to the plist file.

Run this command.

To find out if the launch agent has loaded correctly we will use the list option on launchctl to list out all the current agents.

launchctl list

To get more specific results:

launchctl list | grep "in.ama"

This will show a list of agents whose name starts with “in.ama”.

And that’s it. We have now scheduled the agent. If you have run the command to bootstrap the agent then you should see the popup appearing on the screen.

You should also see the notification:

If you wish to stop the agent run:

launchctl bootout gui/501 /Library/LaunchAgents/in.amaranthine.welcomeMessage.plist

It’s the same command as before. Except with the bootout option instead of the bootstrap.

NOTE: The agent will start running again the next time the user logs in.

Summary

As you can see there are several options available when it comes to automating scripts. This opens up more possibilities when it comes to performing management tasks on the device.

Video

Download

You can download the scripts from here:

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.

Shell scripting in macOS – Part 6: User interaction

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.

Why would we need to do this?

So far our scripts have run without any communication between the script and any user. In many situations this might be enough. Often times we would need to ask the user for some information before proceeding. This could be for confirmation before performing a certain task, or to customise the script actions as they are being performed.

Ways of interacting with the user

We have a few options to do this.

  • read utility
  • GUI pop via AppleScript

Let us look at the read utility first.

Reading from stdin

The read commands captures data entered via stdin. This is typically the terminal prompt from where the user enters the data. This captured value is read into a variable.

#!/bin/bash

read -p " What is your name?": NAME 

echo $NAME 

This is a simple example. The user is prompted (on stdout) to enter their name. The user types the name and it is stored in a variable. The contents of the variable are then echoed out to stdout again.

Of course, this assumes the user is manually running the script. Later in this article we will go over how we can automate such scripts with the help of ‘expect’ utility.

Reading from a file

Reading from a file is possible thanks to the read command as well as the loop operations we learnt earlier.

Asking question via GUI

The easiest thing to do would be to prompt the user for information via a GUI popup. This makes the experience a lot better. However, we will need something else for that. We will leverage AppleScript (which is a macOS specific scripting language) to show the popup. This will be invoked from the shell script using osascript.

I will be covering the basics of AppleScript in a later article.

First we will look at how to get the dialog to appear using AppleScript. This requires the use of AppleScript display dialog command.

display dialog "Would you like to provide names for the folders or use the defaults instead?" buttons {"Custom", "Default"} default button 2

This will simply show a popup to the user with a message and 2 buttons. We can customise it further:

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"

This adds a custom icon. If we run this script the user would see something like:

Make sure that the path to the custom icon is what you would want it to be.

If we wanted to find out which button the user has selected then:

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")

The button returned of tells us which button was clicked. It also adds a nice title for the popup.

The really nice thing to do would be to somehow run this from terminal. To do that we will use the osascript command. This command needs the AppleScript command to be passed is as a statement.

osascript -e '<AppleScript statement>'

Let us add our command here.

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")'

Finally to get this value into a script variable we will use command substitution.

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")')

We can now echo the variable or use it in a condition check.

Example

Let us update our code to ask the user for names they would like to give for their folder.

  1. Just after the line where we finish checking the 3 positional parameters, add this line.
#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")')

This will first ask the user if they wish to provide the folder names or use the defaults. The response is captured in the shell variable userClicked.

  1. Below that we will put an if check to see what the user has selected.
# 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
	
else

fi
  1. If the condition is true we have to show dialog prompts to ask the user for the name. This is going to be similar to our first check. However, this one will also have the ability for the user to enter a value. Add 3 prompts, one for each folder. Also add an echo statement to write to the log file. The if should now look like:
if [[ $userClicked == "Custom" ]]; then
	echo "$(date) The user decided to provide custom names." >> $PATH_TO_LOG
	
	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")')
		
	echo "$(date) User provided: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER" >> $PATH_TO_LOG
else
	echo "$(date) User decided to use default values: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER" >> $PATH_TO_LOG
fi

The script is now ready to accept user input. The completed script should look like:

#!/bin/zsh

#-------------------------------------------------------------------------------------------------
#NAME:		Folder creator
#AUTHOR:	Arun Patwardhan
#CONTACT:	arun@amaranthine.co.in
#DATE:		10th August 2021
#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.5
#-------------------------------------------------------------------------------------------------

#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-5.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.
#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# ------------------------------ 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.5"

#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-5.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

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

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

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

#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
	echo "$(date) The user decided to provide custom names." >> $PATH_TO_LOG
	
	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")')
		
	echo "$(date) User provided: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER" >> $PATH_TO_LOG
else
	echo "$(date) User decided to use default values: $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER" >> $PATH_TO_LOG
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. 
echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG

for item in $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER; do
	if [[ -d $item ]]; then
		echo "$(date) Not creating $item as it already exists." >> $PATH_TO_LOG
	else
		echo "$(date) Creating $item" >> $PATH_TO_LOG
		mkdir $item
	fi
	
	#7. Create the task completion file inside each folder.
	echo "$(date) Creating hidden file for $item folder." >> $PATH_TO_LOG
	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 ---------------------------------------------------

I have also performed some additional cleanup. That has been highlighted. The comments and the help have also been updated to reflect the new changes. I have also moved a couple of the echo statements further up the script. These have also been highlighted.

Video

Download

The completed script can be downloaded from here.

Shell scripting in macOS – Part 5: Loops

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.

Loops

Often times we will come across a situation where we want to perform a task repeatedly with different pieces of data.

For example, here is a snippet of our shell script:

if [[ -d $TOOLS_FOLDER ]]; then
	echo "$(date) Not creating $TOOLS_FOLDER as it already exists." >> $PATH_TO_LOG
else
	echo "$(date) Creating $TOOLS_FOLDER" >> $PATH_TO_LOG
	mkdir $TOOLS_FOLDER
fi

if [[ -d $REPORTS_FOLDER ]]; then
	echo "$(date) Not creating $REPORTS_FOLDER as it already exists." >> $PATH_TO_LOG
else
	echo "$(date) Creating $REPORTS_FOLDER" >> $PATH_TO_LOG
	mkdir $REPORTS_FOLDER
fi

if [[ -d $HELP_FOLDER ]]; then
	echo "$(date) Not creating $HELP_FOLDER as it already exists." >> $PATH_TO_LOG
else
	echo "$(date) Creating $HELP_FOLDER" >> $PATH_TO_LOG
	mkdir $HELP_FOLDER
fi

You can see that we are performing the same task repeatedly. Only the name of the folder is changing. This is the kind of situation that we will commonly encounter. Loops can help us make the script a little more efficient.

Handling loops

We are going to look at 2 solutions for handling loops:

  1. For loop
  2. While loop

For loops

A for loop works on a collection of data. It steps through the collection in the sequence in which it is present and picks one item from it at a time. The loop then performs the tasks specified in it using the item collected. Once finished it proceeds to pick up the next item. This is done till all the items in the collection are used.

Here is an example of a simple for loop. It contains 4 pieces of data in its collection: “Applications”, “Documents”, “Downloads”, & “Library”.

#!/bin/bash

for folder in Applications Documents Downloads Library
do	
	echo "The folder is $folder"
done

The output would be:

The folder is Applications
The folder is Documents
The folder is Downloads
The folder is Library

The collection could be data coming from somewhere else; such as the output of a command.

#!/bin/bash

for details in $(ls)
do
	echo "-> $details"
done

The output would be:

-> Desktop
-> Documents
-> Downloads
-> Library
-> Movies
-> Music
-> Pictures
-> Public

While loops

A while loop also performs tasks repeatedly. However it does this till a certain condition is satisfied. So we could use the test operations we learnt in an earlier article to achieve the check.

#!/bin/bash

while [[ ! -f /Users/Shared/exit.txt ]]; do
	echo "File not found"
done

echo "Found"

The script is checking to see if the file at the given path exists. If the file doesn’t exist then it prints out a message and performs the check again.

This will be done till the file is found.

NOTE: If you are testing the above code then you will need to create the file manually. You can do that by executing the following command in terminal:
touch /Users/Shared/exit.txt

Example

Let us go ahead and update our script.

  1. First we will remove the variables that store the names for the hidden files.
  2. Next we will remove the code for creating the folders. This will be replaced by a single block of code inside a for loop. Just remove the code for the reports folder and the help folder. We will use the code for the tools folder inside the for loop.
  3. Right below the line where we echo out the statement that we are creating the folder, add the code for the for loop. Move the remaining folder creation logic inside it and rename the variable.
for item in $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER; do
	if [[ -d $item ]]; then
		echo "$(date) Not creating $item as it already exists." >> $PATH_TO_LOG
	else
		echo "$(date) Creating $item" >> $PATH_TO_LOG
		mkdir $item
	fi

done
  1. Next, remove the code to create the hidden file. As before, only remove the code for creating the hidden file for the reports and help folders.
  2. Copy the remaining lines of code into the for loop. Place it right after the fi statement.
for item in $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER; do
	if [[ -d $item ]]; then
		echo "$(date) Not creating $item as it already exists." >> $PATH_TO_LOG
	else
		echo "$(date) Creating $item" >> $PATH_TO_LOG
		mkdir $item
	fi
	
	#6. Create the task completion file inside each folder.
	echo "$(date) Creating hidden file for $item folder." >> $PATH_TO_LOG
	cd $item
	#7. Generate the file names based on the folder names.
	touch ".$item-FolderCreated"
	cd ..
done

Your final script should look like this:

#!/bin/zsh

#-------------------------------------------------------------------------------------------------
#NAME:		Folder creator
#AUTHOR:	Arun Patwardhan
#CONTACT:	arun@amaranthine.co.in
#DATE:		10th August 2021
#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.4
#-------------------------------------------------------------------------------------------------

#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-4.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
#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# ------------------------------ 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.4"

#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-4.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.

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. 

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

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

#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

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

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

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

#5. Check to see if each of the folders exists. If it exists then do not create it. Else create the folder. 
echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG

for item in $TOOLS_FOLDER $REPORTS_FOLDER $HELP_FOLDER; do
	if [[ -d $item ]]; then
		echo "$(date) Not creating $item as it already exists." >> $PATH_TO_LOG
	else
		echo "$(date) Creating $item" >> $PATH_TO_LOG
		mkdir $item
	fi
	
	#6. Create the task completion file inside each folder.
	echo "$(date) Creating hidden file for $item folder." >> $PATH_TO_LOG
	cd $item
	#7. 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 --------------------------------------------------

Video

Download

You can download the completed script from here.

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 4: Documentation and help

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.

Documentation in scripts

As you must have observed, our script has already grown to a fairly large one. It still performs the same task we intended it to perform, but there are many more features and capabilities in it. As our script grows it becomes more and more difficult for us to understand what is happening and why. Of course, since we have written the script it is a lot easier for us to understand. But let’s suppose there is a colleague of yours that is reading this script. He/she might have a more difficult time trying to understand the purpose of the script. In fact, even if you attempted to read your own script, say after a break of 6 months, you will take some time understanding why exactly you wrote a piece of code.

A good way to address these issues is to document our code. Documentation gives the creator/editor of the script a chance to explain in simple terms what he/she intendeds to do and potentially why they did a particular thing.

What should be documented?

This is how the documentation template looks.

#!/bin/bash
#-------------------------------------------------------------------------------------------------
#NAME: <#documentation#>
#AUTHOR: <#author name#>
#CONTACT: <#email#>
#DATE: <#date#>
#WEBSITE: <#url#>
#-------------------------------------------------------------------------------------------------
#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) <#2021#> <#company name#>
#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 -------------------------------------------------------------------------------------------
#This give basic information about the file such as
# <#filename#>
# <#version#>
#-------------------------------------------------------------------------------------------------
#DESCRIPTION -------------------------------------------------------------------------------------
# Here you will give a brief decription of what your script does. Here are some of the things that you can mention.
# <#- Intended purpose#>
# <#- Situations when you can use this#>
# <#- The target computers where this should ideally run on.#>
#-------------------------------------------------------------------------------------------------
#USAGE -------------------------------------------------------------------------------------------
# This is where you give instructions on how to run the script. These could include:
# <#- Command#>
# <#- Available options#>
# <#- Getting help#>
#-------------------------------------------------------------------------------------------------
#WARNING/CAUTION ---------------------------------------------------------------------------------
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
# SOME SCRIPTS MAY MAKE SIGNIFICANT AND/OR IRREVERSIBLE CHANGES TO THE SYSTEM.
# THIS IS A GOOD PLACE TO MENTION THOSE ACTIONS. TRY TO DAW THE READERS ATTENTION OUT HERE.
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#******************************************************************************************************************
#-------------------------------------------------------------------------------------------------
#INSTALLATION ------------------------------------------------------------------------------------
# Instructions for placing the script in the correct place are listed here.
# Location: <#/Library/Scripts/#>
# Permissions: <#rwx r-x r-x#>
#-------------------------------------------------------------------------------------------------
#REQUIREMENTS ------------------------------------------------------------------------------------
# Specify what is required
# Shell: <#/bin/zsh#>
# OS: <#macOS Big Sur 11.x#>
# Dependencies: <#Other tools#>
#-------------------------------------------------------------------------------------------------
#HELP/SUPPORT ------------------------------------------------------------------------------------
# Give information about how users can get help about your script. This can onclude the use of help options or even man pages.
# Mention how they could access the same.
# For example:
# You can get help by running the following commands.
# <#<command> -h#>
# <#<command> -help#>
# OR
# <#man <command>#>
#-------------------------------------------------------------------------------------------------
#HISTORY -----------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------
# This is a good place to give version history. This helps the user of the script or any future modifiers of the script understand how the script evolved.
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ SCRIPT STARTS HERE ----------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ END OF SCRIPT ---------------------------------------------------

Let us look at some of the things that can be offered.

Item
Description
Ownership
This is where we provide details about the person who originally created the script. There might be more than one author involved. Also, it would be nice to maintain a history of people who have modified the script in any way.
Legal disclaimers and License termsThis is where you specify any disclaimers or warning that you wish to provide to anyone who wishes to use the script. You may also include license terms incase you wish to share the script with a wider audience.
Description
Of all the pieces this is one of the most important components. This section actually describes the scope and purpose of the script. Anyone who is using the script will probable be looking at this section first.
Usage
Use this section to provide information about how this script should be used. You could also provide information about the location of the script.
Warning
The idea behind this section is to provide the user information about possible issues with the script. Some examples of this would include: bugs, edge cases, expected supporting assets. Anything that is necessary for the script to run without issues.
Installation
This is where information for placing the script in the correct place with the correct permissions are provided.
Requirements
Any assets, resources or other items that may be required to run the script.
Help and Support
Information about where the user of the script can get more help or how they can get more help is provided here.
History
This section gives information about who modified the script, when they did it, along with the version number. This information may also be available via a version control tool such as Git. However, it may not be the case with every user of the script. In any case it is a good idea to also keep the history in the script.

Offering help

In most situations documentation is enough. But in some situations it may not be possible to read the script. This could be because the user may not have permissions to read the script. Another good way to provide information would be by providing a mechanism to get help for this command.

This is most commonly achieved by providing an option to out script either as -h or -help.

The script has now been modified to support this. It essentially prints a message on the screen if it sees the -h or the -help option.

This is how your final code should look.

#!/bin/zsh
#-------------------------------------------------------------------------------------------------
#NAME: Folder creator
#AUTHOR: Arun Patwardhan
#CONTACT: arun@amaranthine.co.in
#DATE: 10th August 2021
#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.3
#-------------------------------------------------------------------------------------------------
#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-3.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.
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ 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.3"
#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-3.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.
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.
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
echo "$(date) Running script $0 to create folders."
echo ""
#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. Generate the file names based on the folder names.
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-3.log"
echo "$(date) Starting" >> $PATH_TO_LOG
#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.
echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG
if [[ -d $TOOLS_FOLDER ]]; then
echo "$(date) Not creating $TOOLS_FOLDER as it already exists." >> $PATH_TO_LOG
else
echo "$(date) Creating $TOOLS_FOLDER" >> $PATH_TO_LOG
mkdir $TOOLS_FOLDER
fi
if [[ -d $REPORTS_FOLDER ]]; then
echo "$(date) Not creating $REPORTS_FOLDER as it already exists." >> $PATH_TO_LOG
else
echo "$(date) Creating $REPORTS_FOLDER" >> $PATH_TO_LOG
mkdir $REPORTS_FOLDER
fi
if [[ -d $HELP_FOLDER ]]; then
echo "$(date) Not creating $HELP_FOLDER as it already exists." >> $PATH_TO_LOG
else
echo "$(date) Creating $HELP_FOLDER" >> $PATH_TO_LOG
mkdir $HELP_FOLDER
fi
#7. Create the task completion file inside each 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!"
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
#-------------------------------------------------------------------------------------------------
# ------------------------------ END OF SCRIPT ---------------------------------------------------

Other options

There is one more way of providing information.

  • man pages
  • Other documents such as: pdf, txt…
  • Web based blog: This could be a dedicated page or part of the version control tool page.
  • Video based tutorial. This is used in cases where the script has a lot of capabilities and is used very often.

In fact, our blog has made use of the GitHub webpage all this while. To this we have added documentation and help tools too.

Downloads

You can download the completed file from here. You can download a template for the documentation from here.

Creating custom operators in Swift

What are custom operators?

Custom operators are operators that are defined by us and are not part of the programming language natively.

We are all aware of the built in operators in the Swift Language. 

Operators like: + – * % > == ! to name a few.

These operators are defined by the system. It is also possible for us to overload some of these operators. However there are situations where we would like to create our own operators that perform operations not defined by the system. 

Thats exactly what Custom operators are. They are operators defined by the developer. These are not overloaded operators but completely new operators that don’t exist otherwise.

These operators are used within the project that we are working on. Though it is possible for us to share these operators using Swift Packages or XCFrameworks.

These operators are typically associated with a specific type and their behavior is also defined by us.

Why do we need them?

There are many reasons why we would want custom operators:

  1. Allow for more compact and concise syntax.

Using custom operators allows our code to be more compact. Entire function calls can be condensed into a single operator.

  1. Make the code more readable

This also improves the readability of our code. Properly chosen symbols can convey the message immediately and easily. 

  1. Allow for consistency in design of code

One of the other things that custom operators help us achieve is consistency. By using standard operations as operators we make our code more familiar and consistent to others who may read it. Programmers are familiar with the concept of operators and using them for different operations. So even if they may not immediately recognise the operator they would understand that there is some task for them to perform.

And finally it encourages reusability.

What do we need to create custom operators?

There are a couple of things that we need to create custom operators:

  1. A logic for the action being performed by the operator
  2. A list of valid symbols
  3. Information about the operators attributes like prefix, postfix, infix.
  4. The precedence of the operator if it is an infix operator

Operator Rules

There are some rules that must be followed when we are constructing the symbol for our operator. Most of the requirements are rather straightforward. However, choosing the right symbol is a very important task. There are a set of symbols that are allowed. 

There are rules as far as whitespace around operators is concerned.

And finally there are certain symbols are allowed only in combination with other symbols. 

Operator types
TypeDescription
PrefixOperators that appear before a variable or value. These are unary operators.
PostfixOperators that appear after a variable or value. These are unary operators.
InfixOperators that appear in between variables or values. These are binary operators.

Allowed Characters

This is the important bit. Which characters are allowed for usage as an operator. 

We can have ASCII symbols that are used for builtin operators.

There are also many mathematical symbols that can be used as operators.

Note that the list of symbols show in the slide are not complete. 

TypeExamples of different symbols
ASCII Characters/, =, -, +, !, *, %,<, >, &, |, ^, ?, ~
Mathematical Operators,
Miscellaneous symbols, dingbats*
∝, √, ⊆, ≿, ∫

Here are some more

U+00A1–U+00A7U+2190–U+23FF
U+00A9 or U+00ABU+2500–U+2775
U+00AC or U+00AEU+2794–U+2BFF
U+00B0–U+00B1U+2E00–U+2E7F
U+00B6U+3001–U+3003
U+00BBU+3008–U+3020
U+00BFU+3030
U+00D7U+0300–U+036F
U+00F7U+1DC0–U+1DFF
U+2016–U+2017U+20D0–U+20FF
U+2020–U+2027U+FE00–U+FE0F
U+2030–U+203EU+FE20–U+FE2F
U+2041–U+2053U+E0100–U+E01EF
U+2055–U+205E

Whitespace

The next important bit is the whitespace around the operator.

If an operator has a whitespace on both the sides or doesn’t have whitespace on both the sides then it is interpreted as a binary operator. This is what would appear for infix operator.

If an operator has whitespace only on the left then it is a prefix unary operator.

If an operator has whitespace only on the right then it is a postfix unary operator.

If an operator does not have whitespace on the left but is followed by a dot then it is treated as a postfix unary operator.

Finally, any round, brace, square brackets appearing before or after the operator along with comma, colon, & semicolon are treated as whitespace

Making sure that we put the whitespace in the correct place while using these operators is very important.

No.RuleExample code
1If an operator has a whitespace on both the sides or doesn’t have whitespace on both the sides then it is interpreted as a binary operatora**b 
or 
a ** b
2If an operator has whitespace only on the left then it is a prefix unary operator**a
3If an operator has whitespace only on the right then it is a postfix unary operatora**
4If an operator does not have whitespace on the left but is followed by a dot then it is treated as a postfix unary operatora**.b is treated as a** .b
5(, {, [ before the operator and ), }, ] after the operator along with ,, :, ; are treated as whitespace

There are some exceptions to the rules we just saw. Especially with exclamation mark & question mark.

  1. ! & ? which are predefined are always treated as postfix if there is no whitespace on the left
  2. If we wish to use ? In optional chaining then it must not have whitespace on the left
  3. To use it as a ternary conditional operator ?: it must have whitespace on both the sides
  4. Operators with a leading or trailing <, > are split into multiple tokens. For example, in Dictionary<String, Array<Int>> the last 2 arrows are not interpreted as shift operator.

Operator grammar

There are rules for constructing operators. Only certain combinations are allowed.

Each operator contains a symbol which forms the operator head. The head is the first character in the operator. 

The head may or may not be followed by 1 or more characters which are operator characters. 

The head and the optional characters combined together form the operator. 

The head itself can contain a one out of a set of valid symbols. Or it can contain a period.

These are some of the symbols allowed for usage as the head of the operator. You can choose any one of those.

/, =, -, +, !, *, %,<, >, &, |, ^, ?, ~U+2055–U+205E
U+00A1–U+00A7U+2190–U+23FF
U+00A9 or U+00ABU+2500–U+2775
U+00AC or U+00AEU+2794–U+2BFF
U+00B0–U+00B1U+2E00–U+2E7F
U+00B6U+3001–U+3003
U+00BBU+3008–U+3020
U+00BFU+3030
U+00D7
U+00F7
U+2016–U+2017
U+2020–U+2027
U+2030–U+203E
U+2041–U+2053

For the successive characters you can use any of the symbols allowed for the head plus some additional allowed symbols. The list above contains all the allowed symbols.

/, =, -, +, !, *, %,<, >, &, |, ^, ?, ~U+2055–U+205E
U+00A1–U+00A7U+2190–U+23FF
U+00A9 or U+00ABU+2500–U+2775
U+00AC or U+00AEU+2794–U+2BFF
U+00B0–U+00B1U+2E00–U+2E7F
U+00B6U+3001–U+3003
U+00BBU+3008–U+3020
U+00BFU+3030
U+00D7U+0300–U+036F
U+00F7U+1DC0–U+1DFF
U+2016–U+2017U+20D0–U+20FF
U+2020–U+2027U+FE00–U+FE0F
U+2030–U+203EU+FE20–U+FE2F
U+2041–U+2053U+E0100–U+E01EF
Examples
.+.
≈
√
**

Operator Precedence

As far as infix operators are concerned there is also the question of precedence. Precedence is used to determine the operator priority when there are multiple operators in a single statement. 

precedencegroup <#precedence group name#> {
    higherThan: <#lower group names#>
    lowerThan: <#higher group names#>
    associativity: <#associativity#>
    assignment: <#assignment#>
}

While the first 2 values are straightforward, they simply help determine the exact position of the newly created precedence as compared to existing precedences, the associativity and assignment are extra items that are not immediately clear.

TypeDescriptionValues
AssociativityDetermines order in which a sequence of operators with the same precedence are evaluated in the absence of grouping bracketsleft, right, none
AssignmentSpecifies priority when used with optional chaining. 
TRUE: Same grouping rules as assignment operator from standard libraryFALSE: Same rules as operators that don’t perform assignment
true, false

The assignment of a precedence group specifies the precedence of an operator when used in an operation that includes optional chaining. When set to true, an operator in the corresponding precedence group uses the same grouping rules during optional chaining as the assignment operators from the standard library. Otherwise, when set to false or omitted, operators in the precedence group follows the same optional chaining rules as operators that don’t perform assignment.

Determines order in which a sequence of operators with the same precedence are evaluated in the absence of grouping brackets. so for example 4 – 6 – 7 has the minus sign which has left associativity. The operation 4-6 is grouped and then the – 7 operation is performed.

Nonassociative operators of the same precedence level can’t appear adjacent to each to other.

The priority for the built in precedences can be seen in Apple’s documentation.

Creating the operators

It is fairly easy to create our own operators. You can try the code in a playground. We will be creating 1 operator of each type: postfix, prefix, infix.

  1. Create a new playground.
  2. Declare the creation of the prefix operator as shown. This will be used as a squaring operator.
prefix operator **
  1. Now we will provide a generic version of the operator implementation.
prefix func **<T:Numeric> (inputValue : T) -> T {
    return inputValue * inputValue
}

That’s it. It is that simple to create our own prefix operator. Now let us test it.

  1. Create a variable of type Float and use the operator we have just created.
var lengthOfSideOfSquare : Float = 1.1

var areaOfSquare : Float = **lengthOfSideOfSquare

print("The area of a square whose side is \(lengthOfSideOfSquare) centimeters long is \(areaOfSquare) square centimeters")

  1. Similarly declare a postfix operator. This one will perform conversion to a string.
postfix operator ~>
  1. Now we will implement this operator. To do that let us make a simple type which will have the to string operator capability.
struct Person {
    var name : String = ""
    var age : Int = 0
}

extension Person {
    static postfix func ~> (inputValue : Person) -> String {
        return "NAME: \(inputValue.name)\nAGE: \(inputValue.age)"
    }
}
  1. Let us try this operator out and see.
var developer : Person = Person(name: "Arun Patwardhan",
                                age: 35)

var description : String = developer~>

print(#line, description)
  1. Now let us implement an infix operator. The one that we are going to implement is a similarity operator which can be used to determine the degree of similarity between objects of the same type. To do that let us start off by declaring an enum which holds the values for the degree of similarity.
enum DegreeOfSimilarity {
    case exactly_the_same
    case almost_the_same
    case slightly_similar
    case completely_different
}
  1. Infix operator can also have a precedence associated with it. Let us declare our own precedence and use it for our operator.
precedencegroup DegreeOfSimilarityPrecedence {
    higherThan: AdditionPrecedence
    lowerThan: MultiplicationPrecedence
    associativity: none
    assignment: true
}

Let us examine the values we have given:

higherThan: This indicates that our precedence has higher priority than the Addition precedence

lowerThan: This indicates that our precedence has lower priority than the Multiplication precedence

Associativity: This indicates that our operator is not associative. So we cannot combine multiple occurrences of our operator in one statement.

assignment: This indicates that out operators has the same behaviour, as other operators that assign, when it comes to optional chaining.

  1. Now we can declare our infix operator.
infix operator ≈ : DegreeOfSimilarityPrecedence

It is useful to save your new operator symbols as code snippets to easily use them. You can read this article if you don’t know how to create a code snippet.

  1. Let us look at the implementation. I am going to use the same person type we used earlier.
extension Person {
    static func ≈ (lhsValue : Person, rhsValue : Person) -> DegreeOfSimilarity {
        guard lhsValue.name == rhsValue.name else {
            return DegreeOfSimilarity.completely_different
        }
        
        guard lhsValue.age == rhsValue.age else {
            return DegreeOfSimilarity.almost_the_same
        }
        
        return DegreeOfSimilarity.exactly_the_same
    }
}
  1. Now we will test them and see.
var employee1 : Person = Person(name: "Jack",
                                age: 22)

var employee2 : Person = Person(name: "John",
                                age: 21)

var employee3 : Person = Person(name: "Jack",
                                age: 23)

var employee4 : Person = Person(name: "Jack",
                                age: 23)

print(#line, employee1 ≈ employee2)

print(#line, employee1 ≈ employee3)

print(#line, employee3 ≈ employee4)
  1. Run the code and see the end result.

Feel free to create more operators and play around. You could also package these operators in a swift package and share them around. I have shared links to

Summary the new operator

Creating operators is very easy. Most of the requirements are rather straightforward. However, choosing the right symbol is a very important task.

The one thing that we should keep in mind is not to over use these. It can be tempting to do this. But abstracting everything can make the code look a little too vague.

So that is how you can create operators. 

Download the sample project

I have uploaded some of the custom operators, that I have shown above, as a Swift Package. You can download the package as well as a demo project, which shows how to use them, from the links below.

Video

Here is the video describing what we discussed above.

Creating Code Snippets in Xcode

What are code snippets?

Code snippets are as the name suggests, short pieces of code that can quickly be inserted into your code file. This is done either by dragging the snippet or by typing out the completion. Code snippets are very easy to create and use and can be applied in a wide variety of situations.

We will look at how you can create & use snippets. The following example is done in a playground, but this could be done from anywhere within Xcode.

Note: The example below was performed on Xcode 11.7

How do we create code snippets?

  1. Start off by writing the code or text that you want to convert into a snippet. For example, I have a set of comments that I add at the start of every function. Write it down.
/**
 This function performs a comparison of the 2 objects
 - important: This function does not perform data validation.
 - returns: `Bool`.
 - requires: iOS 13 or later
 - Since: iOS 13
 - parameter lhsValue: This holds the value on the lhs of the operator
 - parameter rhsValue: This holds the value on the rhs of the operator
 - Example: `var answer =  venueAddress == hotelAddress`
 - author: Arun Patwardhan
 - copyright: Copyright (c) Amaranthine 2020
 - date: 14th September 2020
 - version: 1.0
 */

2. Select it.
3. From the menu bar select Editor > Create Code Snippet.

This brings up the snippet editor.
4. Give your snippet the following details.

OptionDescription
NameThis is the name of your code snippet.
PlatformThis determines whether your snippet is available only for certain platforms: say only for iOS.
AvailabilityThis determines the place where the snippet can be added.
CompletionThis is the word that we will be typing in the Xcode editor to trigger the implementation of the snippet
LanguageThis specifies the language for which the snippet will be applied.

Name: Func Documentation

Language: Swift

Platform: All

Availability: All scopes

Completion: doc

Note that the values for Name and Completion can be whatever you want.

This is how the snippet should look.

5. Now we will try to use it in the editor. Start typing the completion word in the Xcode editor.

6. Select the snippet with your name and completion.
7. Hit enter. You should see the comments you want appearing in the editor.

Placeholder

We can make our snippet above even better by using placeholders. Placeholders are pieces of text that can be replaced by the user. They also give information about what is expected in the placeholder.

We can add place holders by simply typing the hint inside placeholder brackets. Placeholder brackets are nothing but open <# and closing #>. For example:

<# some text #>

Which appears as

The user will simply click on the “some text” placeholder.

There are plenty of places in our comments where we can use placeholders. When we use the code snippet it should put comments with place holders in them.

  1. Let us change the comments in our Xcode editor first. We will edit the snippet later on. Make the changes as shown below.
/**
 <# put the description of your function here #>
 - important: <# mention some important points here #>
 - returns: `<# return type #>`.
 - requires: iOS  <#iOS Version#>  or later
 - Since: iOS  <#iOS Version#>
 - parameter <#param 1#>: This holds the value on the lhs of the operator
 - parameter <#param2#>: This holds the value on the rhs of the operator
 - Example: `<#put some example code here#>`
 - author: Arun Patwardhan
 - copyright: Copyright (c) Amaranthine 2020
 - date: <#day#>  <#month#>  <#year#>
 - version: 1.0
 */

We have made the following items into comments.

  • Description
  • OS Version
  • Return type
  • Important comments
  • Parameter 1 & 2 names
  • Sample code
  • Day, Month, & Year

Of course, there are other things we could change too. Feel free to make any other changes you can think of.

2. Let us now copy these changes to the code snippet we created. Copy the code from the Xcode editor.

To bring the snippet editor again simply click on the add object button in the upper right hand corner of Xcode.

4. Select the snippet from the list on the left and click edit.
5. Paste the code that you just copied. Your snippet editor should look like this:

6. Click on ‘Done’ once you are finished making changes. Your snippet will now be ready.

7. Try adding the snippet into your editor just like before. Simply type in the completion for your snippet.

Dragging snippets

We can use the autocompletion we saw earlier. But it is also possible for us to drag snippets.

Exporting code snippets

Once created it is possible to export/import code snippets too. All the snippets are located in the following folder.

~/Library/Developer/Xcode/UserData/CodeSnippets/

Any snippets you have created will be located there.

Any new snippets to be added will have to be added there.

Summary

Code snippets are easy to create and have several advantages:

  1. They improve the developers experience
  2. Promote consistent code
  3. Speeds up the process of writing code
  4. Encourages developers to use each others snippets and gain the first 3 advantages.

Creating and using snippets is very very easy and has a lot of benefits. So go ahead and create snippets.

List of macOS Terminal commands

This article lists out different macOS terminal commands you might encounter. You can use this list as a starting point in your search for a command to perform a specific task. This list is by no means exhaustive.

Basic terminal commands are not listed here. Some of them are listed in the following Terminal command articles.
Terminal Commands – Basic
Terminal Commands – Part 2
Terminal Commands – Part 3

Many of the commands have also been used in the article I wrote some time back. You can have a look at the scripts to see some of the commands being used.

To get more information about the commands simply run the following command from within Terminal Application. For example, to view the manual page for tmutil simply type:

man tmutil

For fdesetup

man fdesetup
Here is a nice command to quickly open the man page in the Preview App.
man -t tmutil | open -f -a /System/Applications/Preview.app

Note

  • This is not a complete list of commands
  • Some commands are available through the macOS Recovery Volume only
  • Some commands required other resources such as the OS installer
  • Some commands are available with certain versions of the OS only

Please read the documentation for more details. Use the commands with care. Improper use of commands may result in loss of data or damage to the computer.

Commands


Installation

CommandDescription
startosinstallUsed to start the installation of macOS from the command line.
createinstallmediaUsed to create an external install disk.

Security

CommandDescription
fdesetupManage FileVault configuration.
securityManage keychain and security settings
spctlManage security assessment policy
csrutilConfigure System Integrity Protection (SIP) settings
resetpasswordPassword reset utility located in the Recovery Partition

File System

CommandDescription
hdiutilUsed to manipulate and manage disk images.
diskutilUsed to modify, verify, & repair local disks.

Data Management

CommandDescription
tmutilUsed to configure Time Machine settings in macOS
screencaptureTakes screenshot of the specified screen and saves the image at the specified location.
mdlsUsed to get metadata attributes for a given file
mdutilUsed to manage metadata stores that are used by Spotlight

Settings

CommandDescription
defaultsUsed to modify plist files. Typically used to update preference files.
ioregUsed to view the I/O kit registry
system_profilerUsed to generate system hardware & software reports.
plutilUsed to check syntax of property lists or covert property lists from one format to another
AssetCacheManagerUtilUsed to configure content caching settings.
openUsed to open documents from within the command line.
networksetupPerform network configuration.
systemsetupUsed to configure machine settings in System Preferences.
launchctlUsed to manage and inspect daemons, agents, & XPC Services

Applications

CommandDescription
codesignUsed to create, check, display code signatures.
pkgbuildUsed to build installer packages
productbuildBuilds a product archive
installerSystem software and package installer tool

User Account Management

CommandDescription
dsclThis is a command line Directory service utility that allows us to create, read, and manage Directory Service data.
sysadminctlUser account management
passwdChange user password
loginUsed to login to another user account.

Server & Device Management

CommandDescription
profilesUsed to install, remove, list, or manage Configuration profiles.
serveradminUsed to manage the services in macOS
mdmclientLocated in /usr/libexec/mdmclient it is used to manage interactions with the MDM.
asrApple Software restore: Used to copy volumes.

Scripting

CommandDescription
osascriptUsed to execute the given AppleScript

Share any commands you may know of in the comments window.

Disclaimer

The information 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 information provided Or The Use Or Other Dealings In The information.