Search code examples
jsonbashsublimetext3osascript

Quoting a shell command inside an osascript command run by a shell inside a JSON file


I have the following line for my Sublime text build system json:

"shell_cmd": "osascript -e 'tell application \"Terminal\" to do script \"ssh [email protected] \"'"

It works fine, but now I should add -t 'ls -la; bash -l' after ssh command. I wanted to escape ' characters with backslash (\'), but Sublime doesn't accept it:

"shell_cmd": "osascript -e 'tell application \"Terminal\" to do script \"ssh [email protected] \'ls -la; bash -l\'\"'"

I tried to replace " with ' and vice versa, but it also doesn't work with Sublime.

What else can I try?

Upd. here is the command, which works well from the command line:

osascript -e 'tell application "Terminal" to do script "   ssh [email protected] -t \"ls -la; bash -l\"  "  '

Solution

  • Don't try to figure out quoting by hand when it can be at all avoided: instead, let tools do the work for you. For example, let's say you have a working shell command:

    ssh 1.2.3.4 'cd .. && bash -l'
    

    If you want to create a shell string containing that command, first put it in an array, then ask the shell to quote it:

    cmdArr=( ssh 1.2.3.4 'cd .. && bash -l' )
    cmdStr="${cmdArr[*]@Q}"
    

    In bash 5.x or later, this will store something like 'ssh' '1.2.3.4' 'cd .. && bash -l' in your string.

    The next step would typically be to figure out how to escape the contents of cmdStr to be safely substituted into osascript source code without risk of injection vulnerabilities -- but we can sidestep this entirely by passing cmdStr out-of-band from the osascript source code, on the command line argument vector (argv). osascript source that looks on the argv for the text of a shell command to run might look like the following:

    osascript -- - "$cmdStr" <<'EOF'
      on run(argv)
        return tell application "Terminal" to do script item 1 of argv
      end
    EOF
    

    ...but we want a string containing a shell command that runs that osascript code (with the shell string created by our previous escaping operation passed to it), right? Well, one way to assemble one might be:

    # generate a shell string containing the command we want to run
    cmdArr=( ssh 1.2.3.4 'cd .. && bash -l' )
    cmdStr="${cmdArr[*]@Q}"
    
    # text of osascript code that runs its first argument in terminal
    osascript=$(cat <<'EOF'
      on run(argv)
        tell application "Terminal" to do script item 1 of argv
      end
    EOF
    )
    
    # one array per simple command; pass script on stdin to osascript
    # ...and command on argv of osascript interpreter
    write_script_arr=( printf '%s\n' "$osascript" )
    run_script_arr=( osascript -- - "$cmdStr" )
    
    # finally, assemble those pieces together
    full_cmd_str="${write_script_arr[*]@Q} | ${run_script_arr[*]@Q}"
    

    Okay! Now we just need to make that into JSON. Fortunately, that's something jq will do for you automatically:

    jq -R '{"shell_cmd": .}' <<<"$full_cmd_str"
    

    ...and there we are! Output is something like:

    {
      "shell_cmd": "'printf' '%s\\n' $'  on run(argv)\\n    tell application \"Terminal\" to do script item 1 of argv\\n  end' | 'osascript' '--' '-' ''\\''ssh'\\'' '\\''1.2.3.4'\\'' '\\''cd .. && bash -l'\\'''"
    }
    

    ...and as long as your shell_cmd is executed by bash (not /bin/sh, which doesn't support the $'...' syntax!), it'll work perfectly.