一、前言
在剛學習shell後不久便利用暑假去實習了一段時間,體驗了一下嵌入式BSP開發,剛開學不久,繼續開始艱苦的Linux學習之旅。
二、調試腳本
2.1 啓用shell腳本的跟蹤調試功能
2.1.1 使用選項-x,啓用shell腳本的跟蹤調試功能
$ bash -x script.sh
運行帶有-x選項的腳本可以打印出所執行的每一行命令以及當前狀態。
2.1.2 使用set -x和set +x對腳本進行部分調試
如下程序
1 #!/bin/bash
2
3 for i in {1..6};
4 do
5 set -x
6 echo $i
7 set +x
8 done
9 echo "Script executed"
輸出如下
+ echo 1
1
+ set +x
+ echo 2
2
+ set +x
+ echo 3
3
+ set +x
+ echo 4
4
+ set +x
+ echo 5
5
+ set +x
+ echo 6
6
+ set +x
Script executed
2.1.3 自定義調試信息
前面介紹的調試方法是Bash內建的。它們以固定的格式生成調試信息。但是在很多情況下,我們需要使用自定義的調試信息。可以通過定義 _DEBUG環境變量來啓用或禁止調試及生成特定形式的信息。
代碼如下:
1 #!/bin/bash
2
3 function DEBUG()
4 {
5 [ "$__DEBUG" == "on" ] && $@ || :
6 }
7
8 for i in {1..10}
9 do
10 DEBUG echo "I is $i"
11 done
可以將調試功能設置爲on來運行上面的腳本:
$ _DEBUG=on ./script.sh
我們在每一條需要打印調試信息的語句前加上DEBUG。如果沒有把 _DEBUG=on傳遞給腳本,那麼調試信息就不會打印出來。在Bash中,命令:告訴shell不要進行任何操作。
三、函數和參數
函數和別名乍一看很相似,不過兩者在行爲上還是略有不同。最大的差異在於函數參數可以在函數體中任意位置上使用,而別名只能將參數放在命令尾部。
3.1 函數的定義
函數的定義包括function命令、函數名、開/閉括號以及包含在一對花括號中的函數體。
函數可以這樣定義:
function fname()
{
statements;
}
或者
fname()
{
statements;
}
甚至是這樣(對於簡單的函數):
fname() { statement; }
3.2 函數的調用
只需使用函數名就可以調用函數:
$ fname ; #執行函數
3.3 函數的參數訪問
函數參數可以按位置訪問,$1是第一個參數,$2是第二個參數,以此類推:
fname arg1 arg2 ; #傳遞參數
以下是函數fname的定義。在函數fname中,包含了各種訪問函數參數的方法。
fname()
{
echo $1, $2; #訪問參數1和參數2
echo "$@"; #以列表的方式一次性打印所有參數
echo "$*"; #類似於$@,但是所有參數被視爲單個實體
return 0; #返回值
}
傳入腳本的參數可以通過下列形式訪問。
$0是腳本名稱。
$1是第一個參數。
$2是第二個參數。
$n是第n個參數。
$@被擴展成$1 $2 $3等。
$*被擴展成$1c$2c$3,其中c是IFS的第一個字符。
3.4 函數與別名的比較
3.4.1 下面的這個別名通過將ls的輸出傳入grep來顯示文件子集。別名的參數添加到命令的尾部,因此lsg txt就被擴展成了ls | grep txt:
$> alias lsg='ls | grep'
$> lsg txt
file1.txt
file2.txt
file3.txt
b.如果想獲得/sbin/ifconfig文件中設備對應的IP地址,可以嘗試這樣做:
$> alias wontWork='/sbin/ifconfig | grep'
$> wontWork eth0
eth0 Link encap:Ethernet HWaddr 00:11::22::33::44:55
c.grep命令找到的是字符串eth0,而不是IP地址。如果我們使用函數來實現的話,可以將
設備名作爲參數傳入ifconfig,不再交給grep:
$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> getIP eth0
inet addr:192.168.1.2 Bcast:192.168.255.255 Mask:255.255.0.0
3.5 導出函數
函數也能像環境變量一樣用export導出,如此一來,函數的作用域就可以擴展到子進程中:
export -f fname
$> function getIP() { /sbin/ifconfig $1 | grep 'inet '; }
$> echo "getIP eth0" >test.sh
$> sh test.sh
sh: getIP: No such file or directory
$> export -f getIP
$> sh test.sh
inet addr: 192.168.1.2 Bcast: 192.168.255.255 Mask:255.255.0.0
四、將一個命令的輸出發送給另一個命令
4.1 預備知識
命令輸入通常來自於stdin或參數。輸出可以發送給stdout或stderr。當我們組合多個命令時,通常將stdin用於輸入,stdout用於輸出。我們使用管道(pipe)連接每個過濾器,管道操作符是|。例如:
$ cmd1 | cmd2 | cmd3
這裏我們組合了3個命令。cmd1的輸出傳遞給cmd2,cmd2的輸出傳遞給cmd3,最終的輸出(來自cmd3)會出現在顯示器中或被導入某個文件。
4.2 實戰演練
4.2.1 組合兩個命令
$ ls | cat -n > out.txt
ls(列出當前目錄內容)的輸出被傳給cat -n,後者爲通過stdin所接收到的輸入內容加上行號,然後將輸出重定向到文件out.txt。
4.2.2 將命令序列的輸出賦給變量
cmd_output=$(ls | cat -n)
echo $cmd_output
或者
cmd_output=`ls | cat -n`
echo $cmd_output
4.3 利用子shell生成一個獨立的進程
子shell本身就是獨立的進程。可以使用()操作符來定義一個子shell。
$> pwd
/
$> (cd /bin; ls)
awk bash cat...
$> pwd
/
當命令在子shell中執行時,不會對當前shell造成任何影響;所有的改變僅限於該子shell內。例如,當用cd命令改變子shell的當前目錄時,這種變化不會反映到主shell環境中。
假設我們使用子shell或反引用的方法將命令的輸出保存到變量中,爲了保留輸出的空格和換行符(\n),必須使用雙引號。例如:
$ cat text.txt
1
2
3
$ out=$(cat text.txt)
$ echo $out
1 2 3 # 丟失了1、2、3中的\n
$ out="$(cat text.txt)"
$ echo $out
1
2
3
五、在不按下回車鍵的情況下讀入n 個字符
5.1 read命令
5.1.1 下面的語句從輸入中讀取n個字符並存入變量variable_name:
read -n number_of_chars variable_name
例如:
$ read -n 2 var
$ echo $var
5.1.2 用無回顯的方式讀取
read -s var
5.1.3 使用read顯示提示信息
read -p "Enter input:" var
5.1.4 在給定時限內讀取輸入
read -t timeout var
例如:
$ read -t 2 var
#在2秒內將鍵入的字符串讀入變量var
5.1.5 用特定的定界符作爲輸入行的結束
read -d delim_char var
例如:
$ read -d ":" var
hello: #var被設置爲hello
六、持續運行命令直至執行成功
有時候命令只有在滿足某些條件時才能夠成功執行。例如,在下載文件之前必須先創建該文件。這種情況下,你可能希望重複執行命令,直到成功爲止。
6.1 repeat函數
定義如下函數:
repeat()
{
while true
do
$@ && return
done
}
函數repeat()中包含了一個無限while循環,該循環執行以函數參數形式(通過$@訪問)傳入的命令。如果命令執行成功,則返回,進而退出循環。
6.2 一種更快的做法
在大多數現代系統中,true是作爲/bin中的一個二進制文件來實現的。這就意味着每執行一次之前提到的while循環,shell就不得不生成一個進程。爲了避免這種情況,可以使用shell的內建命令:,該命令的退出狀態總是爲0:
repeat() { while :; do $@ && return; done }
6.2 加入延時
假設你要用repeat()從Internet上下載一個暫時不可用的文件,不過這個文件只需要等一會就能下載。一種方法如下:
repeat wget -c http://www.example.com/software-0.1.tar.gz
如果採用這種形式,會產生很多發往www.example.com的流量,有可能會對服務器造成影響。(可能也會牽連到你自己;如果服務器認爲你是在向其發起攻擊,就會把你的IP地址列入黑名單。)要解決這個問題,我們可以修改函數,加入一段延時:
repeat() { while :; do $@ && return; sleep 30; done }
這樣命令每30秒纔會運行一次。
七、字段分隔符與迭代器
7.1 內部字段分隔符IFS
內部字段分隔符(Internal Field Separator,IFS)是shell腳本編程中的一個重要概念。在處理文本數據時,它的作用可不小。
作爲分隔符,IFS有其特殊用途。它是一個環境變量,其中保存了用於分隔的字符。它是當前shell環境使用的默認定界字符串。
考慮一種情形:我們需要迭代一個字符串或逗號分隔型數值(Comma Separated Value,CSV)中的單詞。如果是前者,可以使用IFS=" “;如果是後者,則使用IFS=”,"。
7.2 IFS實例
考慮CSV數據的情況:
data="name, gender,rollno,location"
我們可以使用IFS讀取變量中的每一個條目。
oldIFS=$IFS
IFS=, #IFS現在被設置爲,
for item in $data;
do
echo Item: $item
done
IFS=$oldIFS
輸出如下:
Item: name
Item: gender
Item: rollno
Item: location
IFS的默認值爲空白字符(換行符、製表符或者空格)。
當IFS被設置爲逗號時,shell將逗號視爲一個定界符,因此變量$item在每次迭代中讀取由逗號分隔的子串作爲變量值。
如果沒有把IFS設置成逗號,那麼上面的腳本會將全部數據作爲單個字符串打印出來。
7.3 IFS的另一種用法
在文件/etc/passwd中,每一行包含了由冒號分隔的多個條目。該文件中的每行都對應着某個用戶的相關屬性。
考慮這樣的輸入:root❌0:0:root:/root:/bin/bash。每行的最後一項指定了用戶的默認shell。
可以按照下面的方法巧妙地利用IFS打印出用戶以及他們默認的shell:
#!/bin/bash
#用途: 演示IFS的用法
line="root:x:0:0:root:/root:/bin/bash"
oldIFS=$IFS;
IFS=":"
count=0
for item in $line;
do
[ $count -eq 0 ] && user=$item;
[ $count -eq 6 ] && shell=$item;
let count++
done;
IFS=$oldIFS
echo $user's shell is $shell;
輸出爲:
root's shell is /bin/bash
7.3 多種類型的循環
7.3.1 面向列表的for循環
for var in list;
do
commands; #使用變量$var
done
list可以是一個字符串,也可以是一個值序列。
我們可以使用echo命令生成各種值序列:
echo {1..50}; #生成一個從1~50的數字序列
echo {a..z} {A..Z}; #生成大小寫字母序列
同樣,我們可以將這些方法結合起來對數據進行拼接(concatenate)。
下面的代碼中,變量i在每次迭代的過程裏都會保存一個範圍在a到z之間的字符:
for i in {a..z}; do actions; done;
7.3.2 迭代指定範圍的數字
for((i=0;i<10;i++))
{
commands; #使用變量$i
}
7.3.2 循環到條件滿足爲止
當條件爲真時,while循環繼續執行;當條件不爲真時,until循環繼續執行。
while condition
do
commands;
done
用true作爲循環條件能夠產生無限循環。
7.3.2 until循環
在Bash中還可以使用一個特殊的循環until。它會一直循環,直到給定的條件爲真。例如:
x=0;
until [ $x -eq 9 ]; #條件是[$x -eq 9 ]
do
let x++; echo $x;
done
七、比較與測試
7.1 if else
7.1.1 if條件
if condition;
then
commands;
fi
7.1.1 else if和else
if condition;
then
commands;
else if condition; then
commands;
else
commands;
fi
7.2 邏輯運算符
[ condition ] && action; # 如果condition爲真,則執行action
[ condition ] || action; # 如果condition爲假,則執行action
&&是邏輯與運算符,||是邏輯或運算符。編寫Bash腳本時,這是一個很有用的技巧。
7.3 算術比較
比較條件通常被放置在封閉的中括號內。一定要注意在[或]與操作數之間有一個空格。如果忘記了這個空格,腳本就會報錯。
[$var -eq 0 ] or [ $var -eq 0]
對變量或值進行算術條件測試:
[ $var -eq 0 ] #當$var等於0時,返回真
[ $var -ne 0 ] #當$var不爲0時,返回真
其他重要的操作符如下。
● -gt:大於。
● -lt:小於。
● -ge:大於或等於。
● -le:小於或等於。
-a是邏輯與操作符,-o是邏輯或操作符。可以按照下面的方法結合多個條件進行測試:
[ $var1 -ne 0 -a $var2 -gt 2 ] #使用邏輯與-a
[ $var1 -ne 0 -o $var2 -gt 2 ] #邏輯或-o
7.4 文件系統相關測試
我們可以使用不同的條件標誌測試各種文件系統相關的屬性。
● [ -f $file_var ]:如果給定的變量包含正常的文件路徑或文件名,則返回真。
● [ -x $var ]:如果給定的變量包含的文件可執行,則返回真。
● [ -d $var ]:如果給定的變量包含的是目錄,則返回真。
● [ -e $var ]:如果給定的變量包含的文件存在,則返回真。
● [ -c $var ]:如果給定的變量包含的是一個字符設備文件的路徑,則返回真。
● [ -b $var ]:如果給定的變量包含的是一個塊設備文件的路徑,則返回真。
● [ -w $var ]:如果給定的變量包含的文件可寫,則返回真。
● [ -r $var ]:如果給定的變量包含的文件可讀,則返回真。
● [ -L $var ]:如果給定的變量包含的是一個符號鏈接,則返回真。
例如:
fpath="/etc/passwd"
if [ -e $fpath ]; then
echo File exists;
else
echo Does not exist;
fi
7.5 字符串比較
進行字符串比較時,最好用雙中括號,因爲有時候採用單箇中括號會產生錯誤。
7.5.1 測試兩個字符串是否相同
● [[ $str1 = $str2 ]]:當str1等於str2時,返回真。也就是說,str1和str2包
含的文本是一模一樣的。
● [[ $str1 == $str2 ]]:這是檢查字符串是否相同的另一種寫法。
7.5.2 測試兩個字符串是否不同
● [[ $str1 != $str2 ]]:如果str1和str2不相同,則返回真。
7.5.3 找出在字母表中靠後的字符串
字符串是依據字符的ASCII值進行比較的。例如,A的值是0x41,a的值是0x61。因此,A小於a,AAa小於Aaa。
● [[ $str1 > $str2 ]]:如果str1的字母序比str2大,則返回真。
● [[ $str1 < $str2 ]]:如果str1的字母序比str2小,則返回真。
7.5.4 測試空串
● [[ -z $str1 ]]:如果str1爲空串,則返回真。
● [[ -n $str1 ]]:如果str1不爲空串,則返回真。
7.5.5 組合測試
使用邏輯運算符 && 和 || 能夠很容易地將多個條件組合起來:
if [[ -n $str1 ]] && [[ -z $str2 ]] ;
then
commands;
fi
例如:
str1="Not empty "
str2=""
if [[ -n $str1 ]] && [[ -z $str2 ]];
then
echo str1 is nonempty and str2 is empty string.
fi
輸出如下:
str1 is nonempty and str2 is empty string.
7.5 test命令
test命令可以用來測試條件。用test可以避免使用過多的括號,增強代碼的可讀性。之前講過的[]中的測試條件同樣可以用於test命令。例如:
if [ $var -eq 0 ]; then echo "True"; fi
也可以寫成:
if test $var -eq 0 ; then echo "True"; fi
八、使用配置文件定製bash
你在命令行中輸入的絕大部分命令都可以放置在一個特殊的文件中,留待登錄或啓動新的bash會話時執行。將函數定義、別名以及環境變量設置放置在這種特殊文件中,是一種定製shell的常用方法。
放入配置文件中的常見命令如下:
# 定義ls命令使用的顏色
LS_COLORS='no=00:di=01;46:ln=00;36:pi=40;33:so=00;35:bd=40;33;01'
export LS_COLORS
# 主提示符
PS1='Hello $USER'; export PS1
# 正常路徑之外的個人應用程序安裝目錄
PATH=$PATH:/opt/MySpecialApplication/bin; export PATH
# 常用命令的便捷方式
function lc () {/bin/ls -C $* ; }
8.1 用戶登錄時執行的文件
當用戶登錄shell時,會執行下列文件:
/etc/profile, $HOME/.profile, $HOME/.bash_login, $HOME/.bash_profile /
注意,如果你是通過圖形化登錄管理器登入的話,是不會執行/etc/profile、
~/.profile和$HOME/.bash_profile這3個文件的。這是因爲圖形化窗口管理器並不會啓動shell。當你打開終端窗口時纔會創建shell,但這個shell也不是登錄shell。
如果.bash_profile或.bash_login文件存在,則不會去讀取.profile文件
8.2 啓動交互式shell時執行
交互式shell(如X11終端會話)或ssh執行單條命令(如ssh 192.168.1.1 ls /tmp)時,會讀取並執行以下文件:
/etc/bash.bashrc $HOME/.bashrc
8.3 調用shell處理腳本文件時執行的
如果運行如下腳本:
$> cat myscript.sh
#!/bin/bash
echo "Running"
不會執行任何配置文件,除非定義了環境變量BASH_ENV:
$> export BASH_ENV=~/.bashrc
$> ./myscript.sh
使用ssh運行下列命令時:
ssh 192.168.1.100 ls /tmp
會啓動一個bash shell,讀取並執行/etc/bash.bashrc和$HOME/.bashrc,但不包括/etc/profile或.profile。
如果調用ssh登錄會話:
ssh 192.168.1.100
這會創建一個新的登錄bash shell,該shell會讀取並執行以下文件:
/etc/profile
/etc/bash.bashrc
$HOME/.profile or .bashrc_profile