109

In my .bashrc I define a function which I can use on the command line later:

function mycommand() {
    ssh user@123.456.789.0 cd testdir;./test.sh "$1"
}

When using this command, just the cd command is executed on the remote host; the test.sh command is executed on the local host. This is because the semicolon separates two different commands: the ssh command and the test.sh command.

I tried defining the function as follows (note the single quotes):

function mycommand() {
    ssh user@123.456.789.0 'cd testdir;./test.sh "$1"'
}

I tried to keep the cd command and the test.sh command together, but the argument $1 is not resolved, independent of what I give to the function. It is always tried to execute a command

./test.sh $1

on the remote host.

How do I properly define mycommand, so the script test.sh is executed on the remote host after changing into the directory testdir, with the ability to pass on the argument given to mycommand to test.sh?

m81
  • 2,217
  • 5
  • 31
  • 47
Alex
  • 41,580
  • 88
  • 260
  • 469
  • 6
    This is unrelated to the main point, but you might want to use `&&` instead of the semi-colon to join the commands executed on the remote host: `cd testdir && ./test.sh "$1"`. With that form (because evaluation of `&&` short-circuits in bash), if the `cd` fails the second command won't be executed, and you won't inadvertently run a different `test.sh` in `user`'s homedir. – Alp Aug 29 '13 at 07:09

6 Answers6

132

Do it this way instead:

function mycommand {
    ssh user@123.456.789.0 "cd testdir;./test.sh \"$1\""
}

You still have to pass the whole command as a single string, yet in that single string you need to have $1 expanded before it is sent to ssh so you need to use "" for it.

Update

Another proper way to do this actually is to use printf %q to properly quote the argument. This would make the argument safe to parse even if it has spaces, single quotes, double quotes, or any other character that may have a special meaning to the shell:

function mycommand {
    printf -v __ %q "$1"
    ssh user@123.456.789.0 "cd testdir;./test.sh $__"
}
  • When declaring a function with function, () is not necessary.
  • Don't comment back about it just because you're a POSIXist.

Starting Bash version 4.4, it can also be simplified to this:

function mycommand {
    ssh user@123.456.789.0 "cd testdir;./test.sh ${1@Q}"
}

See ${parameter@operator} section in Shell Parameter Expansion.

konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • @JoSo Yes that's the basic of it so depending on usage the user can opt to sanitize the argument he needs it. With respect to basic `ssh` there's probably no better way - unless you do file transfers first, etc. – konsolebox Jun 25 '14 at 21:21
  • 3
    @JoSo Yes, a better approach would be to define a quote function like `quote() { printf "'%q'" "$1"; }`, and then do `ssh user@host "cd testdir; ./test.sh $(quote "$1")"` – augurar Aug 05 '14 at 00:54
  • @augurar, almost, but `%q` results are self-quoting; surrounding them with literal quotes isn't necessary or correct. `printf -v arg_str '%q ' "$1"; ssh user@host "cd testdir; ./test.sh $arg_str"` suffices. – Charles Duffy May 16 '16 at 20:03
  • @CharlesDuffy Why do you need to add space though? – konsolebox May 17 '16 at 06:30
  • @konsolebox, for just the immediate case, don't really, but that way the idiom scales if more arguments are added rather than smushing them all together. – Charles Duffy May 17 '16 at 13:20
  • @CharlesDuffy Good catch, that's a typo. Should be `printf "%q" "$1"`. – augurar May 18 '16 at 09:31
13

I'm using the following to execute commands on the remote from my local computer:

ssh -i ~/.ssh/$GIT_PRIVKEY user@$IP "bash -s" < localpath/script.sh $arg1 $arg2
muratgozel
  • 2,333
  • 27
  • 31
12

This is an example that works on the AWS Cloud. The scenario is that some machine that booted from autoscaling needs to perform some action on another server, passing the newly spawned instance DNS via SSH

# Get the public DNS of the current machine (AWS specific)
MY_DNS=`curl -s http://169.254.169.254/latest/meta-data/public-hostname`


ssh \
    -o StrictHostKeyChecking=no \
    -i ~/.ssh/id_rsa \
    user@remotehost.example.com \
<< EOF
cd ~/
echo "Hey I was just SSHed by ${MY_DNS}"
run_other_commands
# Newline is important before final EOF!

EOF
Cyril Duchon-Doris
  • 12,964
  • 9
  • 77
  • 164
10

Solution: you want to be able connect to machine remotely using ssh protocol and trigger/run some actions outside.

on ssh use a -t flag, from documentation:

-t Force pseudo-terminal allocation.
This can be used to execute arbitrary screen-based programs on a remote machine, which can be very useful, e.g. when implementing menu services. Multiple -t options force tty allocation, even if ssh has no local tty.

formula:

ssh -i <key-path> <user>@<remote-machine> -t '<action>'

Example: as administrator I want to be able to connect remotely into ec2 machines and trigger a revert process for a bad deployment on a several machines in a raw, moreover you better implement this action as an automation script that use ips as an arguments and running on different machines in parallel.

ssh -i /home/admin/.ssh/key admin@10.20.30.40 -t 'cd /home/application && make revert'
avivamg
  • 12,197
  • 3
  • 67
  • 61
8

Reviving an old thread, but this pretty clean approach was not listed.

function mycommand() {
    ssh user@123.456.789.0 <<+
    cd testdir;./test.sh "$1"
+
}
Ted Bigham
  • 4,237
  • 1
  • 26
  • 31
  • 1
    This would fail if `$1` would expand to a string having `"` and expandable characters like `$` and `\``. – konsolebox Jan 19 '15 at 08:37
  • 2
    It doesn't "fail". It has the same effect as executing any non-ssh command with arguments. The OP wasn't asking for a lesson in double escaping arguments. Just how to get his second command to execute what [whatever] argument he was already intending. – Ted Bigham Jan 19 '15 at 18:09
  • If the user asks how to execute something **over ssh**, then it's reasonable to expect any answer to cover caveats specific to ssh (ie. which don't exist when running code locally). Double expansion (and shell injection vulnerabilities caused by same) are among such caveats which wouldn't exist with `cd testdir; ./test.sh "$1"` run directly in a local shell, but do exist with the code as-given. – Charles Duffy May 18 '16 at 12:55
  • I don't imagine you'd upvote an answer to a database question that showcased a practice with SQL injection vulnerabilities, but the potential of someone running arbitrary remote code via `mycommand '$(rm -rf ~)'` is every bit as severe. – Charles Duffy May 18 '16 at 13:00
  • (...and the thing is, the changes needed to make your code safe are fairly small: `printf -v argv_str '%q ' "$@"; ssh user@host "bash -s $argv_str" <<'+'` and the rest can stay as-is; quoting the `+` sigil for the heredoc prevents its expansion). – Charles Duffy May 18 '16 at 13:02
  • @CharlesDuffy "but the potential of someone running arbitrary remote code via mycommand '$(rm -rf ~)' is every bit as severe." -> I don't see the sense in that. There are differences: mistaken or unexpected expansion vs. explicit command. You don't say that they give equal danger just because they both give danger. There are degrees or levels. – konsolebox May 20 '16 at 08:03
  • @konsolebox, if I run `for file in /data/**; do ./mycommand "$file"; done`, did I just "explicitly" run a command because a file in the `data` directory had a malicious name? Part of the point of keeping code and data separate (that is, preventing the latter from being inadvertantly parsed as the former) is that code should remain fully under audited human control, but data is often from untrusted sources. – Charles Duffy Nov 28 '16 at 16:56
  • @konsolebox, ...yes, ease-of-exploitability does vary, but that's no excuse to have something be known-exploitable when it could instead be entirely safe (absent some as-yet-entirely-unknown vulnerability). – Charles Duffy Nov 28 '16 at 16:58
0

A little trick for me, using the "bash -s" they said they allow POSITIONAL ARGS but apparently the $0 is already reserved for whatever reason... Then using twice the same args rocks like so:

ssh user@host "bash -s" < ./start_app.sh -e test -e test -f docker-compose.services.yml