ssh
실행할 때 성가신 기능이 있습니다.
ssh user@host cmd and "here's" "one arg"
cmd
인수를 공백으로 host
연결 하고 cmd
쉘을 실행하여 host
결과 문자열을 해석합니다(그래서 ssh
대신 호출되는 것 같습니다 sexec
).
user
더 나쁜 것은 로그인 쉘 이 Bourne의 로그인 쉘과 동일하다는 보장조차 없기 때문에 해당 문자열을 해석하는 데 어떤 쉘이 사용될지 알 수 없다는 것입니다. 여전히 이를 tcsh
로그인 쉘로 사용하는 사람들이 있고 fish
그 수가 증가하고 있기 때문입니다.
이 문제를 해결할 방법이 있나요?
배열에 저장된 인수 목록으로 명령이 있다고 가정합니다 bash
. 각 인수에는 null이 아닌 바이트 시퀀스가 포함될 수 있습니다. 명령에 대한 로그인 쉘에 관계없이 일관된 방식으로 실행되도록 하는 방법 이 있습니까 host
? 주요 Unix 쉘 제품군(Bourne, csh, rc/es, fish) 중 하나입니까?user
user
host
내가 할 수 있어야 하는 또 다른 합리적인 가정은 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
로컬 쉘에서 생성된 것과 동일한 방식으로 참조를 이해하고) 동일한 로케일에서 실행되는 경우에만 작동합니다.
답변1
ssh
나는 어떤 구현 에도 셸을 사용하지 않고 클라이언트에서 서버로 명령을 전달하는 기본 방법이 없다고 생각합니다 .
이제 원격 셸에 특정 인터프리터만 실행하도록 지시하고(예: 예상 구문을 알고 있음) 다른 방식으로 실행될 코드를 제공할 수 있다면 sh
작업이 더 쉬워질 것입니다 .
예를 들어, 또 다른 접근 방식은 다음과 같습니다.표준 입력또는환경 변수.
둘 다 작동하지 않으면 아래에서 세 번째 해결책을 생각해 냈습니다.
표준 입력 사용
이는 원격 명령에 데이터를 제공할 필요가 없는 경우 가장 간단한 솔루션입니다.
원격 호스트에 xargs
이 -0
옵션을 지원하는 명령이 있고 명령이 너무 크지 않은 경우 다음을 수행할 수 있습니다.
printf '%s\0' "${cmd[@]}" | ssh user@host 'xargs -0 env --'
명령 xargs -0 env --
줄은 이러한 모든 쉘 제품군에 대해 동일하게 해석됩니다. xargs
stdin에서 null로 구분된 인수 목록을 읽고 이를 에 인수로 전달합니다 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
그렇지 않으면:
print -r -- ${(qq)cmd} | ssh user@host sh
zsh를 로컬 쉘로 사용하는 경우.
환경 변수 사용
이제 클라이언트에서 원격 명령의 표준 입력으로 일부 데이터를 공급해야 하는 경우 위의 솔루션은 작동하지 않습니다.
그러나 일부 ssh
서버 배포에서는 임의의 환경 변수를 클라이언트에서 서버로 전달할 수 있습니다. 예를 들어 Debian 기반 시스템의 많은 openssh 배포에서는 LC_
.
LC_CODE
이러한 경우에는 예를 들어 다음을 포함하는 변수를 가질 수 있습니다.sh가 인용함 sh
위와 같이 코딩하고 sh -c 'eval "$LC_CODE"'
클라이언트에 해당 변수를 전달하라고 지시한 후 원격 호스트에서 실행합니다(이 역시 모든 셸에서 동일하게 해석되는 명령줄입니다).
LC_CODE=$(shquote "${cmd[@]}") ssh -o SendEnv=LC_CODE user@host '
sh -c '\''eval "$LC_CODE"'\'
또는:
LC_CODE=${(qqj[ ])cmd} ssh -o SendEnv=LC_CODE user@host '
sh -c '\''eval "$LC_CODE"'\'
존재하다 zsh
.
모든 쉘 제품군과 호환되는 명령줄 구축
위의 옵션 중 어느 것도 허용되지 않는 경우(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
한계:
- 서문과 명령이 인용되는 방식은 원격 명령줄이 훨씬 더 커진다는 것을 의미하며, 이는 최대 명령줄 크기 제한에 더 빨리 도달한다는 의미입니다.
- 저는 Bourne 쉘(가보 도구 상자에 있음), dash, bash, zsh, mksh, lksh, yash, ksh93, rc, es, akanga, csh, tcsh, 최근 Debian 시스템에서 발견된 fish 및/Solaris bin/으로만 테스트했습니다. sh, /usr/bin/ksh, /bin/csh 및 /usr/xpg4/bin/sh(10).
yash
텔넷 셸인 경우 인수에 잘못된 문자가 포함된 명령을 전달할 수 없지만 이는 어쨌든yash
해결할 수 없는 제한 사항입니다.- 일부 셸(예: csh 또는 bash)은 ssh를 통해 호출될 때 일부 시작 파일을 읽습니다. 우리는 이것이 행동을 크게 바꾸지 않는다고 가정하므로 서문은 유효합니다.
- 무엇보다도
sh
원격 시스템에printf
명령이 있다고 가정합니다.
이것이 어떻게 작동하는지 이해하려면 다른 셸에서 인용이 어떻게 작동하는지 알아야 합니다.
- Bourne:
'...'
강력한 인용문입니다아니요그 안에 특수 문자가 있습니다."..."
약한 따옴표이며"
백슬래시로 이스케이프할 수 있습니다. csh
."
탈출구가 없다는 점을 제외하면 The Bourne Supremacy와 같습니다"..."
. 또한 개행 문자를 입력해야 하며 앞에 백슬래시가 와야 합니다.!
작은따옴표 안에도 문제가 발생할 수 있습니다.rc
. 유일한 따옴표는'...'
(strong)입니다. 작은따옴표 안의 작은따옴표는''
(예 : ) 로 입력됩니다'...''...'
. 큰따옴표나 백슬래시는 특별하지 않습니다.es
. 외부 따옴표를 제외하면 rc와 동일하며 백슬래시는 작은 따옴표를 이스케이프할 수 있습니다.fish
'
: 백슬래시가 내부적으로 이스케이프된다는 점을 제외하면 Bourne과 동일합니다'...'
.
이러한 모든 제한으로 인해 모든 셸에서 작동하도록 명령줄 인수를 안정적으로 인용할 수 있는 방법이 없다는 것을 쉽게 알 수 있습니다.
다음과 같이 작은따옴표를 사용하세요.
'foo' 'bar'
다음을 제외한 모든 상황에 적용됩니다.
'echo' 'It'\''s'
에서는 작동하지 않습니다 rc
.
'echo' 'foo
bar'
에서는 작동하지 않습니다 csh
.
'echo' 'foo\'
에서는 작동하지 않습니다 fish
.
$b
그러나 이러한 문제가 있는 문자를 의 백슬래시 , 의 작은따옴표 $q
, 의 개행 ( 및 $n
csh 기록 확장 ) 과 같은 쉘 독립적 방식으로 변수에 저장하면 대부분의 문제를 해결할 수 있습니다.!
$x
'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
이 코드는 지원되는 셸에서 해석될 때 동일한 명령을 실행하게 됩니다.
답변2
너무 길어요.
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"'