1

I've been attempting to simplify installation of a set of scripts by nesting them within a larger script using here documents (so I can still edit them like normal scripts). However, although nothing within the here document is supposed to be interpreted, my shell seems to stumble on mismatched closing brackets, such as can be found in a case statement.

For example:

#!/bin/sh
script=$(cat <<- 'NESTED_SCRIPT'
    case "$1" in
        foo)
            echo "Foo"
        ;;
        bar)
            echo "Bar"
        ;;
    esac
NESTED_SCRIPT
)

The above will fail with a syntax error on line 6 unless backslashes are placed before the closing brackets within the case statement. It's like for some reason the shell isn't waiting for the special here document termination, but is terminating as soon as it finds the first closing bracket that has no corresponding opening bracket. Unfortunately escaping the brackets isn't an option as this leaves me with a script that needs to have the backslashes stripped from it; although I could do that I suppose I'd rather avoid it if I can.

Anyway, I know it's a bit of a weird use-case, but what I can't understand is why it's terminating early like this. Is there a way that I can force it to only terminate with the NESTED_SCRIPT sequence without interpreting brackets at all, or some other workaround I can use? Unfortunately I need to assign the script to a variable due to the way I'm working with it (using eval).

Haravikk
  • 3,109
  • 1
  • 33
  • 46
  • I'm not sure what the problem is, but `script='...'` is an option if your script doesn't contain single quotes (a single-quoted string can contain embedded newlines). If it does, some shells support `script=$'...'` with any single quotes escaped as `\'`. – chepner Oct 19 '13 at 13:55
  • `eval` is usually a last resort; can you post more context? Perhaps someone can suggest an alternative (although it will be interesting nonetheless to find out why the syntax error occurs). – chepner Oct 19 '13 at 13:58
  • My script runs commands on a remote server as well using `ssh`, but some of what I need to do requires the same functions, so I decided to wrap them up in a string so I could `eval` them locally, and send them for remote execution as well. @mbratch backticks do seem to work, but then the whole here document appears as a string in all editors I've tried, but it seems redundant as I thought the special sequence you define should be the only way to terminate the here document? – Haravikk Oct 19 '13 at 18:58

3 Answers3

4

The POSIX standard allows a leading parenthesis to balance the final one in case entries, so you can just fix your script that way:

#!/bin/sh
script=$(cat <<- 'NESTED_SCRIPT'
    case "$1" in
        (foo)
            echo "Foo"
        ;;
        (bar)
            echo "Bar"
        ;;
    esac
NESTED_SCRIPT
)

This should work with all modern shells like ksh, bash and the likes.

jlliagre
  • 29,783
  • 6
  • 61
  • 72
3

The solution in the OP works in Linux but not in osx.

The right way to assign a here document to a variable seems to be like this:

read -r -d '' script <<- 'NESTED_SCRIPT'
... # your nested script...
NESTED_SCRIPT

But this works only in not too old bash, it does not work in /bin/sh of Debian where read doesn't have the -d flag.

If you want something more portable, you could try this uglier solution:

script=
IFS=''
while read line; do
    script="$script$line\n"
done <<- 'NESTED_SCRIPT'
    case "$1" in
        foo)
            echo "Foo"
        ;;
        bar)
            echo "Bar"
        ;;
    esac
NESTED_SCRIPT

You can find more details and suggestions in this other question:

How to assign a heredoc value to a variable in Bash?

BUT...

If you just want to reuse the same script to run both locally and remotely, there's a much better way than using eval on the remote side. You could run the local script on a remote server like this:

ssh myserver bash < script.sh

If you want to pass command line arguments to the script, you can do like this:

ssh myserver bash -s arg1 arg2 < script.sh
Community
  • 1
  • 1
janos
  • 120,954
  • 29
  • 226
  • 236
  • I'd love to do that, but unfortunately one of my requirements is to use a single script only, though I could maybe investigate spitting my values out into temp-files. The solution to the original problem of using `read` sounds great and works fine for OS X, but I've found that some devices I'm targeting don't support the `-d` parameter, and I can't get the same effect using `IFS`, any ideas? – Haravikk Oct 21 '13 at 11:32
  • @Haravikk that's really too bad. It would be really best to use my final suggestion with `ssh server bash < script.sh`. I added one more idea for you without `read -d`, see my updated answer. However, if you use `#!/bin/bash` as the shebang then `read -d` *should* work. – janos Oct 23 '13 at 05:42
  • Thanks for the addition of the looping method, unfortunately it seems to suffer from another similar issue, at least under the systems I tried, which is that it requires double escaping of special characters, for example if the nested script contains `sed` expressions though I realise I don't have any in my example. I think @jlliagre's answer may be the most correct for my question, though yours are the better solutions where applicable. – Haravikk Oct 23 '13 at 11:11
  • @Haravikk fair enough, no worries ;-) – janos Oct 23 '13 at 13:23
2

The reason for the syntax error is that the first ) in the script closes the process substitution. Normally, I would argue to avoid backticks like the plague and use $() syntax. But in this case...

#!/bin/sh
script=`cat <<- 'NESTED_SCRIPT'
    case "$1" in
        foo)
            echo "Foo"
        ;;
        bar)
            echo "Bar"
        ;;
    esac
NESTED_SCRIPT
`

Just make sure you avoid using backticks in any embedded scripts, and you should be fine.

William Pursell
  • 204,365
  • 48
  • 270
  • 300