Advanced Quoting in Shell Scripts

Quoting strings and variable substitutions is a bit of a dark art in shell scripts. It looks simple and straightforward enough, but there are lots of small devils in the details, that can come out and haunt you.

Basics: why we quote strings

In shell scripts (sh, bash, and zsh) you use the equals character = to assign a string value to a variable:

> name=John
> dirpath=/Library

As long as there are no special characters in the literal string, there is no need to quote the string.

When you use the variable, you prefix a $ symbol:

> echo $name
John
> cd $dirpath
> pwd
/Library

When the literal string contains special characters, you need to either escape the special characters with the backslash \ or quote the entire string with either single quotes ' or double quotes ". Space is proverbial ‘killer character’, especially for file paths. (More details in this post.)

name='John Doe'
dirpath="/Library/Application Support"

The difference between single quotes and double quotes is important. Single quotes escape every special character except the single quote itself. A single quoted string of '#$"\!' will represent exactly those characters.

Double quotes escape most characters, except the double quote " the backtick `, the dollar sign $, the backslash \, and the exclamation mark !. (There are slight differences between the shells on this.)

This allows us to use old-style command substitution with backticks and variable substitution (dollar sign) within double quoted strings:

> echo "Hello, $name"
Hello, John Doe
> echo "The Computer Name is `scutil --get ComputerName`"

Though you should be using the $(…) syntax for command substitution instead of backticks `. The parenthesis syntax is more readable and can be nested.

In general, it is a good rule to always quote literal strings. Whether you should use double quotes or single quotes depends on the use case.

Combining literal strings with special characters

Things can start getting complicated when you want special characters with their special functionality. For example, when you want to refer to the path ~/Library/Application Support, you should put it in quotes, because of the space. But when you put the ~ in the quotes, it will not be substituted to the user’s home directory path.

There are a few ways to solve this problem. You could escape the space with a backslash. You could use the $HOME variable instead (but be sure you are in a context where this is set). But the easiest is to move the special character out of the quotes:

dirpath=~"/Library/Application Support"

Quotes in quotes

Sometimes it is necessary to have a set of quotes within quotes. A common situation for MacAdmins is the following osascript:

osascript -e 'display dialog "Hello, World"'

The osascript command can be used to run Apple commands or scripts. Since AppleScript uses double quotes for literal strings, the entire AppleScript command is passed in single quotes. This keep the command string together and the double quotes in single quotes don’t confuse the shell.

This works fine, until you want to do something like this:

computerName=$(scutil --get ComputerName)
newName=$(osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "$computerName")')

Again, we put the AppleScript command in single quotes, so we can use double quotes inside. But now, the single quotes are also blocking the variable substitution and we get the literal $computerName in the dialog.

There are a few solutions out of this, I will demonstrate three:

First, you could close the single quotes before the variable substitution and re-open them after:

osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'$computerName'")'

This will in this form as long as $computerName contains no spaces. This is unlikely as the default computer name is something like Armin's MacBook Pro. The shell will consider that space a separator before a new argument, breaking the AppleScript command into meaningless pieces and failing the osascript command. We can avoid that by putting the substitution itself in double quotes:

osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'"$computerName"'")'

This works and is entirely legal syntax, but not very legible.

Escaping the escape characters

Another solution is to use double quotes for the entire AppleScript command, we can use variable substitution inside. But then we have to deal with the double quotes required for the AppleScript string literal. The good news here is that we can escape those with the backslash:

osascript -e "text returned of (display dialog \"Enter Computer Name\" default answer \"$computerName\")"

This doesn’t win prizes for legibility either, but I consider it an improvement over the previous approach.

Here Docs

The above approaches with work in sh, bash, and zsh. But bash and zsh have another tool available that can work here. The ‘here doc’ syntax can be used to include an entire block of AppleScript code in a bash or zsh script:

#!/bin/bash

computerName=$(scutil --get ComputerName)

newName=$(osascript <<EndOfScript
    text returned of (display dialog "Enter Computer Name" default answer "$computerName")
EndOfScript
)

echo "New name: $newName"

The syntax is a bit weird. The <<EndOfScript says: take all the text until the next appearance of EndOfScript and pipe it into the preceding command, in this case osascript.

The ‘marker’ EndOfScript is entirely arbitrary. Many people choose EOF but I prefer something a little more descriptive. Whatever label you choose the ending marker has to stand alone in its line. This is why the parenthesis ) which closes the command substition $( has to stand alone in the next line.

You can still use variable substitution in a here doc, so the variable $computerName will be substituted before the here doc is piped into osascript.

13 thoughts on “Advanced Quoting in Shell Scripts”

  1. I prefer using single quotes and escaping with

    osascript -e ‘ ” ‘ “$var” ‘ ” ‘
    (Note: here with whitespace for better legibility)

    I know, it’s easier to make errors with this approach, but it keeps me concentrated. 😉

    However, it will get complicated with “do shell script”. For example, it’s easy to perform something like

    while read -r filepath
    do
    osascript -e ‘ do shell script “rm -rf \” ‘ “$filepath” ‘ \” ” with administrator privileges ‘
    done < <(echo "$filepath_list")

    But that will prompt for your admin password every single time. So the idea would be to use $filepath_list in `osascript` as is. But apparently you can't run a complex series of shell commands incl. a loop within osascript -e 'do shell script', e.g.

    osascript -e ' do shell script " while read -r filepath ; do ; rm -rf \"$filepath\" ; done < <(echo \" ' "$filepath_list" ' \") " with administrator privileges '

    (Note: the same happens when piping to the loop or to xargs.)

    It seems that to perform just a slightly more complex series of shell commands as root, you'd have to export them as a temporary shell script, and call that one with admin privileges:

    read -d '' tempscript < /tmp/tempscript.sh
    chmod +x /tmp/tempscript.sh
    osascript -e ‘do shell script “/tmp/tempscript.sh” with administrator privileges’

    Maybe I’m missing something, but that’s one of the osascript problems I’ve been dealing with lately.

    PS: in the case of the `rm` command, you could always use the variant `rm -rf file-1 file-2 … file-n`, but I’ve tried it, and it doesn’t seem to work with a list of files, with each path on a different line.

    1. why are you running `do shell script … with administrator privileges` from a shell script? Why are you not simply running the shell script as root (with `sudo`)?

      1. That would be too dangerous for the user. The `rm -f` command is one of the file operations, and I don’t want the user to unlink files without an admin prompt. And it also has to work for standard users that are not in the admin group, so the default use case would be to run the script as the current user. (In fact, the script will even exit, when run as root, unless the logged-in user is also root. In the latter case, the osascript is moot, of course, because root can do anything anyway.)

        1. But `sudo` requires admin authentication, and `with administrator privileges` won’t work for non-admin users?

          If it works for you, then great! It seems a little convoluted, though.

          1. It’s just for added protection. Don’t want users to accidentally delete some important directory in the global library etc. 🙂

            The script would have to run for every user—root user (i.e. logged-in as “root”), standard user (no admin group), user in the admin group (macOS default)—, but with different behavior or outcome for each. The script will check the logged-in user, and if that user is root, it will forgo all “with administrator privileges” stuff, because it can just run the rm command on any file, unless it’s further protected (sunlnk, restricted, schg). If the executing user is in the admin group, then the “with administrator privileges” will come into play for those files that are only removable with root escalation. If it’s a standard user, “with administrator privileges” will also come into play, but all the user can do is click “Cancel”, because he doesn’t know the admin password.

            As for running the script as sudo: since it’s a shell script, anyone would be able to modify it and remove the lines that block the script from executing, when it’s run that way.

            But at any rate, my main issue was that it seems impossible to run a complex combination of commands with “do shell script” via osascript. But I must admit that I haven’t researched this in depth, so the “impossible” might not be true.

    1. A heredoc might actually be the solution to my problem of running more complex commands/scripts within “do shell script” within osascript, without actually exporting them as a standalone script, e.g.:

      var=”variable”
      osascript -e ” do shell script \” /bin/bash -s <<EOF
      # script with $var etc.
      EOF
      \" with administrator privileges "

      Though not tested yet.

      Found here: https://grahamrpugh.com/2017/01/07/application-to-run-shell-commands-with-admin-rights.html

    1. Thee shouldn’t be any confusion. When you quote the marker, there is no substitution. When you don’t quote, there is.

      1. Indeed. I meant but didn’t clearly said: if you don’t intend to have things expanded in a heredoc, always quote the marker. It’s also meant as documentation, even if the quoting doesn’t perform a single thing.

  2. You could also take advantage of AppleScript’s “quoted form”.

    newName=$(osascript -e ‘text returned of (display dialog “Enter Computer Name” default answer quoted form of $computerName)’)

    1. That won’t work since AppleScript has no idea what to do with $computerName. And if it did, it would put the quotes around it and fill the text input field with the quoted value.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.