A discussion that comes up frequently on MacAdmin Slack and other admin discussions is:
Should commands in scripts have their full path hardcoded or not?
Or phrased slightly differently, should you use
/bin/echo or just
echo in your admin scripts?
I talked about this briefly in my MacSysAdmin session: Scripting Bash
Why can’t I just use the command?
When you enter a command without a path, e.g.
echo, the shell will use the
PATH environment variable to look for the command.
PATH is a colon separated list of directories:
$ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
The shell will look through these directories in order for the given command.
You can read more detail about the
PATH and environment variables in these posts:
PATH is Unreliable
PATH above is the default on macOS on a clean installation. Yours will probably look different – mine certainly does:
$ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Applications/VMware Fusion.app/Contents/Public:/usr/local/munki:/Users/armin/bin
Third party applications and tools can and will modify your
PATH. You yourself might want to change your
PATH in your shell profile.
But on top of that, the
PATH will be different in different contexts. For example, open the Script Editor application, make a new script document, enter
do shell script "echo $PATH" and run the script by hitting the run/play button.
The small AppleScript we just built runs the shell command
echo from the AppleScript context. The result is
Note how the
PATH in this context is different from the default macOS
PATH in Terminal and also different from your
I also built a simple payload-free installer package which only runs the
echo "Installer PATH: $PATH" command. You will have to search through
/var/log/install.log to get the output:
installd[nnn]: ./postinstall: Installer PATH: /bin:/sbin:/usr/bin:/usr/sbin:/usr/libexec
Which is yet another, different
Solutions to the PATH confusion
PATH may be different in different contexts that your script may run in.
That means we cannot be consistently certain if a command will be or which command will be found in a script in different contexts. This is, obviously, bad.
Mac system adminstrative scripts which can run in unusual contexts, such as the Login Window, NetInstall, the Recovery system or over Target Disk Mode and usually run with root privileges. You really want to avoid any uncertainty.
There are two solutions to make your scripts reliable in these varying contexts:
- hardcode the full path to every command
- set the
PATHin the script
Both are valid and have upsides and downsides. They are not exclusive and can be both used in the same script.
Going Full PATH
You can avoid the unreliability of the
PATH by not using it. You will have to give the full path to every command in your script. So instead of
#!/bin/bash echo "hello world"
you have to use:
#!/bin/bash /bin/echo "hello world"
When you do not know the path to a command you can use the
which command to get it:
$ which defaults /usr/bin/defaults
whichcommand evaluates the path to a command in the current shell environment, which as we have seen before, is probably different from the one the script will run in. As long as the resulting
PATHstarts with one of the standard directories (
/sbin) you should be fine. But if a different
PATHis returned you want to verify that the command is actually installed in all contexts the script will run in.
Using full paths for the commands works for MacAdmin scripts because Mac administrative scripts will all run on some version of macOS (or OS X or Mac OS X) which are very consistent in regard to where the commands are stored. When you write scripts that are supposed to run on widely different falvors of Unix or Linux, then the location of certain commands becomes less reliable.
Also note that some elements of shell scripting are commands, even if they don’t look like it. For example the square bracket
[ is actually a command:
$ which [ /bin/[
so if you are using full command paths, you should use
if /bin/[ … instead of
if [ ….
(On the other hand, you should be using double brackets
[[ instead of single brackets for tests, since they are safer and more flexible, but that is a topic for another post.)
Choosing your own PATH
The downside of hardcoding all the command paths is that you will have to memorize or look up many command paths. Also, the extra paths before the command make the script less legible, especially with chained (piped) commands.
If you want to save effort on typing and maintenance, you can set the
PATH explicitly in your script. Since you cannot rely on the
PATH having a useful value or even being set in all contexts, you should set the entire
This should be the first line after the shebang in a script:
#!/bin/bash export PATH=/usr/bin:/bin:/usr/sbin:/sbin echo "hello world"
Note: any environment variable you set in a script is only valid in the context of that script and any sub-shells or processes this script calls. So it will not affect the
PATHin other contexts.
This has the added benefit of providing a consistent and well known
PATH to child scripts in case they don’t set it themselves.
The downside of this is that even with a known
PATH you cannot be entirely sure which tool will be called by the script. If something installed a modified copy of
/usr/bin it would be called instead of the expected
However, on macOS the four
standard locations (
/sbin, as well as the less standard
/usr/libexec) are protected by System Integrity Protection (SIP) so we can assume those are ‘safe’ and locked down.
/usr/local/bin is a special case
But notice that I do not include
/usr/local/bin when I set the
PATH for my scripts, even though it is part of the default macOS
PATH seen in the installer context does not include
/usr/local/bin is a standard location where third party solutions can install their commands. It is convenient to have this directory in your interactive
PATH under the assumption that when you install a tool, you want to use it easily.
However, this could create conflicts and inconsistent results for administrative scripts. For example, when you install
bash version 4, it will commonly be installed as
/usr/local/bin/bash, which (with the standard
PATH) overrides the default
/bin/bash version 3.
Since you chose to install bash v4, it is a good assumption that you would want the newer version over the older one, so this is a good setting for the interactive shell.
But this might break or change the behavior of administrative scripts, so it is safe practice to not incluse
/usr/local/bin in the
PATH for admin scripts.
When you use commands from other directories (like
/usr/libexec/PlistBuddy, or third party tools like the Munki or Jamf tools) then it is your choice whether you want to use full path for these commands or (when you use the commands frequently in a script) add their directory to the
PATH in your script:
E.g. for Munki
Since the third party folders are not protected by SIP, it is safer to append them to the PATH, so they cannot override built-in commands.
Commands in Variables
Another solution that is frequently used for single commands with long paths is to put the entire path to the command in a variable. This keeps the script more readable.
#!/bin/bash # use kickstart to enable full Remote Desktop access # for more info, see: http://support.apple.com/kb/HT2370 kick="/System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart" #enable ARD access $kick -configure -access -on -users remoteadmin,localadmin -privs -all $kick -configure -allowAccessFor -specifiedUsers $kick -activate
Note: that you need to quote the variable when the path to the command contains spaces or other special characters.
As a system administrator it is important to understand the many contexts and environments that a script might be run in.
Whether you choose to write all command paths or explictly set the
PATH in the script is a matter of coding standards or personal preference.
You can even mix and match, i.e. set the
PATH to the ‘default four’ and use command paths for other commands.
My personal preference is the solution where I have to memorize and type less, so I set the
PATH in the script.
Either way you have to be aware of what you are doing and why you are doing it.