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?
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
' " \ ` $#