원격 사용자의 로그인 쉘을 모르고 ssh를 통해 임의의 간단한 명령을 실행하는 방법은 무엇입니까?


26

ssh 실행할 때 성가신 기능이 있습니다.

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

cmd인수를 on 으로 실행하는 대신 공백과 함께 인수를 host연결 하고 결과 문자열을 해석하기 cmd위해 셸을 실행 host합니다 (그렇기 때문에 호출 ssh되지 않은 이유입니다 sexec).

더 나쁜 것은, 어떤 문자열이 해당 문자열을 해석하는 데 사용 될지 알지 못하는 것입니다. 로그인 쉘은 로그인 쉘로 user여전히 사람들이 사용 tcsh하고 있으며 계속 증가하고 있기 때문에 Bourne이 보장되지 않습니다 fish.

그 주위에 방법이 있습니까?

I가 저장된 인수 목록으로 명령 있다고 가정 bash비 - 널 바이트 중 어느 시퀀스를 포함 할 수있다 각각의 배열을, 그것에서 실행해야 할 방법이있다 host같이 user그 로그인 쉘에 관계없이 일관된 방식 user켜짐 host(우리는 Bourne, csh, rc / es, fish의 주요 유닉스 쉘 제품군 중 하나라고 가정합니다)?

내가 할 수 있어야하는 또 다른 합리적인 가정 은 Bourne과 호환되는 sh명령을 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 shell로 끝났다면, 그렇지 않습니까? 물론 특수 문자를 이스케이프 처리하는 것은 조금 더 고통 스럽지만 초기 문제를 해결할 것입니까?
Bananguin

당신이 실행되지 아니하는 경우 @Bananguin, ssh host sh -c 'some cmd'동일, ssh host 'sh -c some cmd'원격 사용자의 로그인 쉘을 가지고, 그 해석 sh -c some cmd명령 줄을. 우리는 그 쉘에 대한 올바른 구문으로 명령을 작성해야하며 (그리고 그것이 무엇인지 알지 못하므로) 인수 와 인수 sh로 호출 해야 합니다 . -csome cmd
Stéphane Chazelas

1
@Otheus, 그렇습니다. sh -c 'some cmd'some cmd명령 줄은 모든 쉘에서 동일하게 해석됩니다. 이제 echo \'원격 호스트 에서 Bourne 명령 줄 을 실행하려면 어떻게해야 합니까? echo command-string | ssh ... /bin/sh내 대답에 준 하나의 솔루션이지만 원격 명령의 stdin에 데이터를 공급할 수 없습니다.
Stéphane Chazelas

1
더 오래 지속되는 솔루션은 ftp 플러그인 인 ssh 용 rexec 플러그인입니다.
Otheus

1
@myrdd, 아닙니다. 쉘 명령 행에서 인수를 분리하려면 공백이나 탭이 필요합니다. 경우 cmd이며 cmd=(echo "foo bar"), 쉘 명령 줄에 전달 ssh''에코 ''foo는 바 '같은 것을해야한다 . The *first* space (the one before 에코 ) is superflous, but doen't harm. The other one (the ones before 'foo는 바 ' ) is needed. With '%의 Q ' , we'd pass a 'echo''foo bar'` 명령 줄을.
Stéphane Chazelas

답변:


19

ssh셸을 사용하지 않고 클라이언트에서 서버로 명령을 전달하는 기본 방법이 구현에 있다고 생각하지 않습니다 .

이제 원격 쉘에게 특정 인터프리터 (예 sh: 예상 구문을 알고 있음) 만 실행하고 다른 방법으로 실행할 코드를 제공하도록 지시 하면 상황이 더 쉬워 질 수 있습니다 .

다른 평균은 예를 들어 표준 입력 또는 환경 변수 일 수 있습니다.

둘 다 사용할 수 없으면 아래에서 해키 해킹 솔루션을 제안합니다.

stdin 사용

원격 명령에 데이터를 공급할 필요가없는 경우 가장 쉬운 솔루션입니다.

원격 호스트 xargs-0옵션 을 지원 하는 명령이 있고 명령이 너무 크지 않은 경우 다음을 수행 할 수 있습니다.

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

해당 xargs -0 env --명령 행은 모든 해당 쉘 제품군과 동일하게 해석됩니다. xargsstdin에서 널로 구분 된 인수 목록을 읽고 인수로 인수로 전달합니다 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그러나 일부 서버 배포에서는 클라이언트에서 서버로 임의의 환경 변수를 전달할 수 있습니다. 예를 들어, 데비안 기반 시스템의 많은 openssh 배포에서는 이름이로 시작하는 변수를 전달할 수 LC_있습니다.

이 경우 위와 같이 따옴표 붙은 코드를 LC_CODE포함 하는 변수를 가질 수 있고 클라이언트에게 해당 변수를 전달하도록 지시 한 후 원격 호스트에서 실행할 수 있습니다 (다시 말해 모든 쉘에서 동일하게 해석되는 명령 줄입니다). shsh -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;

즉 A의 perl래퍼 스크립트 주위 ssh. 나는 그것을 부른다 sexec. 당신은 그것을 다음과 같이 부릅니다.

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

그래서 당신의 예에서 :

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

그리고 랩퍼는 cmd and its args명령 행 으로 바뀌어 모든 쉘 cmd은 인수 를 호출하는 것으로 해석합니다 (내용에 무관심).

한계 :

  • 프리앰블과 명령이 인용되는 방식은 원격 명령 행이 크게 커져 명령 행의 최대 크기에 대한 한계에 빨리 도달한다는 의미입니다.
  • Bourne shell (가보 툴 체스트에서), 대시, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, 최근 데비안 시스템에서 발견 된 물고기 및 / Solaris 10의 경우 bin / sh, / usr / bin / ksh, / bin / csh 및 / usr / xpg4 / bin / sh
  • yash원격 로그인 쉘인 경우 인수에 유효하지 않은 문자가 포함 된 명령을 전달할 yash수 없지만 어쨌든 해결할 수 없다는 한계가 있습니다.
  • csh 또는 bash와 같은 일부 쉘은 ssh를 통해 호출 될 때 일부 시작 파일을 읽습니다. 우리는 그것들이 프리앰블이 여전히 작동하도록 행동을 극적으로 변화시키지 않는다고 가정합니다.
  • 옆에 sh, 그것은 또한 원격 시스템이 가지고 가정 printf명령을 사용합니다.

작동 방식을 이해하려면 다른 쉘에서 인용이 작동하는 방식을 알아야합니다.

  • Bourne : 특별한 특성 '...'없는 강력한 인용문입니다 . 백 슬래시로 이스케이프 할 수있는 "..."약한 따옴표 "입니다.
  • csh. "내부에서 벗어날 수 없다는 점을 제외하고는 Bourne과 동일합니다 "...". 또한 줄 바꿈 문자 앞에 줄 바꿈 문자를 입력해야합니다. 그리고 !심지어는 작은 따옴표 안에 원인 문제.
  • 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 )은 원격 명령을 문자열로 정의합니다. 해당 문자열을 해석하는 방법을 결정하는 것은 서버에 달려 있습니다. 유닉스 시스템에서 일반적인 해석은 문자열을 사용자의 로그인 쉘에 전달하는 것입니다. 제한된 계정의 경우 임의의 명령을 허용하지 않는 rssh 또는 rush와 같은 것일 수 있습니다. 계정이나 키에 클라이언트가 보낸 명령 문자열이 무시되도록하는 강제 명령이있을 수도 있습니다.
Gilles 'SO- 악마 그만'

1
@Gilles, RFC 참조 주셔서 감사합니다. 예,이 Q & A에 대한 가정은 원격 사용자의 로그인 셸을 사용할 수 있고 (실행하려는 원격 명령을 실행할 수있는 것처럼) POSIX 시스템의 기본 셸 제품군 중 하나입니다. 나는 제한된 쉘이나 비 쉘 또는 강제 명령 또는 원격 명령을 실행할 수없는 것에 관심이 없습니다.
Stéphane Chazelas

1
일반적인 쉘들 사이의 문법의 주요 차이점에 대한 유용한 참고 자료는 Hyperpolyglot 에서 찾을 수 있습니다 .
lcd047

0

tl; dr

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의 printf % q가 bash 방식으로 인용 됨)이고 bash원격 시스템에서 사용 가능한 것으로 가정합니다. 따옴표가 누락되어 공백과 와일드 카드에 문제가 발생할 수도 있습니다.
Stéphane Chazelas

@ StéphaneChazelas 사실, 내 솔루션은 아마도 bash껍질 만을 목표로하고 있습니다. 그러나 사람들이 그것을 사용하기를 바랍니다. 그래도 다른 문제를 해결하려고했습니다. 내가 잃어버린 것 외에 다른 것이 있으면 알려주십시오 bash.
x-yuri

1
질문 ( ssh_run user@host "${cmd[@]}") 의 샘플 명령에서는 여전히 작동하지 않습니다 . 여전히 누락 된 따옴표가 있습니다.
Stéphane Chazelas

1
그게 낫다. bash의 출력은 printf %q다른 로케일에서 사용하기에 안전하지 않습니다 (그리고 또한 버그가 있습니다. 예를 들어 BIG5 문자 세트를 사용하는 로케일에서는 (4.3.48) 따옴표 εα`!). 이를 위해 최선의 방법은 모든 것을 인용 shquote()하고 내 대답 과 같이 작은 따옴표 만 사용하는 것입니다.
Stéphane Chazelas
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.