Search code examples
evalaliaswrappertcsh

How to *not* expand "~" in tcsh backtick/eval wrapper?


I'm not interested in a "why tcsh is evil" response, this question requires tcsh for it's use. If this is a bug in tcsh, please just let me know that.

I'm generally pretty good at quoting output but I've been stumped by a use case I have to deal with. Here's the simplified case:

I have an executable that I eval the output of. Sometimes it needs to print output (to stdout) so I have tried using echo (builtin), /bin/echo and /bin/printf.

As a simple example, say my executable is called "simpleWrap" and it outputs:

echo "hello world"

So I run (eventually using an alias, but that's irrelevant here):

eval `simpleWrap`

And I get, as expected:

hello world

But here's the problem. Sometimes I need a tilde in the output. So let's try some examples. We'll put tildes in the simpleWrap output (this is not the contents of the script, but what it outputs):

echo "These are tildes:  ~ and \~ and \\~"

And surprisingly when I eval the output of simpleWrap now, I get:

These are tildes: /home/dave and \~ and \~

Either the ~ is expanded to my home directory, or the \ protects it, but I can't get rid of the backslash.

How can I just print a '~' in the output of an eval with backticks?

I believe the backticks are forcing the ~ expansion, but they aren't doing it in a consistent way. For example, if I skip the backticks and just do:

eval echo "These are tildes:  ~ and \~ and \\~"

Then I get a consistent, expected output:

These are tildes: /home/dave and ~ and \~

Is there something wrong with substitution in backticks, or am I missing the proper quoting possibility? (I've also tried wrapping in single and double quotes to no avail)


Solution

  • The differences arises from the order shell substitutions take place, and the different behavior of parsing the \, when it is inside double-quotes, as opposed to no double-quotes.

    To demonstrate the behavior of \:

    echo \~
    => ~
    echo "\~"
    => \~
    echo "\\~"
    => \~
    

    Scenario 1

    eval `simpleWrap`
    

    Here, "simpleWrap" outputs a "raw" string which includes tildes. The string is then passed unchanged (no substitutions) to the eval command (because that's how backticks work), which basically runs a new shell. So the new shell sees this command line:

    echo "These are tildes:  ~ and \~ and \\~"
    

    (Note that the quotes are seen by the eval command).

    The output of this command is the one you didn't expect, i.e. both \~ (denote A) and \\~ (denote B) produce the same output. Why?

    First of all, neither A nor B is substituted, because tilde substitution only happens if the tilde is the first character in its word, which isn't the case for either.

    Now, for A, since \~ is not a known escape sequence (unlike \n for example), the shell leaves it as is, producing \~.

    For B, \\ is a known escape sequence, so the shell correctly interprets it as \, then appends the rest of the string to it, producing \~ again.

    Scenario 2

    eval echo "These are tildes:  ~ and \~ and \\~"
    

    Here, there's only one command line, which includes the tildes. First, there's the shell substitution, which affects neither A nor B, but in this stage, the quotes drop (they are used for grouping words into a single argument, and are not passed to the command). Then, eval runs (echo hasn't run yet), and is pass this as input: echo These are tildes: ~ and \~ and \\~.

    Note the quotes dropped. The output of the command without the quotes is what you expect (see "demonstration" above):

    echo These are tildes:  /home/dave and \~ and \\~
    => These are tildes: /home/dave and ~ and \~
    

    How to make it work?

    Drop the quotes!

    simpleWrap should print this:

    echo These are tildes:  ~ and \~ and \\~
    

    instead of this:

    echo "These are tildes:  ~ and \~ and \\~"