csh

What is the correct quoting for a variable expansion (a filename) inside double-quoted backticks in CSH?


Let's say that you're in a system with devilish filenames, for eg.

# a horrible filename: 
set link = '  '\''  "  \  `  $#  \
\
'
# an even worse logical link
ln -s $link:q $link:q

Now I would like to read the target of that logical link and put it in the target variable, so that echo $target:q would output:

  '  "  \  `  $#  


Here's what I've tried:

$ set target = `readlink -n $lnk:q`
$ echo $target:q
' " \ ` $#
$ set target = "`readlink -n $lnk:q`"
Unmatched '.
$ set target = "`readlink -n "$lnk"`"
Unmatched `.
$ set target = "`readlink -n '$lnk:q'`"
Unmatched ".

What is the correct syntax for using a variable within double-quoted back-ticks?


Solution

  • Let's start with an extract of the CSH manual:

    Command Substitution

    A command enclosed by backquotes (`...`) is performed by a subshell. Its standard output is broken into separate words at each space character, tab and newline; null words are discarded. This text replaces the backquoted string on the current command line.
    Within double-quotes, only newline characters force new words; space and tab characters are preserved. However, a final newline is ignored. It is therefore possible for a command substitution to yield a partial word.

    So, getting a path directly from a command substitution would, at the very least, require to enclose the backticks within double-quotes. That'll work as long as the path doesn't contain any LF character (which is a very common case ;-P). The newlines will split the result into different words, successive newlines will be squeezed, and the trailing newlines will be discarded.


    1. The correct way to use a variable within a double-quoted command substitution.

    With "`readlink $link:q`", CSH expands $link:q before launching the sub-shell, which will lead to potentially broken commands being executed. To safeguard against it you'll need to make $link:q a literal string until the sub-shell is executed; you can do it by closing the double-quotes, escaping the $ and then re-opening the double-quotes; for example:

    set target = "`   readlink "\$"link:q   `"
    
    echo $target:q
    

    A somewhat more readable way:

    set target = "`"'   readlink -n $link:q   '"`"
    
    echo $target:q
    

    Outputs:

      '  "  \  `  $#  
    

    It's a good start, as readlink seems to output the correct value, and the blanks were preserved; but as you can see, the LFs disappeared...

    2. How to get the path outputted by readlink, verbatim.

    Here comes the difficult part. I've been exploring quite a few possibilities and, given the limitations of CSH's command substitutions, the only robust solution I could find is to generate a csh script that defines the value of the target variable, then source it:

    ( readlink -n $link:q && echo ) | \
    sed -e "    s/\\/\\\\/g"          \
        -e '    s/\!/\\\!/g'          \
        -e "    s/'/'\\''/g"          \
        -e "1   s/^/set target = '/"  \
        -e '$ ! s/$/\\/'              \
        -e '$   s/$/'"'/"             \
    > target_def.csh
    
    source target_def.csh
    
    echo $target:q
    

    note: the escaping logic relies on the trailing LF that readlink adds to the output

    fixed: on macOS readlink doesn't add a trailing LF when the path of the target already ends with one... A workaround is to use readlink -n and append the trailing LF oneself

    fixed: CSH escaping isn't so simple; there are other characters like \ and ! that need to be escaped, even within single-quotes.

    Output:

      '  "  \  `  $#  
    
    
    

    APENDUM

    Now that I've learned more about CSH, I'm not sure that I got my CSH escaping right; I fixed a few special cases in the previous code but there might be others that I'm not aware of...

    So, at the end of the day, it may be safer to make use of the :h modifier instead. The idea is to add a / at the end of each line and append an additional not(/) to the last line of the output; you will then be able to rebuild the original output by replacing the trailing / characters with a LF and stripping the trailing /* of the last line:

    set target
    
    foreach line ("`"' (readlink -n $link:q && echo) | sed -e s,\$,/, -e \$s/\$/./ '"`")
        set target = $target:q${line:h:q}
        if ( $line:q =~ */ ) set target = $target:q'\
    '
    end
    
    echo $target:q
    
      '  "  \  `  $#