Scripts, no matter which language they are written in, are an incredibly important part of a MacAdmin’s tool kit.
Most posts, books and tutorials focus on how to write scripts. Admittedly, the skill to make the computer and all its software running do what you want is very important. But once you have a working script, you will have to consider how to launch the script at the right time and with the right input.
When I started to think about this, I realized macOS provides many ways to launch or schedule scripts. Some are fairly obvious, others are quite obscure and maybe even frivolous.
Over the next few weeks (i.e. when I find time), I will publish a series of posts describing various ways of launching scripts and workflows on macOS. This post starts with the most obvious: launch a script from Terminal.
The Basics: Launch from Terminal
It might seem trivially obvious that you can launch scripts and workflows from Terminal. Nevertheless, it does require some explanation and context. Launching from Terminal is the most basic form of launching a script. As such, it defines the ‘baseline’ experience of how we expect scripts to run.
When you enter something into a Terminal prompt and hit the return key, the shell will split the text on whitespace. The first element of the text is the command. If that text contains a
/ character, the shell will interpret the entire element as a path to the executable. The path can be relative or absolute.
If the first element does not contain a
/ then the shell will first look for functions, aliases and built-in commands, in that order. Then the shell will search for a command executable using the colon-separated paths in the
PATH environment variable. It will use the first executable it finds.
If the shell fails to find something to execute, it will show an error.
This is why you need to the
./ to execute scripts in the current working directory. Even though the
./ seems redundant, it tells the shell to look for the executable in the current working directory, instead of using the
Scripts are ‘just’ text files. Two things distinguish them from normal text files.
First, the executable bit is set to tell the system this file contains executable code. You do this with the
chmod +x my_script.sh command. Here comes the trick, though. The text in the script file isn’t really executable code that the CPU can understand directly. It needs an ‘interpreter’ which is another program that, wells, interprets the text file and can tell the CPU what to do.
The interpreter for a script file is set in the first line of the script with the
#! character combination. The
#! is also called ‘hashbang’ or ‘shebang.’ The shebang is followed by an absolute path to the interpreter, e.g.
Note: the shebang has to be in the very first line (actually the first characters) of the script, you cannot have comments (or anything else) before it.
Note: you often see shebangs using the
env command in the form
#!/usr/bin/env bash. These are useful for cross-platform portability, but have trade-offs. I have an article just for that.
The shell is only interested in the first part of the command line text you entered. The entire list of parts (or elements, split on whitespace) are passed into the command as its arguments. A shell script sees the first element, the command or path to the command as
$0 and the the remaining arguments as
$2, and so on.
Other languages will handle this in a similar fashion, arrays or lists of arguments are common.
Shells also have environment variables. We already talked about the role of the
PATH variable in the command lookup. In Terminal, you can list all variables in your current environment with the
env command. Some of these environment variables can be extremely useful, like
When you launch a script from Terminal, a new shell environment is created and all of the environment variables are copied. This is great, beacuse the script can access the environment variables, and might even change them. But the changes in the script’s sub-shell, will not affect the current shell. In other words, a script can change the
PATH environment variable in its own context, without changing yours.
In other Unix-based systems, environment variables are a common means of providing data to apps and processes. In the interactive shell on macOS, environment variables are very useful for this purpose. We will see, however, that when you launch processes and scripts through other means, environment variables can be a challenge on macOS.
Note: A shell environment also contains shell options, which are also inherited to sub shells.
Input and Output
Shell scripts and tools also have input and output. When you launch a script from the the Terminal, both output streams (Standard Output and Standard Error) are connected to the Terminal, so the output will be shown in the Terminal window.
When you use a script with pipes, then its Standard Input (stdin) will be connected to the previous tool’s Standard Out (stdout). The last script’s stdout and stderr will be shown in Terminal.
One thing that is special about interactive Terminal input and output, is that it happens while the script or tool is running. That means you can get live updates on the progress.
When we launch scripts in other contexts, their input and output may be buffered. This means that the system waits for the script or tool process to complete before piping its output to the next tool or to the process that called it. This is not something you usually have to worry about, but again it is something you should have in mind when running scripts in non-Terminal contexts.
While glaringly obvious, launching a script from Terminal does have some intricacies. This post sets a ‘baseline’ for how scripts work on macOS. In future installments, we will re-visit some of these topics, and the differences will become relevant when we launch scripts in contexts other than an interactive shell.
In the next post, learn how to create a double-clickable file to launch a script from Finder.