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
The example 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
/usr/bin:/bin:/usr/sbin:/sbin
Note how the PATH
in this context is different from the default macOS PATH
in Terminal and also different from your PATH
.
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 PATH
.
Solutions to the PATH confusion
The 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
PATH
in 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
Update: 2020-08-25 changed some of the sample commands.
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/sh
systemsetup -settimezone "Europe/Amsterdam"
you have to use:
#!/bin/sh
/usr/sbin/systemsetup -settimezone "Europe/Amsterdam"
When you do not know the path to a command you can use the which
command to get it:
$ which systemsetup
/usr/sbin/systemsetup
Note: the
which
command 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 resultingPATH
starts with one of the standard directories (/usr/bin
,/bin
,/usr/sbin
, or/sbin
) you should be fine. But if a differentPATH
is 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.
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 PATH
.
This should be the first line after the shebang in a script:
#!/bin/sh
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
systemsetup -settimezone "Europe/Amsterdam"
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
PATH
in 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 echo
in /usr/bin
it would be called instead of the expected /bin/bash
.
However, on macOS the four standard
locations (/usr/bin
, /bin
, /usr/sbin
, /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
. The PATH
seen in the installer context does not include /usr/local/bin
, either.
/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.
Other Tools
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
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki
or Jamf
export PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/jamf/bin
Since the third party folders are not protected by SIP, it is safer to append them at the end of 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.
For example:
#!/bin/sh
# 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.
Summary
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.
/bin/echo & echo are not equivalent. echo is a shell builtin. /bin/echo is a completely different binary with different options, so, that is not a good example of what you are talking about.
@Fred Johnsen
could you gently explain how MUCH completely different are the two commands please?
They should not be different in behavior, the binaries for the built-ins exist as fallbacks on disk for some weird edge case situations. However, @Fred Johnson is correct that this was a poor example in this case.
if you set your PATH at the beginning of the script, you should not worry about this ever, because the shell will choose the built-in over the binary on disk, which is the behavior, you want.