Linux_Shell腳本學習第一章-小試牛刀(下)

一、前言

在剛學習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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章