近日在使用ssh指令 ssh [email protected] ~/myscript.sh
登陸到遠端機器remote上執行腳本時,遇到一個奇怪的問題:
ssh [email protected] ~/myscript.sh
~/myscript.sh: line n: app: command not found
app是一個新安裝的程式,安裝路徑明明已認證
/etc/profile
配置檔案加到環境變量中,但這裡為何會找不到?如果直接登陸機器remote并執行
~/myscript.sh
時,app程式可以找到并順利執行。但為什麼使用了ssh遠端執行同樣的腳本就出錯了呢?兩種方式執行腳本到底有何不同?如果你也心存疑問,請跟随我一起來展開分析。
說明,本文所使用的機器是:SUSE Linux Enterprise。
問題定位
這看起來像是環境變量引起的問題,為了證明這一猜想,我在這條指令之前加了一句:
which app
,來檢視app的安裝路徑。在remote本機上執行腳本時,它會列印出app正确的安裝路徑。但再次用ssh來執行時,卻遇到下面的錯誤:
which: no app in (/usr/bin:/bin:/usr/sbin:/sbin)
這很奇怪,怎麼括号中的環境變量沒有了
app
程式的安裝路徑?不是已認證
/etc/profile
設定到
PATH
中了?再次在腳本中加入
echo $PATH
并以ssh執行,這才發現,環境變量仍是系統初始化時的結果:
/usr/bin:/bin:/usr/sbin:/sbin
這證明
/etc/profile
根本沒有被調用。為什麼?是ssh指令的問題麼?
随後我又嘗試了将上面的ssh分解成下面兩步:
[email protected] > ssh [email protected] # 先遠端登陸到remote上
[email protected]> ~/myscript.sh # 然後在傳回的shell中執行腳本
結果竟然成功了。那麼ssh以這兩種方式執行的指令有何不同?帶着這個問題去查詢了
man ssh
:
If command is specified, it is executed on the remote host instead of a login shell.
這說明在指定指令的情況下,指令會在遠端主機上執行,傳回結果後退出。而未指定時,ssh會直接傳回一個登陸的shell。但到這裡還是無法了解,直接在遠端主機上執行和在傳回的登陸shell中執行有什麼差別?即使在遠端主機上執行不也是通過shell來執行的麼?難道是這兩種方式使用的shell有什麼不同?
暫時還沒有頭緒,但隐隐感到應該與shell有關。因為我通常使用的是
bash
,是以又去查詢了
man bash
,才得到了答案。
bash的四種模式
在man page的INVOCATION一節講述了
bash
的四種模式,
bash
會依據這四種模式而選擇加載不同的配置檔案,而且加載的順序也有所不同。本文ssh問題的答案就存在于這幾種模式當中,是以在我們揭開謎底之前先來分析這些模式。
interactive + login shell
第一種模式是互動式的登陸shell,這裡面有兩個概念需要解釋:interactive和login:
login故名思義,即登陸,login shell是指使用者以非圖形化界面或者以ssh登陸到機器上時獲得的第一個shell,簡單些說就是需要輸入使用者名和密碼的shell。是以通常不管以何種方式登陸機器後使用者獲得的第一個shell就是login shell。
interactive意為互動式,這也很好了解,interactive shell會有一個輸入提示符,并且它的标準輸入、輸出和錯誤輸出都會顯示在控制台上。是以一般來說隻要是需要使用者互動的,即一個指令一個指令的輸入的shell都是interactive shell。而如果無需使用者互動,它便是non-interactive shell。通常來說如
bash script.sh
此類執行腳本的指令就會啟動一個non-interactive shell,它不需要與使用者進行互動,執行完後它便會退出建立的shell。
那麼此模式最簡單的兩個例子為:
- 使用者直接登陸到機器獲得的第一個shell
- 使用者使用
獲得的shellssh [email protected]
加載配置檔案
這種模式下,shell首先加載
/etc/profile
,然後再嘗試依次去加載下列三個配置檔案之一,一旦找到其中一個便不再接着尋找:
- ~/.bash_profile
- ~/.bash_login
- ~/.profile
下面給出這個加載過程的僞代碼:
execute /etc/profile
IF ~/.bash_profile exists THEN
execute ~/.bash_profile
ELSE
IF ~/.bash_login exist THEN
execute ~/.bash_login
ELSE
IF ~/.profile exist THEN
execute ~/.profile
END IF
END IF
END IF
為了驗證這個過程,我們來做一些測試。首先設計每個配置檔案的内容如下:
[email protected] > cat /etc/profile
echo @ /etc/profile
[email protected] > cat ~/.bash_profile
echo @ ~/.bash_profile
[email protected] > cat ~/.bash_login
echo @ ~/.bash_login
[email protected] > cat ~/.profile
echo @ ~/.profile
然後打開一個login shell,注意,為友善起見,這裡使用
bash -l
指令,它會打開一個login shell,在
man bash
中可以看到此參數的解釋:
-l Make bash act as if it had been invoked as a login shell
進入這個新的login shell,便會得到以下輸出:
@ /etc/profile
@ /home/user/.bash_profile
果然與文檔一緻,bash首先會加載全局的配置檔案
/etc/profile
,然後去查找
~/.bash_profile
,因為其已經存在,是以剩下的兩個檔案不再會被查找。
接下來移除
~/.bash_profile
,啟動login shell得到結果如下:
@ /etc/profile
@ /home/user/.bash_login
因為沒有了
~/.bash_profile
的屏蔽,是以
~/.bash_login
被加載,但最後一個
~/.profile
仍被忽略。
再次移除
~/.bash_login
,啟動login shell的輸出結果為:
@ /etc/profile
@ /home/user/.profile
~/.profile
終于熬出頭,得見天日。通過以上三個實驗,配置檔案的加載過程得到了驗證,除去
/etc/profile
首先被加載外,其餘三個檔案的加載順序為:
~/.bash_profile
>
~/.bash_login
>
~/.profile
,隻要找到一個便終止查找。
前面說過,使用ssh也會得到一個login shell,是以如果在另外一台機器上運作
ssh [email protected]
時,也會得到上面一樣的結論。
配置檔案的意義
那麼,為什麼bash要弄得這麼複雜?每個配置檔案存在的意義是什麼?
/etc/profile
很好了解,它是一個全局的配置檔案。後面三個位于使用者主目錄中的配置檔案都針對使用者個人,也許你會問為什麼要有這麼多,隻用一個
~/.profile
不好麼?究竟每個檔案有什麼意義呢?這是個好問題。
Cameron Newham和Bill Rosenblatt在他們的著作《Learning the bash Shell, 2nd Edition》的59頁解釋了原因:
bash allows two synonyms for .bash_profile: .bash_login, derived from the C shell’s file named .login, and .profile, derived from the Bourne shell and Korn shell files named .profile. Only one of these three is read when you log in. If .bash_profile doesn’t exist in your home directory, then bash will look for .bash_login. If that doesn’t exist it will look for .profile.
One advantage of bash’s ability to look for either synonym is that you can retain your .profile if you have been using the Bourne shell. If you need to add bash-specific commands, you can put them in .bash_profile followed by the command source .profile. When you log in, all the bash-specific commands will be executed and bash will source .profile, executing the remaining commands. If you decide to switch to using the Bourne shell you don’t have to modify your existing files. A similar approach was intended for .bash_login and the C shell .login, but due to differences in the basic syntax of the shells, this is not a good idea.
原來一切都是為了相容,這麼設計是為了更好的應付在不同shell之間切換的場景。因為bash完全相容Bourne shell,是以
.bash_profile
和
.profile
可以很好的處理bash和Bourne shell之間的切換。但是由于C shell和bash之間的基本文法存在着差異,作者認為引入
.bash_login
并不是個好主意。是以由此我們可以得出這樣的最佳實踐:
- 應該盡量杜絕使用
,如果已經建立,那麼需要建立.bash_login
來屏蔽它被調用.bash_profile
-
适合放置bash的專屬指令,可以在其最後讀取.bash_profile
,如此一來,便可以很好的在Bourne shell和bash之間切換了.profile
non-interactive + login shell
第二種模式的shell為non-interactive login shell,即非互動式的登陸shell,這種是不太常見的情況。一種建立此shell的方法為:
bash -l script.sh
,前面提到過-l參數是将shell作為一個login shell啟動,而執行腳本又使它為non-interactive shell。
對于這種類型的shell,配置檔案的加載與第一種完全一樣,在此不再贅述。
interactive + non-login shell
第三種模式為互動式的非登陸shell,這種模式最常見的情況為在一個已有shell中運作
bash
,此時會打開一個互動式的shell,而因為不再需要登陸,是以不是login shell。
加載配置檔案
對于此種情況,啟動shell時會去查找并加載
/etc/bash.bashrc
和
~/.bashrc
檔案。
為了進行驗證,與第一種模式一樣,設計各配置檔案内容如下:
[email protected] > cat /etc/bash.bashrc
echo @ /etc/bash.bashrc
[email protected] > cat ~/.bashrc
echo @ ~/.bashrc
然後我們啟動一個互動式的非登陸shell,直接運作
bash
即可,可以得到以下結果:
@ /etc/bash.bashrc
@ /home/user/.bashrc
由此非常容易的驗證了結論。
bashrc VS profile
從剛引入的兩個配置檔案的存放路徑可以很容易的判斷,第一個檔案是全局性的,第二個檔案屬于目前使用者。在前面的模式當中,已經出現了幾種配置檔案,多數是以profile命名的,那麼為什麼這裡又增加兩個檔案呢?這樣不會增加複雜度麼?我們來看看此處的檔案和前面模式中的檔案的差別。
首先看第一種模式中的profile類型檔案,它是某個使用者唯一的用來設定全局環境變量的地方, 因為使用者可以有多個shell比如bash, sh, zsh等, 但像環境變量這種其實隻需要在統一的一個地方初始化就可以, 而這個地方就是profile,是以啟動一個login shell會加載此檔案,後面由此shell中啟動的新shell程序如bash,sh,zsh等都可以由login shell中繼承環境變量等配置。
接下來看bashrc,其字尾
rc
的意思為Run Commands,由名字可以推斷出,此處存放bash需要運作的指令,但注意,這些指令一般隻用于互動式的shell,通常在這裡會設定互動所需要的所有資訊,比如bash的補全、alias、顔色、提示符等等。
是以可以看出,引入多種配置檔案完全是為了更好的管理配置,每個檔案各司其職,隻做好自己的事情。
non-interactive + non-login shell
最後一種模式為非互動非登陸的shell,建立這種shell典型有兩種方式:
- bash script.sh
- ssh [email protected] command
這兩種都是建立一個shell,執行完腳本之後便退出,不再需要與使用者互動。
加載配置檔案
對于這種模式而言,它會去尋找環境變量
BASH_ENV
,将變量的值作為檔案名進行查找,如果找到便加載它。
同樣,我們對其進行驗證。首先,測試該環境變量未定義時配置檔案的加載情況,這裡需要一個測試腳本:
[email protected] > cat ~/script.sh
echo Hello World
然後運作
bash script.sh
,将得到以下結果:
Hello World
從輸出結果可以得知,這個新啟動的bash程序并沒有加載前面提到的任何配置檔案。接下來設定環境變量
BASH_ENV
:
[email protected] > export BASH_ENV=~/.bashrc
再次執行
bash script.sh
,結果為:
@ /home/user/.bashrc
Hello World
果然,
~/.bashrc
被加載,而它是由環境變量
BASH_ENV
設定的。
更為直覺的示圖
至此,四種模式下配置檔案如何加載已經講完,因為涉及的配置檔案有些多,我們再以兩個圖來更為直覺的進行描述:
第一張圖來自這篇文章,bash的每種模式會讀取其所在列的内容,首先執行A,然後是B,C。而B1,B2和B3表示隻會執行第一個存在的檔案:
+----------------+--------+-----------+---------------+
| | login |interactive|non-interactive|
| | |non-login |non-login |
+----------------+--------+-----------+---------------+
|/etc/profile | A | | |
+----------------+--------+-----------+---------------+
|/etc/bash.bashrc| | A | |
+----------------+--------+-----------+---------------+
|~/.bashrc | | B | |
+----------------+--------+-----------+---------------+
|~/.bash_profile | B1 | | |
+----------------+--------+-----------+---------------+
|~/.bash_login | B2 | | |
+----------------+--------+-----------+---------------+
|~/.profile | B3 | | |
+----------------+--------+-----------+---------------+
|BASH_ENV | | | A |
+----------------+--------+-----------+---------------+
上圖隻給出了三種模式,原因是第一種login實際上已經包含了兩種,因為這兩種模式下對配置檔案的加載是一緻的。
另外一篇文章給出了一個更直覺的圖:

上圖的情況稍稍複雜一些,因為它使用了幾個關于配置檔案的參數:
--login
,
--rcfile
,
--noprofile
,
--norc
,這些參數的引入會使配置檔案的加載稍稍發生改變,不過總體來說,不影響我們前面的讨論,相信這張圖不會給你帶來更多的疑惑。
典型模式總結
為了更好的理清這幾種模式,下面我們對一些典型的啟動方式各屬于什麼模式進行一個總結:
- 登陸機器後的第一個shell:login + interactive
- 新啟動一個shell程序,如運作
:non-login + interactivebash
- 執行腳本,如
:non-login + non-interactivebash script.sh
- 運作頭部有如
的可執行檔案,如#!/usr/bin/env bash
:non-login + non-interactive./executable
- 通過ssh登陸到遠端主機:login + interactive
- 遠端執行腳本,如
:non-login + non-interactivessh [email protected] script.sh
- 遠端執行腳本,同時請求控制台,如
:non-login + interactivessh [email protected] -t 'echo $PWD'
- 在圖形化界面中打開terminal:
- Linux上: non-login + interactive
- Mac OS X上: login + interactive
相信你在了解了login和interactive的含義之後,應該會很容易對上面的啟動方式進行歸類。
再次嘗試
在介紹完bash的這些模式之後,我們再回頭來看文章開頭的問題。
ssh [email protected] ~/myscript.sh
屬于哪一種模式?相信此時你可以非常輕松的回答出來:non-login + non-interactive。對于這種模式,bash會選擇加載
$BASH_ENV
的值所對應的檔案,是以為了讓它加載
/etc/profile
,可以設定:
[email protected] > export BASH_ENV=/etc/profile
然後執行上面的指令,但是很遺憾,發現錯誤依舊存在。這是怎麼回事?别着急,這并不是我們前面的介紹出錯了。仔細檢視之後才發現腳本
myscript.sh
的第一行為
#!/usr/bin/env sh
,注意看,它和前面提到的
#!/usr/bin/env bash
不一樣,可能就是這裡出了問題。我們先嘗試把它改成
#!/usr/bin/env bash
,再次執行,錯誤果然消失了,這與我們前面的分析結果一緻。
第一行的這個語句有什麼用?設定成sh和bash有什麼差別?帶着這些疑問,再來檢視
man bash
:
If the program is a file beginning with #!, the remainder of the first line specifies an interpreter for the program.
它表示這個檔案的解釋器,即用什麼程式來打開此檔案,就好比Windows上輕按兩下一個檔案時會以什麼程式打開一樣。因為這裡不是bash,而是sh,那麼我們前面讨論的都不複有效了,真糟糕。我們來看看這個sh的路徑:
[email protected] > ll `which sh`
lrwxrwxrwx 1 root root 9 Apr 25 2014 /usr/bin/sh -> /bin/bash
原來sh隻是bash的一個軟連結,既然如此,
BASH_ENV
應該是有效的啊,為何此處無效?還是回到
man bash
,同樣在INVOCATION一節的下部看到了這樣的說明:
If bash is invoked with the name sh, it tries to mimic the startup behavior of historical versions of sh as closely as possible, while conforming to the POSIX standard as well. When invoked as an interactive login shell, or a non-interactive shell with the –login option, it first attempts to read and execute commands from /etc/profile and ~/.profile, in that order. The –noprofile option may be used to inhibit this behavior. When invoked as an interactive shell with the name sh, bash looks for the variable ENV, expands its value if it is defined, and uses the expanded value as the name of a file to read and execute. Since a shell invoked as sh does not attempt to read and execute commands from any other startup files, the –rcfile option has no effect. A non-interactive shell invoked with the name sh does not attempt to read any other startup files. When invoked as sh, bash enters posix mode after the startup files are read.
簡而言之,當bash以是sh命啟動時,即我們此處的情況,bash會盡可能的模仿sh,是以配置檔案的加載變成了下面這樣:
- interactive + login: 讀取
和/etc/profile
~/.profile
- non-interactive + login: 同上
- interactive + non-login: 讀取
環境變量對應的檔案ENV
- non-interactive + non-login: 不讀取任何檔案
這樣便可以解釋為什麼出錯了,因為這裡屬于non-interactive + non-login,是以bash不會讀取任何檔案,故而即使設定了
BASH_ENV
也不會起作用。是以為了解決問題,隻需要把sh換成bash,再設定環境變量
BASH_ENV
即可。
另外,其實我們還可以設定參數到第一行的解釋器中,如
#!/bin/bash --login
,如此一來,bash便會強制為login shell,是以
/etc/profile
也會被加載。相比上面那種方法,這種更為簡單。
配置檔案建議
回顧一下前面提到的所有配置檔案,總共有以下幾種:
- /etc/profile
- ~/.bash_profile
- ~/.bash_login
- ~/.profile
- /etc/bash.bashrc
- ~/.bashrc
- $BASH_ENV
- $ENV
不知你是否會有疑問,這麼多的配置檔案,究竟每個檔案裡面應該包含哪些配置,比如
PATH
應該在哪?提示符應該在哪配置?啟動的程式應該在哪?等等。是以在文章的最後,我搜羅了一些最佳實踐供各位參考。(這裡隻讨論屬于使用者個人的配置檔案)
-
:應該盡可能的簡單,通常會在最後加載~/.bash_profile
和.profile
(注意順序).bashrc
-
:在前面讨論過,别用它~/.bash_login
-
:此檔案用于login shell,所有你想在整個使用者會話期間都有效的内容都應該放置于此,比如啟動程序,環境變量等~/.profile
-
:隻放置與bash有關的指令,所有與互動有關的指令都應該出現在此,比如bash的補全、alias、顔色、提示符等等。特别注意:别在這裡輸出任何内容(我們前面隻是為了示範,别學我哈)~/.bashrc
寫在結尾
至此,我們詳細的讨論完了bash的幾種工作模式,并且給出了配置檔案内容的建議。通過這些模式的介紹,本文開始遇到的問題也很容易的得到了解決。以前雖然一直使用bash,但真的不清楚裡面包含了如此多的内容。同時感受到Linux的文檔的确做得非常細緻,在完全不需要其它安裝包的情況下,你就可以得到一個非常完善的開發環境,這也曾是Eric S. Raymond在其著作《UNIX程式設計藝術》中提到的:UNIX天生是一個非常完善的開發機器。本文幾乎所有的内容你都可以通過閱讀man page得到。最後,希望在這樣一個被妖魔化的特殊日子裡,這篇文章能夠為你帶去一絲幫助。
(全文完)
feihu
2014.11.11 于 Shenzhen
轉自: http://feihu.me/blog/2014/env-problem-when-ssh-executing-command-on-remote/