Thomas 'PointedEars' Lahn <
Point...@web.de>:
> Helmut Waitzmann wrote:
>
>> An equal variant of the “cat” version:
>>
>> </local/path ssh us...@remote.host.example cat '>' /remote/path
[…]
>> […] “ssh” just glues the given words together with spaces in
>> between to compose the command line for the remote shell.
>
> Yes, appearantly that is so:
>
> | $ ssh "$REMOTE_HOST" printf '%s\n' 1 2 3
> | 1n2n3n
>
> Fascinating.
More verbose explanation:
“ssh” is given the command words “printf”, “%s\n”, “1”, “2”, and
“3”. It glues them together with spaces in between:
“printf %s\n 1 2”,
and manages to invoke the shell on the remote host, that is
configured for the remote user (for example, a POSIX‐compliant
shell, or a “tcsh”, or some different one), with the command line
“printf %s\n 1 2”.
If the remote shell is a POSIX‐compliant one, “\n”, when parsed,
yields the same like “n”: “n”. So, eventually, the utility
“printf” is invoked with the parameters “%sn”, “1”, and “2”.
“ssh user@host ...” faces the same problems like
“sh -c -- ...” or “eval ...”: All of them expect a command line
rather than an “execve()” argument vector.
So, if one likes to invoke a utility with its parameters, one has
to reconstruct a shell command line out of the utility name and
the parameters. If the command line is to be given to a remote
“bash”, that can be done by using the “bash”‐built‐in „printf”
with the format specification “%q ”:
quote_words_for_bash()
{
bash -c -- '"$@"' bash printf '%q ' "$@"
}
bash_command_line="$(
quote_words_for_bash printf '%s\n' 1 2
)" &&
ssh "$REMOTE_HOST" "$bash_command_line"
If the local shell is a “bash”, too, the function
“quote_words_for_bash” can be simplified to
quote_words_for_bash()
{
printf '%q ' "$@"
}
To come back to the problem, that
</local/path ssh us...@remote.host.example cat '>' \
'/remote/some funny name'
as well as
</local/path ssh us...@remote.host.example dd \
of='/remote/some funny name'
won't work:
If the remote shell is a “bash”, one can do:
</local/path ssh us...@remote.host.example \
"$(
quote_words_for_bash dd of='/remote/some funny name'
)"
With “cat” and output redirection, it's slightly more complicated,
because the redirection operator “>” must not be quoted in the
command line for the remote shell:
</local/path ssh us...@remote.host.example \
"$( quote_words_for_bash cat )" '>' \
"$( quote_words_for_bash '/remote/some funny name' )"
In this two examples, the words “dd” and “cat” don't need any
quoting, because there are no funny characters in them. So, the
commands might be simplified to
</local/path ssh us...@remote.host.example dd \
"$(
quote_words_for_bash of='/remote/some funny name'
)"
and
</local/path ssh us...@remote.host.example cat '>' \
"$( quote_words_for_bash '/remote/some funny name' )"
respectively.
Finally, if the remote shell is a POSIX‐compliant shell (only)
rather than a “bash”, or, if at the command line constructing
site, there is no “bash” available, a function for a
POSIX‐compliant shell (a “bash” will do as well) can be defined,
which quotes its parameters in a way suitable for a
POSIX‐compliant shell (as well as a “bash”). See the function
“my_quote_words_for_shells” at the end of this posting.
>> But maybe I understand you wrong. Could you explain more
>> verbosely?
>
> (I would use “I _misunderstand you_.)
(My English may not be good enough. I apologize.)
With quoting for the POSIX‐compliant shell's command line, there
are some rules:
Every sequence of characters, except apostrophes and maybe NULL
characters, can be quoted by surrounding that sequence with a pair
of apostrophes.
A sequence of apostrophes can be quoted by either surrounding it
with a pair of quotation marks or by preceding each apostrophe
with a backslash.
Any string, that is to be quoted, can be quoted by splitting it
into any two parts, then quoting each of the parts and finally
gluing the quoted parts together without anything in between.
For example, the string
“This string's purpose is to say "Hello, world!"”
could be quoted by splitting it before and after the apostrophe,
then quoting each of the three parts, and finally gluing them
together:
Splitting:
“This string”, “'”, “s purpose is to say "Hello, world!"”
Quoting each of the three parts according to the rules above:
“'This string'”, “"'"”, “'s purpose is to say "Hello, world!"'”
A A QaQ A q qA
Legend:
A marks a quoting apostrophe
Q marks a quoting quotation mark
a marks a quoted apostrophe
q marks a quoted quotation mark
Gluing them together:
“'This string'"'"'s purpose is to say "Hello, world!"'”
A AQaQA q qA
For example a command line containing a “printf” invocation to
output the sentence
“This message's purpose is to say "Hello, world!"”,
could be written as
“printf '%s\n' \
'This message'"'"'s purpose is to say "Hello, world!"'
”
To invoke that command in a remote POSIX‐compliant shell, one
could write:
“command_line="$( my_quote_words_for_shells \
printf '%s\n' \
'This message'"'"'s purpose is to say "Hello, world!"' \
)" &&
ssh us...@host.remote.example "$command_line"
”
This is the function:
my_quote_words_for_shells()
(
# Outputs a (part of) a command line resembling a 'simple command'
# to be used by a POSIX-compliant shell.
#
# Usage:
#
# my_quote_words_for_shells words to be quoted...
#
# exit code: 0, if the command line has been successfully constructed,
# !=0, if the function failed to construct the command line.
#
# Examples:
#
# Put a word list resembling a command invocation into a command line:
#
# if command_line="$(my_quote_words_for_shells \
# program with parameters)"
# then
# # "eval" it:
# eval "$command_line"
# # or invoke a new shell:
# sh -c "$command_line" sh
# # or use it remotely:
# ssh us...@remote.host.example "$command_line"
# else
# # failed to compute the command line.
# fi
#
#
# Store the shell positional parameters in a variable and restore
# them later:
#
# if args="$(my_quote_words_for_shells "$@")"
# then
# # the positional parameters may be changed for any purpose
# # ...
#
# # restore the positional parameters:
#
# set '' "$args" && shift
# else
# # failed to save the positional parameters.
# fi
# "wordsep" contains a separator used for the list of the quoted
# words. The first need not to be preceded by a separater:
wordsep=
for word
do
# "$wordsep" separates each word (except the first one)
# from its precedessor:
printf '%s' "$wordsep"
if test -z "$word"
then
# The word is empty. Then the result is "''":
printf '%s' "''"
else
# The word is not empty.
while
{
prefix="${word%%\'*}"
word="${word#"$prefix"}"
# "$prefix" is the longest beginning part
# of "$word", that does not contain any
# apostrophes; it is removed from "$word".
# Conclusion: "$word" is either empty or
# begins with an apostrophe.
if test -n "$prefix"
then
# "$prefix" consists of one or more
# characters. Put them in between
# of two apostrophes:
printf \''%s'\' "${prefix}"
fi
test -n "$word" &&
{
# "$word" is not empty.
# Conclusion: "$word" begins with
# one or more apostrophes.
apostr="${word%%[!\']*}"
# "$apostr" ist the longest
# beginning part of "$word", that
# contains apostrophes only.
# Put it in between of two double
# quotes:
printf '"%s"' "${apostr}"
# Remove the beginning apostrophes
# from "$word":
word="${word#"$apostr"}"
# If nothing is left, we are done:
${word:+:} false
}
}
do
:
done
fi
# All following words (except the first one) have to be
# separated from their precedessor with a blank:
wordsep=' '
done
printf '\n'
)