如何透過 ssh 執行複雜的命令列?

如何透過 ssh 執行複雜的命令列?

我有時想取得在遠端主機上執行的 docker 容器的環境。為此,我登入主機:

ssh [email protected]

然後我運行這個命令:

sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

我現在想用一個命令來完成此操作(執行超過 3 次,我想自動化它..:-))。

這有效:

 ssh -t [email protected] sudo docker ps | grep mycontainername

但當我這樣做時

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

我明白了

"docker exec" requires at least 2 arguments.
See 'docker exec --help'.

Usage:  docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

Run a command in a running container
Connection to 123.456.789.10 closed.

有誰知道我怎麼才能讓它運作?

答案1

一般觀察

Bash 中有一些關鍵字會影響對其後面內容的解析,例如[[。但這ssh不是其中之一,它是一個常規命令。這意味著:

  • ssh …行通常由您解析當地的殼;像|;*"$或空格這樣的字元對 shell 來說意味著一些東西,除非您引用或轉義它們,否則它們不會到達ssh$這是解析和解釋的第一層次。

  • sshshell 完成工作後,無論參數(或任何其他常規命令)得到什麼,它們都只是參數、字串。現在工具的工作就是解釋它們。這是第二個層次。

如果ssh其某些(零個、一個或多個)命令列參數被解釋為要在伺服器端運行的命令。一般來說ssh,能夠從許多參數建立命令。效果就好像您在伺服器上調用了類似的東西:

"$SHELL" -c "$command_line_built_by_ssh"

(我並不是說它是確切地像這樣,但它肯定足夠接近以了解發生了什麼。我把它寫得好像在 shell 中呼叫它一樣,所以看起來很熟悉;但實際上還沒有shell。並且沒有$command_line…作為變量,我只是使用這個名稱來引用某個字串以達到此答案的目的。

然後$SHELL在伺服器上$command_line…自行解析。這是第三個層次。


具體觀察

失敗的命令

ssh -t [email protected] sudo docker exec -it `sudo docker ps | grep mycontainername | awk '{print $1;}'` env

失敗,因為123.456.789.10不是有效的 IP 位址。

好的,我知道123.456.789.10這是一個佔位符,但它仍然無效。 :)

該命令失敗,因為它sudo docker ps | grep mycontainername | awk '{print $1;}'在本地執行。輸出可能是空的。那麼$command_line_built_by_ssh離你想要的還很遠。

注意在本地ssh … | grep mycontainername運行grep(您可能會或可能不會意識到這一點)。


討論

為了控制遠端 shell 將獲得的內容$command_line_built_by_ssh,您需要理解、預測和規劃先前發生的解析和解釋。您需要製作本地命令,所以本地 shell 並消化它,它就變成了您想要在遠端執行的ssh確切內容。$command_line…

如果您確實希望本機 shell 在結果到達之前擴展或取代任何內容,則可能會非常複雜ssh。您的情況更簡單,因為您已經擁有所需的逐字字串$command_line_built_by_ssh。該字串是:

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

筆記:

  • 我以 的形式使用命令替換$(),而不是反引號。有偏好的理由$()
  • 我根本不知道docker,我不知道你是否$(…)應該用雙引號引起來。一般來說不引用幾乎總是不好的。問問自己,當替換返回多個單字(即多行 Enter awk)時會發生什麼。這是一個不同的問題(如果在這種情況下有問題的話),我不會在這個答案中解決它。

為了保護所有內容不被本地 shell 擴展/解釋,您需要正確引用或轉義(使用\)所有可以觸發擴展或可以解釋的字元。在這種情況下$,引用()|;{、 、}'(可能或可選)空格。

我說“也許或可選地引用或轉義空格”是因為它的ssh … some command工作原理。如果它找到兩個或多個參數並將其解釋為要在伺服器上運行的程式碼,它將連接它們,並在它們之間添加單個空格。就是這樣$command_line_built_by_ssh建造的。如果您在看起來像遠端 shell 的程式碼中既不引用也不轉義空格,則本機 shell 在分割單字時將消耗空格(和製表符),然後ssh新增空格。如果存在製表符或多個連續空格,結果可能不完全是您想要的。例如:

ssh user@server echo a     b

ssh得到user@server, echo, a, b。遠端命令將是echo a b,並且echo將得到a, b。它將打印a b

然後這個:

ssh user@server 'echo a     b'

ssh得到user@server, echo a b.遠端命令將是echo a b,並且echo將得到a, b。它將打印a b

最後是這個:

ssh user@server 'echo "a     b"'

ssh得到user@server, echo "a b".遠端命令將會是echo "a b"並且echo將會有 get a b。它將打印a b

結論是您應該在本地 shell 的上下文中引用並且分別地在遠端 shell 的上下文中。請記住,當涉及透過 shell 擴展事物時,外引號很重要


解決方案

將所有這些資訊放在一起(並且仍然假設您想要保護一切從本地 shell 擴展/解釋),我建議如下:

  • 引用或逃避。
  • 更喜歡引用過度轉義是因為一對引號可以保護多個字符,而一對引號\只能保護一個字符。您很可能需要許多反斜線來保護所有內容;通常只需一對引號即可達到相同的結果。
  • 更喜歡單引號( '),他們可以保護除 之外的一切'。另一方面,雙引號 ( ") 可以保護',但不能保護$Nor "\有時不能,!Bash 中有時也不能),除非$和此類也被轉義(即轉義)引用,即在雙引號內轉義;除了! 這是 麻煩的)。
  • 更喜歡提供命令作為單身的論據ssh.

這導致以下過程:

  1. 準備要在遠端運行的逐字命令。
  2. 將 every 替換''"'"'或 with '\''(您可以為每個 單獨選擇')。
  3. 用單引號括住整個結果字串。
  4. 添加ssh …在前面。

您的逐字命令是:

sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '{print $1;}') env

該程序的結果是:

ssh -t user@server 'sudo docker exec -it $(sudo docker ps | grep mycontainername | awk '\''{print $1;}'\'') env'
# single-quoted     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^    ^^^^^
# escaped                                                                                ^              ^

請注意,該過程可能會也可能不會導致最短的字串。憑藉洞察力,有時可以“優化”字串。但流程還蠻準的簡單的並且完全可靠。如果您知道您想要保護所有內容不被本地 shell 擴展/解釋,則該過程本身根本不需要進一步的了解。


自動化

事實上,該過程可以自動化。有些工具可以為字串添加引號並正確保留現有引號。 Bash 本身就是這樣的工具。我的這個回答提供了一種透過 Bash 中的擊鍵來實現此目的的方法。根據您的情況調整的可能解決方案是:

  1. 在本地 shell 中定義以下自訂函數和綁定:

     _prepend_ssh() { READLINE_LINE="ssh  ${READLINE_LINE@Q}"; READLINE_POINT=4; }
     bind -x '"\C-x\C-h":_prepend_ssh'
    
  2. 仍然在本機 shell 中鍵入(或貼上)要在遠端 shell 中執行的命令;不執行。命令應該是確切地正如您希望它位於遠端 shell 中一樣。

  3. Ctrl+ xCtrl+ h。本地 shell 將負責引用。它還會ssh在前面添加並將遊標放在後面。

  4. 新增(類型)缺少的參數(例如-t user@server)。為了您的方便,遊標已經處於執行此操作的正確位置。

  5. Enter


備擇方案

還有另一種方法可以將逐字指令傳遞到遠端 shell。在某些情況下,您可以管道他們透過ssh.我們假設遠端命令列應該是:

echo "$PATH"; date

我們可以像上面一樣繼續,添加單引號並在本地運行,如下所示:

ssh user@server 'echo "$PATH"; date'

這個例子很簡單,但通常添加引號並不總是那麼容易。或者,我們可以像這樣透過管道傳輸命令(echo為了簡單起見,第一個;printf更好):

echo 'echo "$PATH"; date' | ssh user@server bash

仍然需要這些單引號。但是如果文件中有命令,那麼:

<file ssh user@server bash

或甚至沒有任何文件(這裡的文檔):

ssh user@server bash <<'EOF'
echo "$PATH"
date
EOF

<<'EOF'(請注意防止在$PATH本地展開的引號。)

優點:

  • 您可以輕鬆地傳遞多行命令/片段/腳本(我分開echo … ; date只是為了展示這一點)。
  • 不需要額外的引用層。
  • 您可以明確選擇遠端解釋器,該解釋器不必是 shell(例如bashzsh、 或python)。

缺點:

  • 您應該明確指定遠端解釋器,否則預設登入shell 將被生成,也許會列印當天的訊息。您仍然可以透過指定正確的引號來使用預設 shell 作為非登入 shell exec "$SHELL"(該行將類似於ssh … 'exec "$SHELL"' <<'EOF')。
  • 標準輸入ssh不是終端,所以你不能使用-t(這就是為什麼我沒有使用你原來的命令作為例子)。
  • 命令bash透過其標準輸入到達遠端解釋器(在範例中)。可能出現的問題:
    • 子進程(或內建進程)將使用相同的標準輸入。如果它們中的任何一個從其標準輸入讀取,那麼它將讀取相同的流,可能它會讀取發送到解釋器的下一個命令。這種行為可以被抑制,甚至可以創造性地(濫用)使用,但我不會詳細說明。
    • 您無法輕鬆地使用此通道來傳輸其他任何內容。

相關內容