in reply to safely passing args through ssh

Here's what I do. This function doesn't really need to be an object/class method, but it does make it easier when it's in a base class to be able to call $self->quote_cmd from a derived object.

# shellish quote. sub quote_cmd { my $self = shift; if (any { ref $_ eq 'ARRAY' } @_) { return join ' ; ', map { $self->quote_cmd(@$_) } @_; } # if we have only one parameter, assume it's a full command by # itself, already quoted (nothing else we can do anyway). return $_[0] if @_ == 1; join ' ', apply { if (not defined) { die("Undefined command parameter?"); } elsif (0 == length) { $_ = q[''] } elsif (/[\s"]/ && !/['\$]/) { s[\\][\\\\]g; s['] [\\']g; $_ = qq['$_'] } elsif (/['\$]/) { s['] ['"'"']g; $_ = qq['$_'] } } @_; }
As for double-ssh'ing, I do that do. I just quote the whole thing again.
# @options has options for ssh itself. my @cmd = ( qw(ssh), @options, $self->usernode(), $self->quote_cmd(@cmd) ); # for testing purposes, allow proxying. if ($ENV{SSH_TEST_PROXY}) { @cmd = ( qw(ssh), @options, $ENV{SSH_TEST_PROXY}, $self->quote_cmd(@cmd) ); }
And then it's all taken care of. I haven't yet found a case where this doesn't work, though I haven't yet had a need to try more than two ssh calls.

My unit tests involve a perl script that takes its args, converts to JSON, and prints it out. The .t file then gets the stdout, decodes the JSON, and compares it to what it sent in (think "is_deeply"). Spaces, quotes (both single and double), etc., all work fine.

Note that my expectation is that the command and arguments are set up as if you were running system(@cmd), and not system($cmd). Of course, this does mean that my code doesn't really go for redirection (> /dev/null, 2>&1, etc.), but that's okay for me because I use IPC::Open3 and IO::Select to read all the output from the ssh calls on my (caller) end, and then I work with the output here. (If you try to pass in the redirection characters, this will kinda choke, but those characters could be added, and, again, it's not a concern here - this didn't go to CPAN.)

My next project here, after a conversion to AnyEvent, is to create a wrapper script where I'll pass the parameters in a method independent of the shell, and have that script do the select bit, sending back all pertinent data via JSON (probably in chunks). The wrapper script will then be able to handle more than simple parameters (args:["a","b","c d e"]), but also set environment variables (env:{foo:"blah"},addenv:{PATH:"/some/extra/path"}), drop privileges if run as root (user:"nobody"), etc., almost like its own minilanguage :-)