腳本基礎 參考資料:Shell Scripts (Bash Reference Manual) 不嚴謹地說,編程語言根據代碼運行的方式,可以分為兩種方式: 編譯運行:需要先將人類可識別的代碼文件編譯成機器可運行的二進位程式文件後,方可運行。例如C語言和Java語言。 解釋運行:需要一個編程語言的解釋 ...
腳本基礎
不嚴謹地說,編程語言根據代碼運行的方式,可以分為兩種方式:
- 編譯運行:需要先將人類可識別的代碼文件編譯成機器可運行的二進位程式文件後,方可運行。例如C語言和Java語言。
- 解釋運行:需要一個編程語言的解釋器,運行時由解釋器讀取代碼文件並運行。例如python語言(解釋器:/usr/bin/python)和shell腳本(解釋器:/bin/bash)。
根據其是否調用OS上的其他應用程式來分來:
- 腳本語言(shell腳本):依賴於bash自身以及OS中的其他應用程式(例如:grep/sed/awk等)。
- 完整編程語言:依賴自身的語法和其自身豐富的庫文件來完成任務,對系統的依賴性很低,例如python、PHP等。
根據編程模型:
- 過程式:以指令為中心來組織代碼,數據是服務於代碼。像C語言和bash。
- 對象式:以數據為中心來組織代碼,圍繞數據組織指令。其編程的過程一般為創建類(class,例如:人類),根據類實例化出對象(例如:阿龍弟弟),對象具有類的通用屬性(例如人類有手有腳,那麼阿龍弟弟也有),對象可以具備自己的方法(method,例如寫博客)。像Java語言。
像C++和python是既支持面向對象又支持面向過程。
因此我們可以總結出:bash是一門解釋運行的過程式腳本語言,而bash的腳本,是一種將自身的編程邏輯和OS上的命令程式堆砌起來的待執行文件。
在shell腳本中,第一行我們需要向內核傳達我們這個腳本是使用哪種解釋器(interpreter)來解釋運行。形如:
#!/bin/sh 或者 #!/bin/bash 或者 #!/usr/bin/python
“#!”是固定的格式,叫做shebang或者hashbang,後面跟著的是解釋器程式的路徑。如果是/bin/bash那就是bash腳本,如果是/usr/bin/python那就是python腳本。
shebang是可以添加選項的,例如可以使得腳本在執行時為登錄式(login)shell。
#!/bin/bash -l
bash腳本的文件的尾碼名(亦稱擴展名)一般為“.sh”,這個名稱主要用於讓人易識別用的,具體腳本在執行的時候使用什麼解釋器,與文件的尾碼名無關。
腳本還需要具備執行許可權。在執行的時候,需要使用相對路徑或者絕對路徑方可正確執行。
~]# cat alongdidi.sh #!/bin/bash ... ~]# chmod a+x alongdidi.sh ~]# ./alongdidi.sh ~]# /PATH/TO/alongdidi.sh
如果直接鍵入腳本的名稱來執行的話,bash會從內置命令、PATH等中尋找alongdidi.sh命令,如果我們的腳本當前路徑不存在於PATH中,就會報錯。
~]# alongdidi.sh bash: alongdidi.sh: command not found...
腳本也可以沒有shebang和執行許可權,我們依然可以通過調用bash命令,將腳本作為bash的參數來執行,這樣也是可以的。
~]# bash alongdidi.sh
腳本中存在的空白行會被忽略,bash並不會將空白行列印出來。
除了shebang(#!)這種特殊的格式,其餘位置出現#,其後面的字元均會被認為是腳本註釋。
Bash執行一個腳本,實際上是在當前shell下創建子shell來執行的。
配置文件
參考資料:
建議英文不好、bash新手直接參考駿馬兄的博文來學習即可,直接跳過官網的參考資料。駿馬兄本人也是基於官網學習並自己反覆驗證的,準確率應該很高,可放心。
無論我們直接通過連接至物理伺服器/機器的物理設備(滑鼠、鍵盤和顯示器),還是我們通過SSH客戶端(無論GUI或者CLI)連接至Linux伺服器中。系統都會在我們所連接上的終端上啟用一個bash,我們通過這個shell來與OS進行交互。
即使我們執行bash腳本,系統也會創建一個子bash來完成任務。
這些bash在啟動的時候,就需要讀取其配置文件,官方也將其稱之為啟動文件(startup files)。用於使bash在啟動的時候讀取這些文件並執行其中的指令來設置bash環境。
互動式和登錄式
Bash需要判斷自身是否具備互動式(interactive)和登錄式(login)的特性來決定自己應該讀取哪些配置文件。
判斷shell是否為互動式有兩種方法:
方法一:判斷特殊變數“$-”是否包含字母i。bash還有其他的特殊變數,有興趣的請參考Special Parameters (Bash Reference Manual)。
[root@c7-server ~]# echo $- himBH [root@c7-server ~]# cat alongdidi.sh #!/bin/bash echo $- [root@c7-server ~]# bash alongdidi.sh hB
方法二:判斷變數“$PS1”是否為空。互動式登錄會設置該變數,如果變數為空,則為非互動式,否則為互動式。PS1是Prompt String,提示符字元串的意思,在官網中它屬於Bourne Shell的變數之一。
[root@c7-server ~]# echo $PS1 [\u@\h \W]\$ [root@c7-server ~]# cat alongdidi.sh #!/bin/bash echo $PS1 [root@c7-server ~]# bash alongdidi.sh [root@c7-server ~]#
判斷shell是否為登錄式,使用bash的內置命令shopt來查看。它和內置命令set一起都用於修改shell的行為。Modifying Shell Behavior (Bash Reference Manual)
[root@c7-server ~]# shopt login_shell login_shell on [root@c7-server ~]# cat alongdidi.sh #!/bin/bash shopt login_shell [root@c7-server ~]# bash alongdidi.sh login_shell off [root@c7-server ~]# bash [root@c7-server ~]# shopt login_shell login_shell off
常見的bash啟動方式
在此之前需要讀者大概瞭解一下終端的概念,可參考【你真的知道什麼是終端嗎? - Linux大神博客】。
PS:最後還把Windows給黑了一下。確實感覺windows應該算不上多用戶,以前維護Windows Server的時候,使用遠程連接只能以超管用戶連接上2或者3個而已,再多就不行了。目前也不曉得為什麼,可能windows自身的限制如此。
1、通過Xshell客戶端使用SSH協議登錄。
偽終端。互動式,登錄式。
[root@c7-server ~]# tty /dev/pts/1 [root@c7-server ~]# who am i root pts/1 2019-12-12 15:39 (192.168.152.1) [root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell on
2、在圖形界面下右擊桌面打開的終端。
偽終端。互動式,非登錄式。
[root@c7-server ~]# tty /dev/pts/0 [root@c7-server ~]# who am i root pts/0 2019-12-12 15:28 (:0) [root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell off
可通過設置終端的屬性來使其變為登錄式。
該圖形界面,在CentOS 7上本身位於Ctrl+Alt+F1的虛擬終端上。
3、虛擬終端。
通過Ctrl+Alt+Fn來切換,n為正整數,該截圖位於Ctrl+Alt+F2,這種叫虛擬終端。互動式,登錄式。
4、su命令啟動的bash。
不使用login選項的su。互動式,非登錄式。
[root@c7-server ~]# su root [root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell off
使用login選項的su。互動式,登錄式。
-, -l, --login:使shell為login shell。
[root@c7-server ~]# su - root Last login: Thu Dec 12 16:10:36 CST 2019 on pts/1 [root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell on
5、通過bash命令啟動的子shell。
一定為互動式,是否登錄式看是否帶-l選項。
[root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell off [root@c7-server ~]# exit exit [root@c7-server ~]# bash -l [root@c7-server ~]# echo $PS1; shopt login_shell [\u@\h \W]\$ login_shell on
6、命令組合時。
PS:這部分看不懂,來自駿馬金龍。
這種情況下,登錄式與互動式的情況繼承於父shell。
[root@c7-server ~]# (echo $BASH_SUBSHELL; echo $PS1; shopt login_shell) 1 [\u@\h \W]\$ login_shell on [root@c7-server ~]# su [root@c7-server ~]# (echo $BASH_SUBSHELL; echo $PS1; shopt login_shell) 1 [\u@\h \W]\$ login_shell off
7、使用ssh命令遠程執行命令。
非互動式,非登錄式。這種方式,在官網叫做遠程shell,Remote Shell Daemon。
[root@c7-server ~]# ssh localhost 'echo $PS1; shopt login_shell' root@localhost's password: login_shell off
8、運行shell腳本。
通過bash命令運行。非互動式,是否登錄式根據是否帶-l選項。
[root@c7-server ~]# cat alongdidi.sh #!/bin/bash echo $PS1 shopt login_shell [root@c7-server ~]# bash alongdidi.sh login_shell off [root@c7-server ~]# bash -l alongdidi.sh login_shell on
文件具備執行許可權後直接運行。非互動式,非登錄式。
[root@c7-server ~]# ./alongdidi.sh login_shell off
如果shebang具備了-l選項,那麼直接運行為非互動式、登錄式。
通過不帶-l選項的bash執行,依然是非互動式,非登錄式。
也就是說是否為登錄式,先看CLI中的bash是否帶-l選項,再看shebang是否帶-l選項。均為非互動式。
[root@c7-server ~]# cat alongdidi.sh #!/bin/bash -l echo $PS1 shopt login_shell [root@c7-server ~]# ./alongdidi.sh login_shell on [root@c7-server ~]# bash alongdidi.sh login_shell off
配置文件的載入方式
在bash中,載入配置文件的方式是通過讀取命令來實現的,它們是bash的內置命令source和“.”。
source filename [arguments]
. filename [arguments]
註意這裡是一個單獨的小數點,是一個bash內置命令。如果有arguments的話就作為位置參數。
本質上是讀取了文件併在當前的shell下執行文件中的命令。(不同於shell腳本的執行是需要創建子shell)
bash相關的配置文件,主要有這些:
/etc/profile ~/.bash_profile ~/.bashrc /etc/bashrc /etc/profile.d/*.sh
註意:這些配置文件,一般是都要求要具備可讀取的許可權才行(雖然對於root用戶可能無所謂)
位於用戶家目錄下的配置文件,為用戶私有的配置文件,只有對應的用戶才會載入,可實現針對用戶的定製。位於/etc/目錄下的配置文件,可以理解為全局配置文件,對所有用戶生效。
為了測試不同的bash啟動場景會載入哪些文件,我們在上述文件的末尾處加上一句echo語句。註意,我們是在文件的末尾加的echo語句,bash腳本的執行是按順序自上而下執行,位置很關鍵。
echo "echo '/etc/profile goes'" >>/etc/profile echo "echo '~/.bash_profile goes'" >>~/.bash_profile echo "echo '~/.bashrc goes'" >>~/.bashrc echo "echo '/etc/bashrc goes'" >>/etc/bashrc echo "echo '/etc/profile.d/test.sh goes'" >>/etc/profile.d/test.sh
1、只要是登錄式(無論是否互動式)的bash:先讀取/etc/profile,再依次搜索~/.bash_profile、~/.bash_login和~/.profile並僅載入第一個搜索到的且可讀的文件。在bash退出時,讀取~/.bash_logout。
在/etc/profile文件中,有讀取指令:
for i in /etc/profile.d/*.sh /etc/profile.d/sh.local ; do if [ -r "$i" ]; then if [ "${-#*i}" != "$-" ]; then . "$i" else . "$i" >/dev/null fi fi done
判斷/etc/profile.d/目錄下的*.sh和sh.local文件是否存在且可讀,如果是的話,則讀取。紅色粗體字的判斷,是判斷是否為互動式的bash,如果是的話在讀取配置文件時輸出STDOUT,否則不輸出。
在CentOS 6中沒有/etc/profile.d/sh.local文件,也沒有載入該文件的指令。在CentOS 7上,這個文件也只有一行註釋,以我蹩腳的英文水平,我猜應該是用來填寫一些環境變數,可用於覆蓋掉/etc/profile中的環境變數。
~]# cat /etc/profile.d/sh.local #Add any required envvar overrides to this file, it is sourced from /etc/profile
對於root用戶來說,由於存在~/.bash_profile文件且可讀(在我的測試環境中,普通用戶也具備有可讀的~/.bash_profile),因此~/.bash_login和~/.profile就被忽略了。
在~/.bash_profile中,有讀取指令:
PS:記得留意那段英文註釋。
# Get the aliases and functions if [ -f ~/.bashrc ]; then . ~/.bashrc fi
在~/.bashrc中,也有讀取指令:
# Source global definitions if [ -f /etc/bashrc ]; then . /etc/bashrc fi
在/etc/bashrc中,雖然有讀取指令,但是這部分指令是在非登錄式的情況下才執行:
if ! shopt -q login_shell ; then # We're not a login shell ... for i in /etc/profile.d/*.sh; do if [ -r "$i" ]; then if [ "$PS1" ]; then . "$i" else . "$i" >/dev/null fi fi done ... fi
圖示如下。按編號順序,首先載入第一條,載入完再載入第二條。
我們來測試之前所述的幾種bash啟動場景來看看。註意,必須得是登錄式的才行。因為我們這個小節討論的是登錄式的。
I. Xshell客戶端,偽終端登錄,互動式登錄式。
/etc/profile.d/test.sh goes /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes
之所以後載入的先顯示,那是因為我們的echo語句是添加在腳本的末尾,而讀取後續配置文件是在腳本的中間段。
II. ssh遠程登陸。互動式登錄式。
[root@c7-server ~]# ssh localhost root@localhost's password: Last login: Fri Dec 13 16:01:43 2019 from 192.168.152.1 /etc/profile.d/test.sh goes /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes
III. 啟動帶有登錄選項的子shell。
~]# bash -l /etc/profile.d/test.sh goes /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes
IV. 登錄式切換用戶。
~]# su -l Last login: Fri Dec 13 16:03:20 CST 2019 from localhost on pts/3 /etc/profile.d/test.sh goes /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes
V. 執行腳本時,帶有登錄選項。
[root@c7-server ~]# cat a.sh #!/bin/bash -l echo 'haha' [root@c7-server ~]# ./a.sh /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes haha [root@c7-server ~]# bash -l a.sh /etc/profile goes /etc/bashrc goes ~/.bashrc goes ~/.bash_profile goes haha
執行腳本屬於非互動式,而在非互動式場景下讀取/etc/profile.d/*.sh文件時,不會有輸出。(在/etc/profile文件中有定義,可以翻上去看)
. "$i" >/dev/null 2>&1
因此就不會輸出:
/etc/profile.d/test.sh goes
註意,僅僅只是不輸出而已,但是還是有載入了配置文件的,如果涉及到比如環境變數的設置等,還是會執行的。
2、互動式但非登錄式的bash:讀取~/.bashrc文件,不讀取/etc/profile、~/.bash_profile、~/.bash_login和~/.profile。
對應的場景為不帶登錄選項的子bash創建或者su用戶切換。
[root@c7-server ~]# bash /etc/profile.d/test.sh goes /etc/bashrc goes ~/.bashrc goes [root@c7-server ~]# su /etc/profile.d/test.sh goes /etc/bashrc goes ~/.bashrc goes
3、非互動式非登錄式的bash。
不載入任何的配置文件,嘗試展開環境變數BASH_ENV(這個變數一般是存儲了某些個配置文件的路徑),若有值則載入對應的配置文件,行為如下:
if [ -n "$BASH_ENV" ]; then . "$BASH_ENV"; fi
正常在編寫和執行bash腳本時,都不會刻意加上登錄選項,因此幾乎所有的bash腳本的執行都屬於這種情況。
存在一種非互動式非登錄式的bash特例,不使用這種配置文件載入方式。看下一個例子。
4、非互動式非登錄式的bash特例:遠程shell(Remote Shell Daemon)。
載入方式如下圖所示。
由於是非登錄式的shell,因此在讀取*.sh的時候不輸出。
[root@c7-server ~]# ssh localhost echo 'Remote Shell Daemon' root@localhost's password: /etc/bashrc goes ~/.bashrc goes Remote Shell Daemon