近日在使用ssh命令
ssh user@remote ~/myscript.sh
登陸到遠(yuǎn)程機(jī)器remote上執(zhí)行腳本時,遇到一個奇怪的問題:~/myscript.sh: line n: app: command not found
app是一個新安裝的程序,安裝路徑明明已通過
/etc/profile
配置文件加到環(huán)境變量中,但這里為何會找不到?如果直接登陸機(jī)器remote并執(zhí)行~/myscript.sh
時,app程序可以找到并順利執(zhí)行。但為什么使用了ssh遠(yuǎn)程執(zhí)行同樣的腳本就出錯了呢?兩種方式執(zhí)行腳本到底有何不同?如果你也心存疑問,請跟隨我一起來展開分析。
說明,本文所使用的機(jī)器是:SUSE Linux Enterprise。
問題定位
這看起來像是環(huán)境變量引起的問題,為了證實(shí)這一猜想,我在這條命令之前加了一句:which app
,來查看app的安裝路徑。在remote本機(jī)上執(zhí)行腳本時,它會打印出app正確的安裝路徑。但再次用ssh來執(zhí)行時,卻遇到下面的錯誤:
which: no app in (/usr/bin:/bin:/usr/sbin:/sbin)
這很奇怪,怎么括號中的環(huán)境變量沒有了app
程序的安裝路徑?不是已通過/etc/profile 設(shè)置到PATH
中了?再次在腳本中加入echo $PATH
并以ssh執(zhí)行,這才發(fā)現(xiàn),環(huán)境變量仍是系統(tǒng)初始化時的結(jié)果:
/usr/bin:/bin:/usr/sbin:/sbin
這證明/etc/profile
根本沒有被調(diào)用。為什么?是ssh命令的問題么?
隨后我又嘗試了將上面的ssh分解成下面兩步:
user@local > ssh user@remote # 先遠(yuǎn)程登陸到remote上user@remote> ~/myscript.sh # 然后在返回的shell中執(zhí)行腳本
結(jié)果竟然成功了。那么ssh以這兩種方式執(zhí)行的命令有何不同?帶著這個問題去查詢了man ssh
If command is specified, it is executed on the remote host instead of a login shell.
這說明在指定命令的情況下,命令會在遠(yuǎn)程主機(jī)上執(zhí)行,返回結(jié)果后退出。而未指定時,ssh會直接返回一個登陸的shell。但到這里還是無法理解,直接在遠(yuǎn)程主機(jī)上執(zhí)行和在返回的登陸shell中執(zhí)行有什么區(qū)別?即使在遠(yuǎn)程主機(jī)上執(zhí)行不也是通過shell來執(zhí)行的么?難道是這兩種方式使用的shell有什么不同?
暫時還沒有頭緒,但隱隱感到應(yīng)該與shell有關(guān)。因為我通常使用的是bash,所以又去查詢了man bash,才得到了答案。
bash的四種模式
在man page的INVOCATION一節(jié)講述了bash的四種模式,bash會依據(jù)這四種模式而選擇加載不同的配置文件,而且加載的順序也有所不同。本文ssh問題的答案就存在于這幾種模式當(dāng)中,所以在我們揭開謎底之前先來分析這些模式。
interactive + login shell
第一種模式是交互式的登陸shell,這里面有兩個概念需要解釋: interactive和login:
login故名思義,即登陸,login shell是指用戶以非圖形化界面或者以ssh登陸到機(jī)器上時獲得的第一個shell,簡單些說就是需要輸入用戶名和密碼的shell。因此通常不管以何種方式登陸機(jī)器后用戶獲得的第一個shell就是login shell。
interactive意為交互式,這也很好理解,interactive shell會有一個輸入提示符,并且它的標(biāo)準(zhǔn)輸入、輸出和錯誤輸出都會顯示在控制臺上。所以一般來說只要是需要用戶交互的,即一個命令一個命令的輸入的shell都是interactive shell。而如果無需用戶交互,它便是non-interactive shell。通常來說如bash script.sh 此類執(zhí)行腳本的命令就會啟動一個non-interactive shell,它不需要與用戶進(jìn)行交互,執(zhí)行完后它便會退出創(chuàng)建的shell。
那么此模式最簡單的兩個例子為:
- 用戶直接登陸到機(jī)器獲得的第一個shell
- 用戶使用ssh user@remote獲得的shell
加載配置文件
這種模式下,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
為了驗證這個過程,我們來做一些測試。首先設(shè)計每個配置文件的內(nèi)容如下:
user@remote > cat /etc/profile
echo @ /etc/profile
user@remote > cat ~/.bash_profile
echo @ ~/.bash_profile
user@remote > cat ~/.bash_login
echo @ ~/.bash_login
user@remote > cat ~/.profile
echo @ ~/.profile
然后打開一個login shell,注意,為方便起見,這里使用bash -l
命令,它會打開一個login shell,在man bash中可以看到此參數(shù)的解釋:
-l Make bash act as if it had been invoked as a login shell
進(jìn)入這個新的login shell,便會得到以下輸出:
@ /etc/profile
@ /home/user/.bash_profile
果然與文檔一致,bash首先會加載全局的配置文件/etc/profile,然后去查找~/.bash_profile
,因為其已經(jīng)存在,所以剩下的兩個文件不再會被查找。接下來移除~/.bash_profile
,啟動login shell得到結(jié)果如下:
@ /etc/profile
@ /home/user/.bash_login
因為沒有了~/.bash_profile
的屏蔽,所以~/.bash_login
被加載,但最后一個~/.profile
仍被忽略。再次移除~/.bash_login
,啟動login shell的輸出結(jié)果為:
@ /etc/profile
@ /home/user/.profile
~/.profile
終于熬出頭,得見天日。通過以上三個實(shí)驗,配置文件的加載過程得到了驗證,除去/etc/profile首先被加載外,其余三個文件的加載順序為:~/.bash_profile
> ~/.bash_login
> ~/.profile
,只要找到一個便終止查找。
前面說過,使用ssh也會得到一個login shell,所以如果在另外一臺機(jī)器上運(yùn)行ssh user@remote時,也會得到上面一樣的結(jié)論。
配置文件的意義
那么,為什么bash要弄得這么復(fù)雜?每個配置文件存在的意義是什么?
/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.
原來一切都是為了兼容,這么設(shè)計是為了更好的應(yīng)付在不同shell之間切換的場景。因為bash完全兼容Bourne shell,所以.bash_profile
和.profile
可以很好的處理bash和Bourne shell之間的切換。但是由于C shell和bash之間的基本語法存在著差異,作者認(rèn)為引入.bash_login
并不是個好主意。所以由此我們可以得出這樣的最佳實(shí)踐:
- 應(yīng)該盡量杜絕使用
.bash_login
,如果已經(jīng)創(chuàng)建,那么需要創(chuàng)建.bash_profile
來屏蔽它被調(diào)用 -
.bash_profile
適合放置bash的專屬命令,可以在其最后讀取.profile
,如此一來,便可以很好的在Bourne shell和bash之間切換了
non-interactive + login shell
第二種模式的shell為non-interactive login shell,即非交互式的登陸shell,這種是不太常見的情況。一種創(chuàng)建此shell的方法為:bash -l script.sh
,前面提到過-l參數(shù)是將shell作為一個login shell啟動,而執(zhí)行腳本又使它為non-interactive shell。
對于這種類型的shell,配置文件的加載與第一種完全一樣,在此不再贅述。
interactive + non-login shell
第三種模式為交互式的非登陸shell,這種模式最常見的情況為在一個已有shell中運(yùn)行bash, 此時會打開一個交互式的shell,而因為不再需要登陸,因此不是login shell。
加載配置文件
對于此種情況,啟動shell時會去查找并加載/etc/bash.bashrc
和~/.bashrc
文件。
為了進(jìn)行驗證,與第一種模式一樣,設(shè)計各配置文件內(nèi)容如下:
user@remote > cat /etc/bash.bashrc
echo @ /etc/bash.bashrc
user@remote > cat ~/.bashrc
echo @ ~/.bashrc
然后我們啟動一個交互式的非登陸shell,直接運(yùn)行bash即可,可以得到以下結(jié)果:
@ /etc/bash.bashrc
@ /home/user/.bashrc
由此非常容易的驗證了結(jié)論。
bashrc VS profile
從剛引入的兩個配置文件的存放路徑可以很容易的判斷,第一個文件是全局性的,第二個文件屬于當(dāng)前用戶。在前面的模式當(dāng)中,已經(jīng)出現(xiàn)了幾種配置文件,多數(shù)是以profile命名的,那么為什么這里又增加兩個文件呢?這樣不會增加復(fù)雜度么?我們來看看此處的文件和前面模式中的文件的區(qū)別。
首先看第一種模式中的profile類型文件,它是某個用戶唯一的用來設(shè)置全局環(huán)境變量的地方, 因為用戶可以有多個shell比如bash, sh, zsh等, 但像環(huán)境變量這種其實(shí)只需要在統(tǒng)一的一個地方初始化就可以, 而這個地方就是profile,所以啟動一個login shell會加載此文件,后面由此shell中啟動的新shell進(jìn)程如bash,sh,zsh等都可以由login shell中繼承環(huán)境變量等配置。
接下來看bashrc,其后綴rc的意思為Run Commands,由名字可以推斷出,此處存放bash需要運(yùn)行的命令,但注意,這些命令一般只用于交互式的shell,通常在這里會設(shè)置交互所需要的所有信息,比如bash的補(bǔ)全、alias、顏色、提示符等等。
所以可以看出,引入多種配置文件完全是為了更好的管理配置,每個文件各司其職,只做好自己的事情。
non-interactive + non-login shell
最后一種模式為非交互非登陸的shell,創(chuàng)建這種shell典型有兩種方式:
bash script.sh
ssh user@remote command
這兩種都是創(chuàng)建一個shell,執(zhí)行完腳本之后便退出,不再需要與用戶交互。
加載配置文件
對于這種模式而言,它會去尋找環(huán)境變量BASH_ENV,將變量的值作為文件名進(jìn)行查找,如果找到便加載它。
同樣,我們對其進(jìn)行驗證。首先,測試該環(huán)境變量未定義時配置文件的加載情況,這里需要一個測試腳本:
user@remote > cat ~/script.sh
echo Hello World
然后運(yùn)行bash script.sh,將得到以下結(jié)果:
Hello World
從輸出結(jié)果可以得知,這個新啟動的bash進(jìn)程并沒有加載前面提到的任何配置文件。接下來設(shè)置環(huán)境變量BASH_ENV:
user@remote > export BASH_ENV=~/.bashrc
再次執(zhí)行bash script.sh,結(jié)果為:
@ /home/user/.bashrc
Hello World
果然,~/.bashrc
被加載,而它是由環(huán)境變量BASH_ENV 設(shè)定的。
更為直觀的示圖
至此,四種模式下配置文件如何加載已經(jīng)講完,因為涉及的配置文件有些多,我們再以兩個圖來更為直觀的進(jìn)行描述:
第一張圖來自這篇文章,bash的每種模式會讀取其所在列的內(nèi)容,首先執(zhí)行A,然后是B,C。而B1,B2和B3表示只會執(zhí)行第一個存在的文件:
+----------------+--------+-----------+---------------+
| | 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實(shí)際上已經(jīng)包含了兩種,因為這兩種模式下對配置文件的加載是一致的。
另外一篇文章給出了一個更直觀的圖:
上圖的情況稍稍復(fù)雜一些,因為它使用了幾個關(guān)于配置文件的參數(shù): --login
,--rcfile
,--noprofile
,--norc
,這些參數(shù)的引入會使配置文件的加載稍稍發(fā)生改變,不過總體來說,不影響我們前面的討論,相信這張圖不會給你帶來更多的疑惑。
典型模式總結(jié)
為了更好的理清這幾種模式,下面我們對一些典型的啟動方式各屬于什么模式進(jìn)行一個總結(jié):
- 登陸機(jī)器后的第一個shell:login + interactive
- 新啟動一個shell進(jìn)程,如運(yùn)行bash:non-login + interactive
- 執(zhí)行腳本,如bash script.sh:non-login + non-interactive
- 運(yùn)行頭部有如#!/usr/bin/env bash 的可執(zhí)行文件,如./executable:non-login + non-interactive
- 通過ssh登陸到遠(yuǎn)程主機(jī):login + interactive
- 遠(yuǎn)程執(zhí)行腳本,如ssh user@remote script.sh:non-login + non-interactive
- 遠(yuǎn)程執(zhí)行腳本,同時請求控制臺,如ssh user@remote -t 'echo $PWD':non-login + interactive
- 在圖形化界面中打開terminal:
- Linux上: non-login + interactive
- Mac OS X上: login + interactive
相信你在理解了login和interactive的含義之后,應(yīng)該會很容易對上面的啟動方式進(jìn)行歸類。
再次嘗試
在介紹完bash的這些模式之后,我們再回頭來看文章開頭的問題。ssh user@remote ~/myscript.sh
屬于哪一種模式?相信此時你可以非常輕松的回答出來:non-login + non-interactive。對于這種模式,bash會選擇加載$BASH_ENV
的值所對應(yīng)的文件,所以為了讓它加載/etc/profile,可以設(shè)定:
user@remote > export BASH_ENV=/etc/profile
然后執(zhí)行上面的命令,但是很遺憾,發(fā)現(xiàn)錯誤依舊存在。這是怎么回事?別著急,這并不是我們前面的介紹出錯了。仔細(xì)查看之后才發(fā)現(xiàn)腳本myscript.sh
的第一行為#!/usr/bin/env sh
,注意看,它和前面提到的#!/usr/bin/env bash
不一樣,可能就是這里出了問題。我們先嘗試把它改成#!/usr/bin/env bash,再次執(zhí)行,錯誤果然消失了,這與我們前面的分析結(jié)果一致。
第一行的這個語句有什么用?設(shè)置成sh和bash有什么區(qū)別?帶著這些疑問,再來查看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,那么我們前面討論的都不復(fù)有效了,真糟糕。我們來看看這個sh的路徑:
user@remote > ll `which sh`
lrwxrwxrwx 1 root root 9 Apr 25 2014 /usr/bin/sh -> /bin/bash
原來sh只是bash的一個軟鏈接,既然如此,BASH_ENV應(yīng)該是有效的啊,為何此處無效?還是回到man bash,同樣在INVOCATION一節(jié)的下部看到了這樣的說明:
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.
簡而言之,當(dāng)bash以是sh命啟動時,即我們此處的情況,bash會盡可能的模仿sh,所以配置文件的加載變成了下面這樣:
- interactive + login: 讀取
/etc/profile
和~/.profile
- non-interactive + login: 同上
- interactive + non-login: 讀取
ENV
環(huán)境變量對應(yīng)的文件 - non-interactive + non-login: 不讀取任何文件
這樣便可以解釋為什么出錯了,因為這里屬于non-interactive + non-login,所以bash不會讀取任何文件,故而即使設(shè)置了BASH_ENV
也不會起作用。所以為了解決問題,只需要把sh換成bash,再設(shè)置環(huán)境變量BASH_ENV
即可。
另外,其實(shí)我們還可以設(shè)置參數(shù)到第一行的解釋器中,如#!/bin/bash --login
,如此一來,bash便會強(qiáng)制為login shell,所以/etc/profile也會被加載。相比上面那種方法,這種更為簡單。
配置文件建議
回顧一下前面提到的所有配置文件,總共有以下幾種:
- /etc/profile
- ~/.bash_profile
- ~/.bash_login
- ~/.profile
- /etc/bash.bashrc
- ~/.bashrc
- $BASH_ENV
- $ENV
不知你是否會有疑問,這么多的配置文件,究竟每個文件里面應(yīng)該包含哪些配置,比如PATH應(yīng)該在哪?提示符應(yīng)該在哪配置?啟動的程序應(yīng)該在哪?等等。所以在文章的最后,我搜羅了一些最佳實(shí)踐供各位參考。(這里只討論屬于用戶個人的配置文件)
-
~/.bash_profile
:應(yīng)該盡可能的簡單,通常會在最后加載.profile
和.bashrc
(注意順序) -
~/.bash_login
:在前面討論過,別用它 -
~/.profile
:此文件用于login shell,所有你想在整個用戶會話期間都有效的內(nèi)容都應(yīng)該放置于此,比如啟動進(jìn)程,環(huán)境變量等 -
~/.bashrc
:只放置與bash有關(guān)的命令,所有與交互有關(guān)的命令都應(yīng)該出現(xiàn)在此,比如bash的補(bǔ)全、alias、顏色、提示符等等。特別注意:別在這里輸出任何內(nèi)容(我們前面只是為了演示,別學(xué)我哈)
寫在結(jié)尾
至此,我們詳細(xì)的討論完了bash的幾種工作模式,并且給出了配置文件內(nèi)容的建議。通過這些模式的介紹,本文開始遇到的問題也很容易的得到了解決。以前雖然一直使用bash,但真的不清楚里面包含了如此多的內(nèi)容。同時感受到Linux的文檔的確做得非常細(xì)致,在完全不需要其它安裝包的情況下,你就可以得到一個非常完善的開發(fā)環(huán)境,這也曾是Eric S. Raymond在其著作《UNIX編程藝術(shù)》中提到的:UNIX天生是一個非常完善的開發(fā)機(jī)器。本文幾乎所有的內(nèi)容你都可以通過閱讀man page得到。最后,希望在這樣一個被妖魔化的特殊日子里,這篇文章能夠為你帶去一絲幫助。
(全文完)
feihu
2014.11.11 于 Shenzhen
來自 http://feihu.me/blog/2014/env-problem-when-ssh-executing-command-on-remote/