Як виконати довільну просту команду над ssh, не знаючи оболонки входу віддаленого користувача?


26

ssh має дратівливу особливість у тому, що при запуску:

ssh user@host cmd and "here's" "one arg"

Замість того, щоб запускати це cmdз його аргументами на host, він об'єднує це cmdта аргументи з пробілами та запускає оболонку hostдля інтерпретації отриманого рядка (я думаю, саме тому його називають, sshа не sexec).

Гірше, ви не знаєте, яка оболонка буде використовуватися для інтерпретації цього рядка, оскільки це оболонка для входу, userнавіть не гарантована як Борн, як все ще є люди, які використовують tcshв якості оболонки для входу, і fishвона наростає.

Чи є шлях до цього?

Припустимо , у мене є команда , як список аргументів , що зберігаються в bashмасиві, кожен з яких може містити будь-яку послідовність байтів непустих, чи є спосіб , щоб він виконаний на , hostяк userна постійній основі , незалежно від входу оболонки , що userна host(який ми будемо вважати, що це одна з основних сімейств оболонок Unix: Bourne, csh, rc / es, fish)?

Ще одне обґрунтоване припущення, що я повинен бути в змозі зробити, це те, що в наявності доступна shкоманда, сумісна з Bourne.host$PATH

Приклад:

cmd=(
  'printf'
  '<%s>\n'
  'arg with $and spaces'
  '' # empty
  $'even\n* * *\nnewlines'
  "and 'single quotes'"
  '!!'
)

Я можу запустити його локально за допомогою ksh/ zsh/ bash/ yashяк:

$ "${cmd[@]}"
<arg with $and spaces>
<>
<even
* * *
newlines>
<and 'single quotes'>
<!!>

або

env "${cmd[@]}"

або

xterm -hold -e "${cmd[@]}"
...

Як би я запустив його так, hostяк userзакінчив ssh?

ssh user@host "${cmd[@]}"

очевидно, не вийде.

ssh user@host "$(printf ' %q' exec "${cmd[@]}")"

буде працювати лише в тому випадку, якщо оболонка входу віддаленого користувача була такою самою, як локальна оболонка (або розуміє цитування так само, як і printf %qв локальній оболонці, що її виробляє) і працює в тому ж самому локалі.


3
Якби cmdаргумент був, /bin/sh -cми б у 99% всіх випадків опинилися з оболонкою posix, чи не так? Звичайно, втеча спеціальних персонажів трохи болючіше таким чином, але чи вирішить це початкову проблему?
Bananguin

@Bananguin, ні, якщо ви запустите те ssh host sh -c 'some cmd'саме ssh host 'sh -c some cmd', що має оболонку входу для віддаленого користувача, що інтерпретує цей sh -c some cmdкомандний рядок. Нам потрібно записати команду у правильний синтаксис для цієї оболонки (і ми не знаємо, що це таке), щоб shвикликати там -cі some cmdаргументи.
Стефан Шазелас

1
@Otheus, так, sh -c 'some cmd'і some cmdкомандні рядки , трапляється, інтерпретується однаково в усіх цих оболонках. Що робити, якщо я хочу запустити echo \'командний рядок Bourne на віддаленому хості? echo command-string | ssh ... /bin/shце одне рішення, яке я дав у своїй відповіді, але це означає, що ви не можете подавати дані до stdin цієї віддаленої команди.
Стефан Шазелас

1
Здається, що більш тривалим рішенням буде плагін rexec для ssh, ala плагін ftp.
Отей

1
@myrdd, ні це не так, вам потрібно або пробіл, або вкладку, щоб розділити аргументи в командному рядку оболонки. Якщо cmdце так cmd=(echo "foo bar"), командний рядок оболонки, переданий до, sshповинен бути чимось на зразок командного рядка `` echo '' foo bar ' . The *first* space (the one before echo ) is superflous, but doen't harm. The other one (the ones before ' foo bar ' ) is needed. With '% q ' , we'd pass a ' echo '' foo bar'`.
Стефан Шазелас

Відповіді:


19

Я не думаю, що будь-яка реалізація sshмає власний спосіб передавати команду від клієнта до сервера без залучення оболонки.

Тепер все може бути простішим, якщо ви можете сказати віддаленій оболонці запускати лише певний інтерпретатор (наприклад sh, для якого ми знаємо очікуваний синтаксис) і дати код виконувати іншим способом.

Це інше значення може бути, наприклад, стандартним входом або змінною середовища .

Коли жодне з них не може бути використане, я пропоную прискіпливе третє рішення нижче.

Використання stdin

Якщо вам не потрібно подавати дані у віддалену команду, це найпростіше рішення.

Якщо ви знаєте, що віддалений хост має xargsкоманду, яка підтримує -0параметр, і команда не надто велика, ви можете:

printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'

Цей xargs -0 env --командний рядок інтерпретується однаково з усіма цими сімействами оболонок. xargsчитає список обмежених нулем аргументів на stdin та передає їх як аргументи env. Це передбачає, що перший аргумент (назва команди) не містить =символів.

Або ви можете використовувати shна віддаленому хості після цитування кожного елемента, використовуючи shсинтаксис цитування.

shquote() {
  LC_ALL=C awk -v q=\' '
    BEGIN{
      for (i=1; i<ARGC; i++) {
        gsub(q, q "\\" q q, ARGV[i])
        printf "%s ", q ARGV[i] q
      }
      print ""
    }' "$@"
}
shquote "${cmd[@]}" | ssh user@host sh

Використання змінних середовища

Тепер, якщо вам потрібно подати деякі дані від клієнта до stdin віддаленої команди, вищевказане рішення не працюватиме.

Деякі sshрозгортання сервера дозволяють передавати довільні змінні середовища від клієнта до сервера. Наприклад, багато відкритих розгортань в системах на базі Debian дозволяють передавати змінні, ім'я яких починається з LC_.

У цих випадках у вас може бути LC_CODEзмінна, наприклад, що містить кодований sh код, як зазначено вище, і запустити sh -c 'eval "$LC_CODE"'на віддаленому хості після того, як ви сказали своєму клієнтові передати цю змінну (знову ж таки, це командний рядок, який інтерпретується однаково у кожній оболонці):

LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
  sh -c '\''eval "$LC_CODE"'\'

Побудова командного рядка, сумісного для всіх сімей оболонок

Якщо жоден із наведених вище варіантів не є прийнятним (тому що вам потрібен stdin, а sshd не приймає змінну або тому, що вам потрібне загальне рішення), вам доведеться підготувати командний рядок для віддаленого хоста, сумісний з усіма підтримувані снаряди.

Це особливо хитро, тому що всі ці оболонки (Bourne, csh, rc, es, fish) мають свій різний синтаксис, зокрема різні механізми цитування, а деякі з них мають обмеження, які важко обійти.

Ось рішення, яке я придумав, описую його далі:

#! /usr/bin/perl
my $arg, @ssh, $preamble =
q{printf '%.0s' "'\";set x=\! b=\\\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\\\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
};

@ssh = ('ssh');
while ($arg = shift @ARGV and $arg ne '--') {
  push @ssh, $arg;
}

if (@ARGV) {
  for (@ARGV) {
    s/'/'\$q\$b\$q\$q'/g;
    s/\n/'\$q'\$n'\$q'/g;
    s/!/'\$x'/g;
    s/\\/'\$b'/g;
    $_ = "\$q'$_'\$q";
  }
  push @ssh, "${preamble}exec sh -c 'IFS=;exec '" . join "' '", @ARGV;
}

exec @ssh;

Це perlсценарій обгортки навколо ssh. Я це називаю sexec. Ви називаєте це так:

sexec [ssh-options] user@host -- cmd and its args

так у вашому прикладі:

sexec user@host -- "${cmd[@]}"

І обгортка перетворюється cmd and its argsна командний рядок, який усі оболонки в кінцевому підсумку інтерпретують як виклик cmdсвоїми арг (не зважаючи на їх зміст).

Обмеження:

  • Преамбула та те, як цитується команда, означають, що віддалений командний рядок стає значно більшим, а значить, швидше буде досягнуто обмеження на максимальний розмір командного рядка.
  • Я протестував його лише з: оболонкою Борна (від heirloom toolchest), тире, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, рибою, як знайдено в останній системі Debian та / bin / sh, / usr / bin / ksh, / bin / csh та / usr / xpg4 / bin / sh на Solaris 10.
  • Якщо yashце оболонка віддаленого входу, ви не можете передавати команду, аргументи якої містять недійсні символи, але це обмеження в yashтому, що ви ніяк не можете обійти.
  • Деякі оболонки, такі як csh або bash, читають деякі файли запуску, коли викликаються через ssh. Ми припускаємо, що вони не змінюють поведінку кардинально, так що преамбула все ще працює.
  • Крім того sh, він також передбачає, що віддалена система має printfкоманду.

Щоб зрозуміти, як це працює, потрібно знати, як працює цитування в різних оболонках:

  • Борн: '...'сильні цитати без особливого характеру. "..."є слабкими котируваннями, куди "можна уникнути зворотної косої риси.
  • csh. Те саме, що Борн, за винятком того, що "всередину не можна уникнути "...". Також символ нового рядка повинен бути введений з попередньою косою рисою. І !викликає проблеми навіть всередині одиничних цитат.
  • rc. Єдині цитати '...'(сильні). Єдина пропозиція в межах однієї лапки вводиться як ''(як '...''...'). Подвійні котирування чи зворотні риси не особливі.
  • es. Так само, як і rc, за винятком того, що зовнішні котирування, зворотна косої риси може уникнути єдиної цитати.
  • fish: Такий же , як Bourne винятком того, що зворотний слеш 'всередині '...'.

Зі всіма цими протипоказаннями легко помітити, що не можна надійно цитувати аргументи командного рядка, щоб він працював з усіма оболонками.

Використання одинарних лапок як:

'foo' 'bar'

працює у всіх, крім:

'echo' 'It'\''s'

не працював би в rc.

'echo' 'foo
bar'

не працював би в csh.

'echo' 'foo\'

не працював би в fish.

Однак ми повинні бути в змозі працювати навколо більшості з цих проблем , якщо нам вдасться зберегти ці проблемні символи в змінних, як зворотна коса риса $b, одинарні лапки в $q, символ нового рядка в $n!в $xдля розширення історії CSH) в оболонці незалежний чином.

'echo' 'It'$q's'
'echo' 'foo'$b

працював би у всіх оболонках. Це все одно не працює для нового рядка, cshхоча. Якщо він $nмістить новий рядок, в csh, ви повинні написати його, $n:qщоб розширити його до нового рядка, і це не буде працювати для інших оболонок. Отже, те, що ми в кінцевому підсумку робимо натомість тут, - це закликати shі shрозширити їх $n. Це також означає, що потрібно виконати два рівні котирування, один для оболонки віддаленого входу та один для sh.

У $preambleцьому коді є найскладнішою частиною. Це робить використання різних різного котирування правил у всіх оболонках , щоб мати деякі ділянки коду витлумачений лише однією з оболонок ( в той час як закоментовані для інших) , кожного з яких тільки що визначають ті $b, $q, $n, $xзмінних для їх відповідної оболонки.

Ось код оболонки, який інтерпретується оболонкою для входу віддаленого користувача hostдля вашого прикладу:

printf '%.0s' "'\";set x=\! b=\\;setenv n "\
";set q=\';printf %.0s "\""'"';q='''';n=``()echo;x=!;b='\'
printf '%.0s' '\'';set b \\;set x !;set -x n \n;set q \'
printf '%.0s' '\'' #'"\"'";export n;x=!;b=\\;IFS=.;set `echo;echo \.`;n=$1 IFS= q=\'
exec sh -c 'IFS=;exec '$q'printf'$q' '$q'<%s>'$b'n'$q' '$q'arg with $and spaces'$q' '$q''$q' '$q'even'$q'$n'$q'* * *'$q'$n'$q'newlines'$q' '$q'and '$q$b$q$q'single quotes'$q$b$q$q''$q' '$q''$x''$x''$q

Цей код в кінцевому підсумку виконує ту саму команду при інтерпретації будь-якої з підтримуваних оболонок.


1
Протокол SSH ( RFC 4254 §6.5 ) визначає віддалену команду як рядок. Сервер вирішує, як інтерпретувати цей рядок. У системах Unix звичайною інтерпретацією є передача рядка в оболонку входу користувача. Для обмеженого облікового запису це може бути щось на зразок rssh або rush, що не приймає довільних команд. В обліковому записі або на ключі може бути навіть примусова команда, яка ігнорує командний рядок, надісланий клієнтом.
Жил "ТАК - перестань бути злим"

1
@Gilles, дякую за посилання RFC. Так, припущення для цього питання та відповідей полягає в тому, що оболонка для входу віддаленого користувача є корисною (як і я можу виконати цю віддалену команду, яку хочу запустити), і одна з основних сімейств оболонок в системах POSIX. Мене не цікавлять обмежені оболонки чи не снаряди, ні командування сили, ні щось, що не дозволить мені запустити цю віддалену команду в будь-якому випадку.
Стефан Шазелас

1
Корисну довідку про основні відмінності синтаксису між деякими загальними оболонками можна знайти на Hyperpolyglot .
lcd047

0

тл; д-р

ssh USER@HOST -p PORT $(printf "%q" "cmd") $(printf "%q" "arg1") \
    $(printf "%q" "arg2")

Для більш детального рішення прочитайте коментарі та ознайомтеся з іншою відповіддю .

опис

Ну, моє рішення не буде працювати з не bashоболонками. Але якщо припустити, що bashз іншого боку, справи стають простішими. Моя ідея - повторне використання printf "%q"для втечі. Крім того, загалом легше читати сценарій з іншого боку, який приймає аргументи. Але якщо команда коротка, то, мабуть, добре її вбудувати. Ось кілька прикладів функцій, які потрібно використовувати в скриптах:

local.sh:

#!/usr/bin/env bash
set -eu

ssh_run() {
    local user_host_port=($(echo "$1" | tr '@:' ' '))
    local user=${user_host_port[0]}
    local host=${user_host_port[1]}
    local port=${user_host_port[2]-22}
    shift 1
    local cmd=("$@")
    local a qcmd=()
    for a in ${cmd[@]+"${cmd[@]}"}; do
        qcmd+=("$(printf "%q" "$a")")
    done
    ssh "$user"@"$host" -p "$port" ${qcmd[@]+"${qcmd[@]}"}
}

ssh_cmd() {
    local user_host_port=$1
    local cmd=$2
    shift 2
    local args=("$@")
    ssh_run "$user_host_port" bash -lc "$cmd" - ${args[@]+"${args[@]}"}
}

ssh_run USER@HOST ./remote.sh "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 "for a; do echo \"'\$a'\"; done" "1  '  \"  2" '3  '\''  "  4'
ssh_cmd USER@HOST:22 'for a; do echo "$a"; done' '1  "2' "3'  4"

remote.sh:

#!/usr/bin/env bash
set -eu
for a; do
    echo "'$a'"
done

Вихід:

'1  '  "  2'
'3  '  "  4'
'1  '  "  2'
'3  '  "  4'
1  "2
3'  4

Крім того, ви можете робити printfроботу самостійно, якщо знаєте, що робите:

ssh USER@HOST ./1.sh '"1  '\''  \"  2"' '"3  '\''  \"  4"'

1
Це передбачає, що оболонка входу віддаленого користувача - це bash (як bash's printf% q цитує в bash mode) і bashдоступна на віддаленій машині. Існує також кілька проблем із відсутніми котируваннями, які можуть спричинити проблеми з пробілами та символами.
Стефан Шазелас

@ StéphaneChazelas Дійсно, моє рішення, ймовірно, націлене лише на bashоболонки. Але, сподіваємось, люди знайдуть це корисним. Я намагався вирішити й інші проблеми. Не соромтесь сказати мені, якщо я чогось, крім bashречі, пропускаю .
x-yuri

1
Зауважте, що він все ще не працює з командою зразка у питанні ( ssh_run user@host "${cmd[@]}"). У вас ще є якісь пропущені цитати.
Стефан Шазелас

1
Так краще. Зауважте, що вихід bash-файлів не printf %qє безпечним для використання в іншій місцевості (і також є досить баггічним; наприклад, у локалях, що використовують схему BIG5, він (4.3.48) цитує εяк α`!). З цього приводу найкраще цитувати все та з одинарними цитатами лише як shquote()у моїй відповіді.
Стефан Шазелас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.