Apple has recently introduced the ability to gather certain information from Apple Business Manager/Apple School Manager using the API. This makes acessing the information using shell scripts possible.
One the big advantages of this is the ability to pull out information and share it with those members of the organisation who do not have + who don’t need access to Apple Business Manager/Apple School Manager.
Requirements
Before we get started itโs important to note that the API calls can be achieved using other languages too. I will be following up with the Swift version of the code later. For now we will be using the Python script along with the shell script per Appleโs documentation.
We need the following items to successfully make the API calls
API Account
Python3
Python libraries: authlib and PyCryptodome
Scripts to generate the client assertion
Script to generate the bearer token
API end points
Let us explore these requirements.
API Account
The API account is an account on Apple Business Manager/Apple School Manager that exists for the sole purpose of making API calls possible.
Python
Next we will look at Python. We will be using the python script that is provided in Apple’s documentation. This means that we need to install Python3. That can be done by simply downloading and installing python from its website or if you have Xcode installed to use Xcode to get python.
Python libraries
Now the python script uses 2 python libraries: authlib and PyCryptodome. In order to generate the client assertion we need to use JWT as well as the Elliptical curve encryption. These two libraries provide us with the necessary tools to achieve this.
Scripts
Next we will need to get the scripts from Apple’s website. You are welcome to write this using any other scripting language or programming language. But for this article we will use their script as is. The scripts achieve 3 different tasks and there is a dependency between them.
So what are these tasks? Basically the API supports OAuth 2 for authentication. It works this way. We will first use credentials to generate a client assertion. A client assetion is a JSON web token that the script creates and signs using the private key for the API account. We will use this client assertion to obtain a access token/bearer token by calling the authorisation server. The client assertion expires after a maximum of 180 days from date of issuance. The bearer token is what we will be using to make authenticated requests for the API calls. The bearer token is valid for 60 minutes. Finally we have to make the API calls themselves.
Script validity
Authentication item
Validity duration
Client assertion
Maximum 180 days
Bearer token
60 minutes
API endpoints
There are several endpoints that we can reach. Each end point specifies the kind of information that we can pull from Apple Business Manager/Apple School Manager. This information is available from Apple’s developer documentation. All the end points have a common part for the url
https://api-business.apple.com/v1
The table below lists the differet endpoints and their use.
Name
Description
URL
Get all devices
Gets all the devices that are there in Apple Business Manager/Apple School Manager
/orgDevices
Get device
Get information about a specific device
/orgDevices/{id}
AppleCare coverage
Get the AppleCare coverage information for a specific device
/orgDevices/{id}/appleCareCoverage
Device management services
Gets a list of MDM servers connected to this Apple Business Manager
/mdmServers
Device serial numbers for management service
Gets a list of all the devices that have been assigned to a particular MDM server
/mdmServers/{id}/relationships/devices
Assigned device management service ID for device
Gets the ID for the MDM server that a particular device has been assigned to
/orgDevices/{id}/relationships/assignedServer
Assign or unassign device
Assign or unassign devices to/from a particular MDM server
/orgDeviceActivities
Device activity information
Gets the activity information for a particular device.
/orgDeviceActivities/{id}
All these things are acheived with the help of 3 scripts.
Client asertion script: This is written in Python.
Bearer token script: This is a shell script.
API call script: This is the main script that makes calls to the API end points.
Armed with all this information we are now ready to make the API calls.
Preparing to use the API
Going forward i will only be mentioning Apple Business Manager, however, the steps apply to both.
Create the API account
The API account is created on Apple Business Manager/Apple School Manager portal.
Login to your Apple Business Manager account.
Open the preferences.
Click on the API tab.
Click on Get Started.
Provide and account name.
Click on create.
You will be prompted to generate a private key. Click on generate and save the private key securely. We will need it later.
You should now see your account listed in Apple Business Manager.
Click on manage to see the details about your account. That is where you will get the client id and the key id. We will be needing those values later.
Install Python
There are several ways of acquiring and installing Python. The simplest is to download it from the python website.
Install the dependencies
Install authlib and Crypto using the following commands:
pip3 install authlib
pip3 install PyCryptodome
Do not install Crypto or pycrypto as those don’t support the ECC algorithm
Make sure python 3 is installed.
Saving the private key
We will save the previously downloaded key in a single location. Let us assume that its on the Desktop. It could be anywhere.
Acquiring the access token
We are going to place all our files on the desktop. They could however be placed anywhere.
NOTE: I have used the same scripts that Apple has provided excepting for the script that is used to make the actual API calls.
If you want you can download the scripts from github or copy the scripts from below.
Getting the client assertion
The first thing to do is to get the client assertion. This is where the python script comes in. Save this script onto your desktop.
Copy the client id for your api account into the clinet_id and team_id variables. Copy the key id for your api account into the key id variable.
#!/usr/bin/env python3
import os
import authlib
import Crypto
import datetime as dt
import uuid as uuid
from authlib.jose import jwt
from Crypto.PublicKey import ECC
private_key_file = "/path/to/certificate.pem"
client_id = "BUSINESSAPI.--------------------------------------------------"
team_id = "BUSINESSAPI.--------------------------------------------------"
key_id = "--------------------------------------------------"
audience = "https://account.apple.com/auth/oauth2/v2/token"
alg = "ES256"
# Define the issue timestamp.
issued_at_timestamp = int(dt.datetime.utcnow().timestamp())
# Define the expiration timestamp, which may not exceed 180 days from the issue timestamp.
expiration_timestamp = issued_at_timestamp + 86400*180
# Define the JWT headers.
headers = dict()
headers['alg'] = alg
headers['kid'] = key_id
# Define the JWT payload.
payload = dict()
payload['sub'] = client_id
payload['aud'] = audience
payload['iat'] = issued_at_timestamp
payload['exp'] = expiration_timestamp
payload['jti'] = str(uuid.uuid4())
payload['iss'] = team_id
# Open the private key.
with open(private_key_file, 'rt') as file:
private_key = ECC.import_key(file.read())
# Encode the JWT and sign it with the private key.
client_assertion = jwt.encode(
header=headers,
payload=payload,
key=private_key.export_key(format='PEM')
).decode('UTF-8')
# Save the client assertion to a file.
with open('client_assertion.txt', 'w') as output:
output.write(client_assertion)
Now we will run this script from terminal. Run it with the command:
This will create a file called client_assertion.txt in your owrking directory. It should be a large piece of text containing random characters. This is because our script encrypted this information. It should look something like this:
Copy it. We will need it while maing the API calls.
This needs to be regenerated once every hour.
Using the API
Now that we have the bearer token we are ready to make API calls. We will use the curl command to get the information from the URL. We just need to change the end point per the table listed above.
#!/bin/bash
ACCESS_TOKEN="IUHNHN89NN9898*(*(HUGYG&**GGGYUY98ny78y7y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds5y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds5y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds59898*(*(HUGYG&**GGGYUY98ny78y7y&N*9898*(*(HUGYG&**GGGYUY98ny78y7y&N*"
#1. Get all devices
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/orgDevices" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#2. Get specific device
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/orgDevices/----------" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#3. Get MDM servers
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/mdmServers" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#4. Get AppleCare coverage
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/orgDevices/----------/appleCareCoverage" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#5. Get devices assigned to an MDM server
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/mdmServers/----------/relationships/devices" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#6. Get MDM server for a specific device
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/orgDevices/----------/relationships/assignedServer" -H "Authorization: Bearer ${ACCESS_TOKEN}"
#7. Get device activity
# --------------------
/usr/bin/curl -X GET "https://api-business.apple.com/v1/orgDeviceActivities/----------" -H "Authorization: Bearer ${ACCESS_TOKEN}"
Run the script by commenting out the other different end points so that you can explore each end point separately:
zsh ~/Desktop/abm-asm-apiCheck.zsh
It might be a good idea to save the output to a JSON file so that we dont have to make repeated calls to get different parts of the data.
Parsing the API output
Once the output is saved to a JSON file we can use commands like jq to parse through the information. This could also be used to generate a CSV file from the output. Here are some examples of the same.
Suppose we run the command:
usr/bin/curl -X GET "https://api-business.apple.com/v1/mdmServers" -H "Authorization: Bearer 'IUHNHN89NN9898*(*(HUGYG&**GGGYUY98ny78y7y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds5y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds5y&N*&*&GGFRDrderd4sertr(*hugyguyg---BYUG6r7vftf5tds59898*(*(HUGYG&**GGGYUY98ny78y7y&N*9898*(*(HUGYG&**GGGYUY98ny78y7y&N*'" > /Users/Shared/mdms.json
This will generate a JSON file that looks like this:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This will return the ‘id’ for the first MDM server. The output will look like this:
98V3T8W934YT8VYNT8TC2483V8239N8323
less /Users/Shared/mdms.json | jq '.data[0].attributes'
This will give us the attributes for the first MDM server. The output will look like this:
{
"serverName" : "Devices Added by Apple Configurator 2",
"createdDateTime" : "2023-11-11T11:35:24.811Z",
"serverType" : "APPLE_CONFIGURATOR",
"updatedDateTime" : "2023-11-11T11:35:24.811Z"
}
less /Users/Shared/mdms.json | jq '.data[0].attributes.serverType'
This will give us the server type attribute value. The output will look like this:
APPLE_CONFIGURATOR
Playing around with these commands its easy to format the data in a way that makes it easy to read. For example, the following snippet of code creates a CSV file.
The CSV file when opened in an application like Numbers would look like:
id
Server name
Create date
Server type
98V3T8W934YT8VYNT8TC2483V8239N8323
Devices Added by Apple Configurator 2
2023-11-11T11:35:24.811Z
APPLE_CONFIGURATOR
98W3T4CY49MY9JCFIQWJFCQ3449TH8C43TNV893248C
Jamf Now Server
2020-09-18T11:15:30.248Z
MDM
YTSFWC674Q6CFB76C367C4B67GW6GTXCQ7G46G8Q3
Microsoft Intune
2025-10-06T06:41:22.173Z
MDM
As you can see there are several options when it comes to parsing the output and formatting it in a way that suits us the best.
Downloading the scripts
So there you go. That’s it about the new API’s from Apple. You can try these out for yourself. I have made the scripts available on Github. You can download the scripts from there.
In an earlier article we discussed how to create documentation for Swift projects and frameworks. In this article we will look at how to use DocC to generate documentation for shell scripts.
DocC is a documentation rendering tool built into Xcode that allows developers to easily build documentation for their code. Traditionally, developers would create documentation for their code via comments and then separately create documentation files for reference for other developers. DocC combines the 2 steps into a single step, making it easy for developers to write documentation and for others to read that documentation.
With a little modification we will be able to do the same for our shell scripts.
NOTE: We will be making use of concepts covered in an earlier article. Please make sure you go through that before going through the steps.
Adding DocC documentation for shell scripts
DocC is designed to help app developers easily create documentation for their code. So as such it is geared towards creating documentation for that. However, we can tweak things a little bit and use this process for our scripts.
We will be creating a macOS App project.This will act as a starting point. Don’t worry! We won’t be writing any code. You do not need to learn any programming language for this. So lets begin.
Open Xcode
Create a new macOS app project. Select File > New > Project from the menu bar.
Choose “Command line” from the template wizard.
Call it “FolderCreator” as thats the script we are using in this demo.
Provide your company’s url in reverse format as the Organisation identifier. For example, com.companyname, org.organisationname, and so on.
Choose the language as Swift. Save it on the desktop.
Add the script file to your project. We will be using the script from our scripting series. You can find the script here.
If we go ahead and build the documentation then we won’t find anything helpful. That’s because the documentation compiler is designed to read code and not our script. So it doesn’t contain anything useful. We will have to build the documentation for our script manually. Lets start by adding the documentation catalog. Select File > New > File, under the documentation heading select documentation catalog.
We will add metadata that specifies that this is the documentation for a script. The formatting style is similar to that of markdown documents. Much like the readme files you find in github.
We will add a description, a table listing out the different articles and a topics section that references articles.
Finally we will add a tab navigator to reference external links and resources.
Your completed file should look like:
# ``FolderCreator``
@Metadata {
@TitleHeading("Shell Script")
}
This script is used to create folders.
## Overview
This script is used to create upto 3 folders on the computer.
| Topic | Description | Link |
| --------- | --------------------------------------- | ---------- |
| License | License information | <doc:LicenseInformation> |
| History | Version history | <doc:History> |
| Usage | How to use the script | <doc:Usage> |
| Requirements | Prerequisites for running the script | <doc:Requirements> |
| Installation | How to install the script | <doc:Installation> |
| Help | Getting help | <doc:Help> |
| Script | Script file for reference | <doc:ScriptFile> |
| Man page | Man page file for reference | <doc:ManPage> |
| Download files | Download the different script versions | <doc:DownloadFiles> |
| Learn scripting | Blog article on learning how to write scripts | <doc:LearnScripting> |
## Topics
### Articles
- <doc:Help>
### Downloads
- <doc:DownloadFiles>
### Tutorials
- <doc:LearnScripting>
@TabNavigator {
@Tab("Github page") {
[https://github.com/AmaranthineTech](https://github.com/AmaranthineTech)
}
@Tab("Blog") {
[https://arunpatwardhan.com](https://arunpatwardhan.com)
}
@Tab("Youtube") {
[Amaranthine YouTube Channel](https://www.youtube.com/@amaranthine5616)
}
}
Let us add and article. We will add an article for usage. Click on File > New > File. Select Article from the template wizard.
In the overview add the information about using our script.
Your completed file should look like this:
# Usage
How to run the script
## Overview
- To create folders with default names run the command:
```shell
folderCreator
```
- To define your own folder names:
```shell
folderCreator <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. Provided the man page was installed along with the command.
```shell
folderCreator -h
folderCreator -help
man folderCreator
```
> Important: It is assumed that the man page for the script has been deployed before use. For more information on how to deploy the man page visit [Shell scripting in macOS โ Part 10: Distribution](https://arunpatwardhan.com/2023/02/02/shell-scripting-in-macos-part-10-distribution/). You can also view the <doc:Installation> article.
Similarly add articles for Help, History, License information, Requirements, Learn scripting, Installation, and Download files. You can look at the comments from the script to build the information.
Add an article that will show the raw manpage file.
Copy and paste the entire manpage in that file as an “md syntax.
Your completed page should look like:
# ManPage
@Metadata {
@PageKind(sampleCode)
@CallToAction(url: "https://github.com/AmaranthineTech/ShellScripts/blob/main/folderCreator.1", purpose: link)
}
Man page file
## folderCreator man page
```md
.\"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
```
Also create a page to show the actual script. This is useful. Anyone reading our documentation will also quickly be able to see our script.
And thats it. We have all the necessary information in there. You can download my completed project from the section below. Use it to compare the documentation that you wrote.
Distributing the documentation
There are several ways to distribute the documentation. I have covered those in the previous article. The best option is to host it on a website and make it available to anyone who wishes to use it. That is what I have done. You can view the website here: https://amaranthinescript.github.io/documentation/foldercreator
Use with other scripts
The good thing about the how we created our documentation is that those steps apply to all other kinds of scripts. Instead of our shell script you can add any other script such as AppleScript in there and create documentation for the same.
Yes the process is a bit manual but it does allow script writers to quickly create highly readable documentation.
DocC is a built-in documentation rendering tool that allows developers to easily build documentation for their code. Traditionally, developers would create documentation for their code via comments and then separately create documentation files for reference for other developers. DocC combines the 2 steps into a single step, making it easy for developers to write documentation and for others to read that documentation.
Requirements
This functionality is built into Xcode, so no other tool is required. You may need a hosting server incase you wish to host a web based version of the documentation. In this article we will be using Github pages to host our documentation.
Markdown comments
A key component to generate the documentation are comments that you have written. The comments must be formatted in a particular way so that Xcode can read them and use them to build the documentation. I have already covered this in an earlier article though we will have a look at some of those in a bit.
Creating additional documentation resources
The documentation tools generate documentation based on several resources:
Markdown comments
Type definitions, property declaration, and function declarations
available attribute
All these sources combined together provide a lot of information. But we are not just limited to these sources. We can add 2 other kinds of resources to our projects.:
Articles
Tutorial
Articles allow us to provide a little more context to the documentation. This is where advanced concepts such as functionality, underlying behavior, and things to know are presented to the user. It is possible to add diagrams and pictures to explain the concepts too.
Tutorials on the other hand allow the creator of the code to offer help to anyone who uses the API so that they can learn how to use the different features with the help of step by step instructions.
Both articles and tutorials add to the resources to make the documentation richer and more helpful.
Creating documentation for apps and packages
The process of creating documentation for apps/packages/frameworks is largely similar.
We will be using an example to understand how this goes. I will show only a small snippets of the code/comments here. You can download the completed project at the bottom of the file.
We will take a structured approach towards the design of our documentation.
First we will add the availability attributes
Second we will put comments for our code
Third we will add articles to our code
Fourth we will provide tutorials for our code
Adding availability attributes
Adding @available attributes is an important and useful part of the documentation process. It helps other users of your code know thing like which version of the language is required. What’s the minimum OS version that is required, and so on. All this becomes part of the documentation too. Let us look at how we can do this.
The code below represents a type called author. It’s a complete code but it’s missing the availability attributes. In fact, you should also see Xcode report an error for the link where we use Date.now saying that it’s only available from macOS 12 or later and that we should put an availability attribute for the same.
public struct Author {
public var name : String = ""
public var email : String = ""
public var dateOfBirth : Date = Date.now
public var phone : String = ""
public var photo : Data?
public var website : URL?
}
extension Author : CustomStringConvertible {
public var description: String {
let df : DateFormatter = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium
return """
Author
----------
Name: \(self.name)
Email: \(self.email)
Birthday: \(df.string(from: self.dateOfBirth))
Phone: \(self.phone)
Website: \(self.website?.description ?? "")
"""
}
}
extension Author : Equatable {
public static func ==(lhs : Author, rhs : Author) -> Bool {
lhs.name == rhs.name && lhs.dateOfBirth == rhs.dateOfBirth
}
}
Let’s add the attributes and see how it looks. The code show now look like this:
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
public struct Author {
public var name : String = ""
public var email : String = ""
public var dateOfBirth : Date = Date.now
public var phone : String = ""
public var photo : Data?
public var website : URL?
}
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
extension Author : CustomStringConvertible {
public var description: String {
let df : DateFormatter = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium
return """
Author
----------
Name: \(self.name)
Email: \(self.email)
Birthday: \(df.string(from: self.dateOfBirth))
Phone: \(self.phone)
Website: \(self.website?.description ?? "")
"""
}
}
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
extension Author : Equatable {
public static func ==(lhs : Author, rhs : Author) -> Bool {
lhs.name == rhs.name && lhs.dateOfBirth == rhs.dateOfBirth
}
}
Adding comments
Let us continue with our previous example. Next we will add comments to the code. We will go use the markup features we saw in the earlier article. The comments should provide more details about the type. Things like version, copyright, date created, author, tips, contact details. In the case of functions you can have information about arguments and return types too. Let us add that to our code.
//
// File.swift
//
//
// Created by Arun Patwardhan on 04/07/23.
//
import Foundation
/**
Represents the author of the book.
**Protocols**
Conforms to `CustomStringConvertible` and `Equatable`
- version: 1.0
- note: The `name` and `dateOfBirth` are deemed to be unique properties.
- since: iOS 14, macOS 11
- author: Arun Patwardhan
- copyright: Amaranthine (c) 2023
- date: 3rd July 2023
- requires: Swift 5.x
- Tip: See the article on creating markup comments [Adding formatted Text to Swift](https://arunpatwardhan.com/2017/11/09/adding-formatted-text-to-swift-in-xcode/)
[arun@amaranthine.co.in](mailto:arun@amaranthine.co.in)
*/
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
public struct Author {
public var name : String = ""
public var email : String = ""
public var dateOfBirth : Date = Date.now
public var phone : String = ""
public var photo : Data?
public var website : URL?
}
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
extension Author : CustomStringConvertible {
public var description: String {
let df : DateFormatter = DateFormatter()
df.dateStyle = .medium
df.timeStyle = .medium
return """
Author
----------
Name: \(self.name)
Email: \(self.email)
Birthday: \(df.string(from: self.dateOfBirth))
Phone: \(self.phone)
Website: \(self.website?.description ?? "")
"""
}
}
@available(swift 5.0)
@available(iOS 14, macOS 12, *)
extension Author : Equatable {
public static func ==(lhs : Author, rhs : Author) -> Bool {
lhs.name == rhs.name && lhs.dateOfBirth == rhs.dateOfBirth
}
}
Note that we did not add comments to the extensions of the type. Also, it isn’t always necessary to add comments. Sometimes the types are fairly simple and self explanatory.
Creating the Documentation catalog
Now that we have added comments letโs add the DocC documentation.
In fact, you donโt have to do much. Simply select build documentation and it should create it for you with the code, comments, and availability information that is already there.
To create our documentation just select Product > Build Documentation from the menu.
You can see that already a lot of information is available without having to add any documentation.
The documentation we add will just build on this.
Creating Documentation Catalogs
The resources that we need for documentation are added within a documentation catalog. Items like Articles, Tutorial, Sample code, images are all added to the documentation catalog. These items are used to build our documentation. Let us add a documentation catalog to our project.
With our package open select File > New > File from the menu bar.
Choose the Documentation catalog option from the template wizard.
Click Next.
You should see the catalog added to your project.
Select the file called Documentation within the Documentation folder. This is the top level documentation file. We will place information about the package in here.
Rename this file to match the name of our project.
Next add the code shown below to our file. We will examine the different items in a moment.
# ``AmaranthineLibrary``
The types available in ths package are to be used in applications that work with books and collections of books.
## Overview

In this documentation we will look at the different types available. The idea behind these types is to support the creation of apps that work in different libraries. This should allow all kinds of institutions to quickly develop their own solutions for in-house libraries.
### Types
| Type | Description | | --------- | --------------------------------------- | | `Genre` | This describes the `Genre` of the book. | | `Book` | This represents a single book | | `Author` | This describes the author of the book | | `Library` | This describes the Library type. |
Under the resources folder add any image of your choice. I have an image called “library” and have added that in there.
That’s it for now. Let us look at what we have written.
First up a lot of the formatting you see is similar to markdown style. This means that many thing like headings with a ‘#’, designing tables are already familiar. Let us look at the first line.
# ``AmaranthineLibrary``
The double back tick is used to link to symbols in our project. In this case it is the framework itself. This can be a nested symbol too which is then indicated by a path. We will see an example of this when we create an article for the Author type.
Next we have text description giving us information about the framework. This is followed by the heading for Overview.

This line of code adds an image to the documentation. The text in the square brackets is the description followed by the name of the image file in round brackets. This name is the same as the name used while uploading the image in step 8. It is also possible to provide variations of the image. You can provide images with different scales and support for dark mode as long as you follow the correct naming convention.
<image name>~dark<scale>.<file extension>
For example, I could have provided different version of the library image.
library~dark@2x.png
library@2x.png
The system picks the correct one based on the need.
| Type | Description | | --------- | --------------------------------------- | | `Genre` | This describes the `Genre` of the book. | | `Book` | This represents a single book | | `Author` | This describes the author of the book | | `Library` | This describes the Library type. |
This creates a simple table. A single back tick is used to make the text appear in the code syntax.
- <enum:Genre>
This is another form of a link. This links directly to the Genre type documentation page. There are other ways of creating links to documentation pages. We see them in the code snippet below.
Now we haven’t created these articles yet but this is how we would create links to them. The name matches the name of the article itself. It could also be a link to a tutorial page.
@Small { MIT License }
Finally this allows us to include any fine print text we wish to add to our page. Build the documentation and look at the output. Notice that we get errors for the links to the articles as we have created them yet. For now we will delete the links to the LibraryInformation and the Tutorial table of contents.
Next we will look at creating articles.
Creating Articles
Articles allow us to provide more information about the different types that we have declared in our code. As I mentioned earlier. It allows us to add more information to the existing documentation that has been built. Let us go ahead and create the article for the Book type.
Click on File > New > File from the menu bar.
Select the document type as Article from the template wizard.
Name it BookInformation
Create it.
Add the following code to the article.
# BookInformation
The ``Book`` type represents a single book.
## Overview

The type is built up using several different properties. ``Book/author``, ``Book/genre``, ``Book/pageCount``, ``Book/publishedOn``, ``Book/title``, and ``Book/isbn``. Is a ``Book`` is to be uniquely identified then the ``Book/isbn`` property can be used for the same.
Add an image called ‘book’ to the resources folder.
Let us have a look at the different things added.
# BookInformation
The ``Book`` type represents a single book.
First up we have the title of the article. Then we have its description. Within the description there is a link for the Book type. Included using the double back ticks. This is a good way to help people reading the documentation to directly go over to the type itself.
## Overview

The type is built up using several different properties. ``Book/author``, ``Book/genre``, ``Book/pageCount``, ``Book/publishedOn``, ``Book/title``, and ``Book/isbn``. Is a ``Book`` is to be uniquely identified then the ``Book/isbn`` property can be used for the same.
Then we have the overview title with the image of a book. This is followed by a description along with links to properties within the type. Links to such properties are established using The path approach. Where we first mention the Type followed by a slash followed by the property. The rest of the article lists out the protocols that our type conforms to and the output format incase its to be printed.
Similarly we will add an article for the Author. Create a new article called author information. Add the code below.
The ``Author`` type represents the author of the book.
## Overview

This type is built up using 3 properties: ``Author/name``, ``Author/email``, and ``Author/dateOfBirth``. An instance of ``Author`` is said to be unique if both the ``Author/name`` as well as the ``Author/dateOfBirth`` are unique.
## Information > Note: The ``Author/name`` and ``Author/dateOfBirth`` are deemed to be unique properties.
> Important: Requires Swift 5.x
> Tip: See the article on creating markup comments [Adding formatted Text to Swift](https://arunpatwardhan.com/2017/11/09/adding-formatted-text-to-swift-in-xcode/)
Next we have the metadata. This tell Xcode how to handle the document creation. Should it merge the auto generated documentation with the contents of our article or should the contents of the article override the information. Here we are saying it overrides.
## Information > Note: The ``Author/name`` and ``Author/dateOfBirth`` are deemed to be unique properties.
> Important: Requires Swift 5.x
> Tip: See the article on creating markup comments [Adding formatted Text to Swift](https://arunpatwardhan.com/2017/11/09/adding-formatted-text-to-swift-in-xcode/)
This bit of code is also different. It creates sections for tips, notes, important information. It renders in the documentation with color highlights.
Build the documentation and see how it looks.
There are many other links and formatting options available.
You can control the page layout using tabs, tables.
You can add small disclosure text
Links can be added to specific properties and functions.
Extra top level documents: These are not articles related to a specific type but rather general information about the project.
Availability for the documentation
Comments: Items that are not rendered but are present for the creator of the documentation to take notes
Page appearance
Don’t forget to look at the completed code to see the different kinds of formats that have been used.
Next we will look at creating tutorials.
Creating tutorials
Tutorials as the name suggests are simple guides that walk through the usage of your code. Its a great way to help users of your code to learn how to use the types and functions that you have declared.
Tutorials are easy to create. Lets start of by creating the table of contents file.
Click on File > New > File from the menu bar.
Choose Tutorial Table of Contents.
Give it a name
Create it.
It should come pre-populated with some formatted text to show the table of contents.
Replace that with the code shown below. We will explore the different parts of the text in a moment.
@Tutorials(name: "Using the different types available") { @Intro(title: "How to use the different types") { In this tutorial we will look at creating and using the different types.
@Image(source: library.png, alt: "Library") }
@Volume(name: "Creating types") {
First we will look at how to create instances of the different types. @Image(source: create.png, alt: "Create")
@Chapter(name: "Author") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Author``. @Image(source: author.png, alt: "Author") @TutorialReference(tutorial: "doc:AuthorTutorial") }
@Chapter(name: "Genre") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Genre``. @Image(source: genre.png, alt: "Genre") @TutorialReference(tutorial: "doc:GenreTutorial") }
@Chapter(name: "Book") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Book``. @Image(source: book.png, alt: "Book") @TutorialReference(tutorial: "doc:BookTutorial") }
@Chapter(name: "Library") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Library``. @Image(source: library.png, alt: "Library") @TutorialReference(tutorial: "doc:LibraryTutorial") } }
@Volume(name: "Working with the library") { Next we will look at how all the types work together as a part of the library.
@Image(source: assemble.png, alt: "Assemble")
@Chapter(name: "Working with the library") { In this chapter we look at how to use the ``AmaranthineLibrary/Library`` object. @Image(source: library.png, alt: "Library") @TutorialReference(tutorial: "doc:UsingTheLibraryTutorial") } }
@Resources { Explore more resources for learning about the different features that we have used in Swift.
@Videos(destination: "https://www.youtube.com/channel/UC127UHd8V7bxPQYnd9QrN8w") { To view various blog articles and videos.
- [My Blog](https://www.arunpatwardhan.com/) }
@SampleCode(destination: "https://github.com/AmaranthineTech") { Download and explore sample code projects.
@Tutorials(name: "Using the different types available")
Right at the top we have the title for the tutorial. All the chapter and volume listings are within this block.
@Intro(title: "How to use the different types") { In this tutorial we will look at creating and using the different types.
@Image(source: library.png, alt: "Library") }
Then we have the introduction for the tutorial. Here we can give a brief introduction about the tutorial itself. We can add artwork to help illustrate things for the user.
@Volume(name: "Creating types") {
First we will look at how to create instances of the different types. @Image(source: create.png, alt: "Create")
@Chapter(name: "Author") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Author``. @Image(source: author.png, alt: "Author") @TutorialReference(tutorial: "doc:AuthorTutorial") }
... }
Next we provide the list of chapters. We can directly provide the list of chapters, or, if our tutorial covers different sections we can have multiple volumes each with a list of chapters. That is what I have done in this example.
The name of the volume, some text explains what is covered in this volume. With the chapter blocks in it. The chapters have a similar structure with name, text, image, and a link to the tutorial document.
@Resources { Explore more resources for learning about the different features that we have used in Swift.
@Videos(destination: "https://www.youtube.com/channel/UC127UHd8V7bxPQYnd9QrN8w") { To view various blog articles and videos.
- [My Blog](https://www.arunpatwardhan.com/) }
@SampleCode(destination: "https://github.com/AmaranthineTech") { Download and explore sample code projects.
At the end there is a resources block. This is a great place to put links to other resources that the reader may find useful. These can be categorized to give the reader more information. Here are some of the categories:
Documentation
Sample code
Videos
Forums
Downloads
Each of these can contain multiple links. Before we build the documentation let us add a tutorial document. In order to do that let us remove the extra volumes and chapters from the table of contents for the moment. This can be added later. Your final code should look like:
@Tutorials(name: "Using the different types available") { @Intro(title: "How to use the different types") { In this tutorial we will look at creating and using the different types.
@Image(source: library.png, alt: "Library") }
@Volume(name: "Creating types") {
First we will look at how to create instances of the different types. @Image(source: create.png, alt: "Create")
@Chapter(name: "Book") { In this chapter we look at how to create objects of type ``AmaranthineLibrary/Book``. @Image(source: book.png, alt: "Book") @TutorialReference(tutorial: "doc:BookTutorial") } }
@Resources { Explore more resources for learning about the different features that we have used in Swift.
@Videos(destination: "https://www.youtube.com/channel/UC127UHd8V7bxPQYnd9QrN8w") { To view various blog articles and videos.
- [My Blog](https://www.arunpatwardhan.com/) }
@SampleCode(destination: "https://github.com/AmaranthineTech") { Download and explore sample code projects.
Then we have the steps within the @Steps block. Each step is in its own @Step block. Note the difference between the two. The outer one is @Steps to indicate it holds a series of steps. Inside this is the @Step which represents a single step.
Each step contains the description for that step along with its @Code block. The way the tutorial works is that it walks the reader through a series of tasks that it performs. What is to be done is described in the text and a sample preview for the code is generated through the code file mentioned in the code block.
We will need to upload a series of code files. Each file contains additional code. Listing them in sequence generates the flow. Add the following files to the resources folder of your documentation. Name them BookCodeFile.swift, BookCodeFile-1.swift, BookCodeFile-2.swift, BookCodeFile-3.swift, and BookCodeFile-4.swift.
BookCodeFile.swift
//
// BookCodeFile.swift
//
//
// Created by Arun Patwardhan on 09/08/23.
//
import AmaranthineLibrary
let authorName : String = "Arun"
let authorEmail : String = "arun@mail.com"
let authorDOB : Date = Date(timeIntervalSince1970: 123456789)
let authorPhone : String = "9182736450"
let authorLink : URL = URL(string: "https://arunpatwardhan.com")
let arun : Author = Author(name: authorName,
email: authorEmail,
dateOfBirth: authorDOB,
phone: authorPhone,
photo: nil,
website: authorLink)
BookCodeFile-1.swift
//
// BookCodeFile.swift
//
//
// Created by Arun Patwardhan on 09/08/23.
//
import AmaranthineLibrary
let authorName : String = "Arun"
let authorEmail : String = "arun@mail.com"
let authorDOB : Date = Date(timeIntervalSince1970: 123456789)
let authorPhone : String = "9182736450"
let authorLink : URL = URL(string: "https://arunpatwardhan.com")
let arun : Author = Author(name: authorName,
email: authorEmail,
dateOfBirth: authorDOB,
phone: authorPhone,
photo: nil,
website: authorLink)
let bookGenre : Genre = Genre.educational
BookCodeFile-2.swift
//
// BookCodeFile.swift
//
//
// Created by Arun Patwardhan on 09/08/23.
//
import AmaranthineLibrary
let authorName : String = "Arun"
let authorEmail : String = "arun@mail.com"
let authorDOB : Date = Date(timeIntervalSince1970: 123456789)
let authorPhone : String = "9182736450"
let authorLink : URL = URL(string: "https://arunpatwardhan.com")
let arun : Author = Author(name: authorName,
email: authorEmail,
dateOfBirth: authorDOB,
phone: authorPhone,
photo: nil,
website: authorLink)
let bookGenre : Genre = Genre.educational
let bookStyle : BookStyle = BookStyle.paperback
BookCodeFile-3.swift
//
// BookCodeFile.swift
//
//
// Created by Arun Patwardhan on 09/08/23.
//
import AmaranthineLibrary
let authorName : String = "Arun"
let authorEmail : String = "arun@mail.com"
let authorDOB : Date = Date(timeIntervalSince1970: 123456789)
let authorPhone : String = "9182736450"
let authorLink : URL = URL(string: "https://arunpatwardhan.com")
let arun : Author = Author(name: authorName,
email: authorEmail,
dateOfBirth: authorDOB,
phone: authorPhone,
photo: nil,
website: authorLink)
let bookGenre : Genre = Genre.educational
let bookStyle : BookStyle = BookStyle.paperback
let bookTitle : String = "Introduction to Swift"
let bookISBN : String = "34243-3433-2"
let pageCount : Int = 987
let publicationDate : Date = Date(timeIntervalSince1970: 9876543210)
BookCodeFile-4.swift
//
// BookCodeFile.swift
//
// BookCodeFile.swift
//
//
// Created by Arun Patwardhan on 09/08/23.
//
import AmaranthineLibrary
let authorName : String = "Arun"
let authorEmail : String = "arun@mail.com"
let authorDOB : Date = Date(timeIntervalSince1970: 123456789)
let authorPhone : String = "9182736450"
let authorLink : URL = URL(string: "https://arunpatwardhan.com")
let arun : Author = Author(name: authorName,
email: authorEmail,
dateOfBirth: authorDOB,
phone: authorPhone,
photo: nil,
website: authorLink)
let bookGenre : Genre = Genre.educational
let bookStyle : BookStyle = BookStyle.paperback
let bookTitle : String = "Introduction to Swift"
let bookISBN : String = "34243-3433-2"
let pageCount : Int = 987
let publicationDate : Date = Date(timeIntervalSince1970: 9876543210)
let swiftTextBook : Book = Book(title: bookTitle,
author: arun,
publishedOn: publicationDate,
isbn: bookISBN,
pageCount: pageCount,
genre: bookGenre,
format: bookStyle)
Build the documentation and see how it renders the tutorial. It should look like this:
You can add preview images to your tutorial too to give a visual preview for your code. This is really useful when you are creating tutorials for UI based elements.
Adding assessments
One nice feature of tutorial is the ability to add assessments.
Assessments are a good way of helping readers determine if they have understood specific aspects of the code well. Itโs also a good way to drive home key concepts related to the code.
Assessments are added to the tutorial and is located at the bottom of the tutorial section. Add the following to our Book Tutorial:
@Assessments { @MultipleChoice {
Which of the following types is not used while creating an instance of a `Book`?
@Choice(isCorrect: false) { `float`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } }
@Choice(isCorrect: false) { `Data`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } }
@Choice(isCorrect: true) { `Author`
@Justification(reaction: "That's right!") { A `Book` has an `Author`. } }
@Choice(isCorrect: false) { `Bool`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } } } }
Let us explore each item in this.
@Assessments {
First we have our assessments block. All our multi choice questions go in here.
@MultipleChoice {
Which of the following types is not used while creating an instance of a `Book`?
Then we have the multi choice block along with the question itself.
@Choice(isCorrect: false) { `float`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } }
A MulitpleChoice block contains 2-4 choices. Each choice is represented with its own @Choice block. A choice block has a boolean flag indicating if its the right answer, the choice value, and a hint in the form of a justification to guide the reader to the correct value in case the choice isn’t correct.
Your complete tutorial should now look like:
@Tutorial(time: 10) { @Intro(title: "Creating an instance of Book.") { We will look at the steps involved in creating an instance of Book. }
@Section(title: "Create an Book object") { @ContentAndMedia { We will look at the steps involved in creating an instance of Book.
Which of the following types is not used while creating an instance of a `Book`?
@Choice(isCorrect: false) { `float`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } }
@Choice(isCorrect: false) { `Data`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } }
@Choice(isCorrect: true) { `Author`
@Justification(reaction: "That's right!") { A `Book` has an `Author`. } }
@Choice(isCorrect: false) { `Bool`
@Justification(reaction: "Try again!") { Have a look at the `Book` type to see what has been used. } } } } }
Build the documentation. Explore the tutorial and its assessment. It should look like this. The incorrect answers are highlighted in red while the correct one is in green.
That’s it. That covers the basic elements of creating documentation and tutorials for your code. Don’t forget to look at the completed code below.
Top level documentation and other markdown attributes
There are many kinds of attributes available for markdown. We have already seen some of them above. Lets look at a few more.
@MetaData
This attribute allows us to specify how DocC should build this document. Here are some of the items you can mention in there:
Attribute
Description
Possible values
@DocumentationExtension
Used to indicate if the contents of the article should override the default documentation or be appended to it.
override, append
@PageColor
Used to specify the color to be used for the banner at the top of the page
blue, gray, green, orange, purple, red, yellow
@TechnologyRoot
Used to indicate that this is a top level document and that it is not related to any specific type or code in the framework. This is useful when we want to provide some other information not related to the API in question.
@Available
Indicates the availability of the documentation itself.
Platform: iOS, macOS, watchOS, tvOS
@CallToAction
This is used to provide links to resources or downloads associated with that particular page.
Purpose argument can have: download, link
@PageKind
Used to specify if the page added is an article or a sample code that is being displayed.
article, samplecode
@PageImage
Used to provide an image for the page.
Purpose argument can have: icon, card
@DisplayName
Used to provide a custom name for a page rather than the symbol’s name.
String
@SupportedLanguage
Used to specify which programming language supports the specific feature.
swift, objc, objective-c
@Options
Similarly we can configure some options for the documentation. This controls how the documentation is rendered. It could be for a specific page or for all the pages in the API. Here are some of the options that we can configure.
Attribute
Description
possible values
@AutomaticSeeAlso
Used to indicate if the see also section is automatically created or not.
enabled, disabled
@AutomaticTitleHeading
Used to indicate if the title head is automatically created or not.
enabled, disabled
@TopicsVisualStyle
Used to specify how the topics on a page should be shown.
list, compactGrid, detailedGrid, hidden
@AutomaticArticleSubheading
Used to indicate if the article subheading is automatically created or not.
The above code generates a systematic structure like this:
@TabNavigator
We can offer information on a page with the help of a tab navigator too. This allows us to quickly show multiple options or related information in a structured way.
@TabNavigator { @Tab("add") {  }
@Tab("assemble") {  }
@Tab("author") {  }
@Tab("book") {  }
@Tab("checkout") {  }
@Tab("create") {  }
@Tab("find") {  }
@Tab("genre") {  }
@Tab("library") {  } }
This renders it as:
If there are fewer tabs then it renders slightly differently.
This renders into a simple list of links. You can choose to have it in a compactGrid style or detailedGrid style.
@Small
There is also a way to add small disclaimer or licensing text using the @Small block.
@Small { MIT License
Copyright (c) 2015 Amaranthine
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.
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. }
This renders it as:
@Comment
Just like we can have comments in our code, we can have comments for our documentation too. The documentation builder does not render them and it is only meant for the author(s) of the documentation. This is a good way to put notes in for things that need to be done.
@Comment { Dont forget to change the name of this file. }
Exporting documentation
Now that we have seen different ways of documenting our code its time to start sharing it with our users. Of course when ever users of our package add the package to their project they can simply build the documentation as we have been doing so far. But in some situations users would like to go through the documentation before hand or would like to access it to check something. It is possible to export our documentation to make it accessible to them.
There are a couple of ways of exporting our documentation:
Directly export the documentation from the graphical user interface
Using the docc command from the command line interface
Let us look at both.
Exporting the documentation via the GUI
First build the documentation for your project.
Select the top level documentation file from the documentation window.
Hover over the right hand side of the documentation name. You should see a more button with 3 dots appear.
Click on the 3 dots and choose “Export”
Choose where you wish to save the archive.
Export it.
Now open the archive by double clicking on the file.
You should see the same documentation but under the imported catalog section.
Export using the command line
First make sure that your project is allready pushed and commited to the github archive.
Now we will be using the Swift-DocC plugin to generate the documentation. We need to add it as a dependency to the Swift Package. Update the Package.swift file to include the dependency.
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "AmaranthineLibrary",
platforms: [
.iOS(.v14),
.macOS(.v11),
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "AmaranthineLibrary",
targets: ["AmaranthineLibrary"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "AmaranthineLibrary",
dependencies: []),
.testTarget(
name: "AmaranthineLibraryTests",
dependencies: ["AmaranthineLibrary"]),
]
)
Next we will run the swift command to generate documentation. Run the following command in your package folder.
swift package generate-documentation --source-service github --source-service-base-url https://github.com/AmaranthineTech/AmaranthineLibrary/blob/main --checkout-path /Users/instructor/Developer/AmaranthineLibrary/
Update the paths to match your own implementation. I have cloned the git repository in the /Users/instructor/Developer/ folder.
When you run the command it will tell you where the doccarchive is saved.
Copy the doccarchive and share it.
Open it to view the links to the different files. These links are generated thanks to the --source-service and --source-service-base-url options.
The links to the files should look like this:
This is one of the big advantages of generating the archive via the command line. You could also use the xcodebuild and xcrun to generate the documentation too.
Hosting the documentation
Exporting documentation is one way of sharing the documentation with users. But it would be even better if we could publish it as a webpage. Let us look at how to do that.
There are a couple of ways of publishing the documentation to a website.
File server
Web server with custom routing
Static pages on github
We will look at how to host them as static pages on github.
In order to host static pages on Github you will need a Github account. You can create one for free if you want.
There are 3 broad steps involved in hosting our documentation webpage on Github.
Creating the Github repository for hosting the webpages
Generating the publishable version of our documentation
Uploading the documentation to Github.
Let’s look at those 3 steps in detail.
Step 1: Creating a Github repository for hosting the web pages.
We are going to use a feature called Github Pages. As explained on the website:
GitHub Pages is a static site hosting service that takes HTML, CSS, and JavaScript files straight from a repository on GitHub, optionally runs the files through a build process, and publishes a website.
Github Documentation
There are 3 types of sites that can be hosted:
Type
Description
Sample URL
Project
The site is connected to a project on Github
User
The site is hosted in a repository owned by a personal user account.
<username>.github.io
Organisation
The site is hosted in a repository owned by an organisation account.
<organisation>.github.io
Depending on your needs you can go in for any one of those. For this demo we will be going in for an Organisation site.
The name of the repository will be in the format mentioned in the sample url above. So let us go ahead and create one.
Create an organisation on Github if needed. You can use an existing one if you want.
Next we will create a repository to host our website. Click on repository and create new.
We need to provide the name of our repository. It should follow the format: <organisation name>.github.io .
Provide a description, this is optional.
Set the site as public or private depending on your requirements.
Create the repository.
Navigate to the repository
Go to settings
Select the pages tab.
Make sure “Deploy from a branch” is sected under source.
Under branch select main and the folder as /root.
Your pages screen should look like this:
Go back to your code section of the repository.
Add a new file called readme.md. Put some basic text in it.
Switch back to the settings > pages section of the repository.
You should see a link to the repository.
Click on Visit site. You should see your readme.md file open in your browser. We will be replacing it with our docc documentation.
Let us clone this repository on our computer.
Now we can add our files there. Run the following commands. I will be creating the repository in the ~/Developer folder on my computer.
cd ~/Developer
git clone https://github.com/AmaranthineLibrary/amaranthinelibrary.github.io
cd amaranthinelibrary.github.io
mkdir docs
Step 2: Generating a publishable version of our documentation
Now that our repository is ready we will generate the documentation.
There are couple of ways of generating that documentation. However, we will simply extract it from the archive we created previously.
There are 2 because i added a top level documentation.
Final thoughts
There you go. We have successfully created, viewed, exported, and hosted documentation for our API using Swift DocC.
As you can see the process is fairly simple and straightforward. Yes, it does appear like its a lot of work, but building this practice will go a long way in making your code more useful, and easy to understand for anyone that’s using your code.
The best way to work with DocC is to start writing the comments, availability attributes, articles, and tutorials as you develop your code. This is far better than leaving it as a standalone activity.
So go ahead, use DocC in as many places as possible, even app projects. It will make life very simple.
Download
You can download the Swift Package Manager project here.
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.
Use the export command
Source another script file into your script
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.
Create a script, message.bash and save it in the /Users/Shared/Scripts/.
#!/bin/bash
echo "$(date) This is a random script $RANDOM"
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
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.
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:
NAME
SYNOPSIS
DESCRIPTION
VERBS
REQUIREMENTS
INSTALLATION
USAGE
WARNINGS
EXIT STATUS
EXAMPLES
DIAGNOSTICS
COPYRIGHT
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.
Macro
Description
.Dd
This is used to specify the date when the man page was created/published.
.Dt
This is used to specify the title for the man page. Its value should always be in all caps.
.Os
The name of the operating system.
.Sh
Section header name,
.Nm
The name of the command. This is the name that will be used throughout the document.
.Nd
The description of the command.
.Ar
Arguments being passed to a command
.Op
Options being passed to a command
.Pp
New paragraph
.Bl
Start a list
.El
End the list
.Ev
For environment variables
.It
Italics
.Ss
Subsection
.An
Name of the author
.Sy
Symbolic 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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.
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.
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 Daemons
Launch Agents
These are owned by the system
These are owned by the currently logged in user
Start as soon as the system boots up
Starts only after the user logs in. Stops as soon as the user logs out
Is system wide
Can 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.
Key
Description
Label
This is the name of our Launch Agent
ProgramArguments
This is the action to be performed. In our case the action is to invoke the script.
StartInterval
This is the time interval after which the program arguments must be executed.
RunAtLoad
This 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.
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 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.
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.
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.
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.
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This will simply show a popup to the user with a message and 2 buttons. We can customise it further:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
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.
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
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.
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:
For loop
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.
First we will remove the variables that store the names for the hidden files.
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.
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
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.
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 --------------------------------------------------
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
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.
Operator
Description
Example
>
Writes the output of the preceding command to the file
echo "ABC" > file
>>
Appends information to the file being pointed to another file
echo "ABC" >> file
|
Passes the output of the preceding command to the next command
ls -l | grep "*.sh"
Using the above redirections there are some interesting actions that we can perform.
Action
Description
command >> /dev/null
This will completely discard the output of the command.
command 2>&1
This will redirect stderr to stdout and show both together on stdout.
command 1>&2
This 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:
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
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.
Performing tasks conditionally
So far our script has been performing tasks uninterrupted one after the other. But often times you will come across a situation where you need to perform some checks before going ahead.
The main reason why we would want to perform checks is to make sure that certain criteria are met or if certain resources are present.
Only if these conditions are satisfied will we proceed ahead. Or take an alternative course of action incase the condition isnโt met.
We can find all these checks in the man page for the test command.
Let us look at some of those checks.
Test operations
You can run the following command to view all the operations possible.
man test
There are different comparison operations possible.
The – followed by a letter and then the file name allows us to check for different aspects of a file. Such as if it exists, whether it is a directory and moreโฆ
We can even compare files with each other.
We can compare strings.
And we can compare numbers.
Conditional code
Now that we have seen the different kinds of condition checks available. Let us explore how we can use the condition checks.
If statement
The if statement has various forms. We will look at the simplest one first.
if [[ -d "$HOME/Applications" ]]; then
echo "The applications folder exists in the home folder."
fi
If else statement
If-elif-else statement
Switch on case statement
Modify our code
We will be adding checks to make sure that the arguments passed in contain values. We will also check to see if the folders exist before trying to create them.
#!/bin/zsh
echo "$(date) Running script $0 to create folders."
TOOLS_FOLDER="Tools"
REPORTS_FOLDER="Reports"
HELP_FOLDER="Help"
if [[ $1 != "" ]]; then
TOOLS_FOLDER=$1
fi
if [[ $2 != "" ]]; then
REPORTS_FOLDER=$2
fi
if [[ $3 != "" ]]; then
HELP_FOLDER=$3
fi
TOOLS_FOLDER_CREATED=".$TOOLS_FOLDER-FolderCreated"
REPORTS_FOLDER_CREATED=".$REPORTS_FOLDER-FolderCreated"
HELP_FOLDER_CREATED=".$HELP_FOLDER-FolderCreated"
TODAY=$(date)
PATH_TO_LOG="$HOME/Library/Logs/folderCreator_log_v1-1.log"
echo "$(date) Starting" >> $PATH_TO_LOG
cd $HOME
echo "$(date) Creating folders: $TOOLS_FOLDER, $REPORTS_FOLDER, $HELP_FOLDER" >> $PATH_TO_LOG
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
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!"
Your completed code should look like.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters