簡潔的Bash編程技巧續篇 <轉>

上一篇文章發表後反響還是不錯的,讓我這個博客熱鬧了不少,以後我會陸陸續續將自己學到的一些新的技巧更新在這篇續篇中,當然也希望其它同學也能一起分享你們的技巧。續篇中有部分的內容已經偏離bash編程了,而是命令行下的技巧,題目我暫時不改,請見諒。

1. bash中alias的使用

alias其實是給常用的命令定一個別名,比如很多人會定義一下的一個別名:

alias ll='ls -l'

以後就可以使用ll,實際展開後執行的是ls -l。現在很多發行版都會帶幾個默認的別名,比如:

alias grep='grep --color=auto'  # 帶顏色顯示
alias ls='ls --color=auto' # 同上
alias rm='rm -i'  # 刪除文件需要確認

alias在某些方面確實提高了很大的效率,但是也是有隱患的,這點可以看我以前的一篇文章終端下肉眼看不見的東西。那麼如何不要展開alias,而是用本來的意思呢?答案是使用轉義:

\ls
\grep

在命令前面加一個反斜槓後就可以了。

這裏要插一段故事,前兩天我在shell腳本中定義了下面的一個alias,假設位於文件util.sh:

#!/bin/bash
...
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...

後面這串ssh選項是爲了去掉一些warning的信息,不提示輸入密碼等等。具體可以看ssh的文檔說明。我自己測試的時候好好的,當時我同事跑得時候卻依然有報Warning。我對比了下我們兩個人的用法:

sh util.sh  # 我的
./util.sh   # 他的

大家應該知道,直接./util.sh執行,shell會去找腳本第一行的shebang中給定的解釋器去執行改腳本,所以第二種用法相當於直接用bash來執行。那想必是bash/sh對alias是否默認展開這一點上是有區別的了。翻閱了下Bash的man手冊,發現可以通過設置expand_aliases選項來打開alias展開的功能,默認在非交互式Shell下是關閉的(什麼是交互式登錄Shell)。

修改下util.sh,打開這個選項就Ok了:

#!/bin/bash
...
# Expand aliases in script
shopt -s expand_aliases
alias ssh='ssh -o StrictHostKeyChecking=no -o LogLevel=quiet -o BatchMode=yes'
...

2. awk打印除第一列之外的其他列

awk用來截取輸入行中的某幾列很有用,當時如果要排除某幾列呢?

例如有如下的一個文件:

$ cat /tmp/test.txt
1 2 3 4 5
10 20 30 40 50

可以用下面的代碼解決(來源):

$ awk '{$1="";print $0}' /tmp/test.txt
 2 3 4 5
 20 30 40 50

但是前面多了一個空格,可以用cut命令稍微調整下:

$ awk '{$1="";print $0}' /tmp/test.txt | cut -c2-
2 3 4 5
20 30 40 50

3. 巧用bash的命令展開功能備份文件

假設要備份文件/your/path/to/file.list爲/your/path/to/file.list.20121106,常規的方法是:

cp /your/path/to/file.list /your/path/to/file.list.20121106

這樣重複寫上一長串的路徑,是不是很麻煩,這裏利用bash的展開特性可以這樣做:

cp /your/path/to/file.list{,.20121106}

/your/path/to/file.list{,.20121106}這一部分會展開爲/your/path/to/file.list /your/path/to/file.list.20121106,再將此傳給cp命令,就達到了與前面同樣的效果。(思路同ls *)。具體可以man bash中的Brace Expansion這一段。

4. 命令行下使用ctrl+x ctrl+e來編輯當前命令

這個技巧來自最牛B的 Linux Shell 命令系列連載(二)。使用方法是鍵入命令之後,再按ctrl+x ctrl+e可以打開一個編輯器來編輯命令,默認是使用emacs。你也可以通過在~/.bashrc中添加以下這一行,將編輯器換成vim:

export EDITOR='vim'

爲什麼推薦這一條呢?對於一般的命令(這裏指的是長度很短的命令)其實這個技巧沒什麼用處,我用方向鍵移一下就OK了,但是有時候(尤其是運維的一些命令)有些命令長度特別長,一堆參數,如果直接在命令行修改其實風險很高的(可以通過在命令的開頭加上一個#號來規避這個風險,Bash將當前的命令當成註釋不執行),而且方向鍵一個一個遷移非常不方便(當然有類似ctrl+x ctrl+e這種預設的快捷鍵來操作,可以看bind -p)。

像使用ctrl+x,ctrl+e打開vim來編輯命令在這種場景有兩種好處:
a. 可以方便的用熟悉的編輯器高效地修改命令;
b. 有一個確認的過程,無誤後,退出vim才執行命令。

不過我不是很推薦最牛B的 Linux Shell 命令 系列連載中的一些對歷史命令的技巧,雖然方便,但是風險很高,因爲沒有一個確認的過程,是執行將歷史命令調出就執行了。

5. 你知道sed的這個特性嗎?

假設一個文件的每一行爲一個路徑:

[Tue Nov 06 06:33 PM] [kodango@devops] ~ 
$ cat /tmp/test.txt
/home/kodango/hello
/home/kodango/hello/world
/home/kodango/good
/home/kodango/good/bye

現在要把/home/kodango/good替換成/home/kodango/bad,普通的作法是:

[Tue Nov 06 06:35 PM] [kodango@devops] ~ 
$ sed -n 's/\/home\/kodango\/good/\/home\/kodango\/bye/p' /tmp/test.txt 
/home/kodango/bye
/home/kodango/bye/bye

因爲路徑中的分隔符與sed的替換命令的分隔符都是'/',所以需要轉義,非常麻煩。幸運的是,sed可以更改分隔符,例如使用#:

[Tue Nov 06 06:34 PM] [kodango@devops] ~ 
$ sed -n 's#/home/kodango/good#/home/kodango/bad#p' /tmp/test.txt 
/home/kodango/bad
/home/kodango/bad/bye

這樣就清爽多了。

補充,如果是在地址對中使用,首個分隔符前面要加反斜槓:

$ sed -n '\#/home/kodango/#p' /tmp/test.txt 
/home/kodango/hello
/home/kodango/hello/world
/home/kodango/good
/home/kodango/good/bye

參見:Using different delimiters in sed

6. 合併連續重複的字符(即squeeze操作)

例如要合併一個字符串中連續的多個空格,假設字符串爲'print hello, world'。

第一種方法,使用sed命令,掃描整個字符串,替換2個以上的空格爲1格:

$ echo 'print  hello,   world  ' | sed -r 's/ {2,}/ /g'
print hello, world

第二種方法,使用tr命令的-s選項,專門就是爲了合併連續重複的字符:

$ echo 'print  hello,   world  ' | tr -s ' '
print hello, world

第三種方法,使用awk的域賦值來完成該目的:

$ echo 'print  hello,   world  ' | awk '$1=$1'
print hello, world

對已經存在的域例如$1,$2..進行賦值,會導致awk重新使用OFS輸出分隔符重組$0,關於這一點的詳細說明見sosodream同學的博文Awk裏的域賦值操作和部分源碼解析($1=$1,$0=$0,FS,OFS)

7. 將文本中某列相同的行輸出到不同的文件中

標題有點繞口,我們以實際例子來講解,假設我們有以下的一個文件:

1
2
3
4
5
6
$ cat /tmp/test.txt
a char
1 int
2 int
b char
abc string

我們的目標是將該文本中的行按第二列的值歸類,並且輸出到相應的文件中,文件名爲第二列的名稱。例如第2行、第3行會輸出到int.txt文件中,而第1行、第4行則輸出到char.txt,以此類推。

我沒有找到其它簡單的方法,只找到一種用awk來處理的方法:

[Wed Nov 07 07:31 PM] [kodango@devops] ~/workspace 
$ awk '{print $1 > $2 ".txt"}' /tmp/test.txt

我們來檢查結果:

[Wed Nov 07 07:34 PM] [kodango@devops] ~/workspace/output 
$ grep -nH . *
char.txt:1:a
char.txt:2:b
int.txt:1:1
int.txt:2:2
string.txt:1:abc

8. 用exec命令來完成重定向

以一個簡單的例子開始,現在需要一個腳本,它可以接受一個文件名作爲參數,然後按行讀取該文件的內容並打印到標準輸出。如果不指定文件名,則默認從標準輸入讀。首先按上面的功能需求寫出一個可以完成功能的腳本:

[Sat Nov 10 12:16 AM] [kodango@devops] ~/workspace 
$ cat test.sh 
 
filename=$1
 
if [ -z "$filename" ]; then
    while read line; do
        echo $line
    done
else
    while read line; do
        echo $line
    done < $filename
fi

如果換exec來實現重定向,可以把腳本寫得更優雅:

$ cat test1.sh 
 
filename=$1
 
if [ -n "$filename" ]; then
    exec 0< $filename
fi
 
while read line; do
    echo $line
done

這裏的關鍵在第5行代碼,exec命令不僅可以用於執行命令,還可以用於打開、關閉或者複製文件描述符,這裏就是利用exec將指定的文件名打開重定向到標準輸入。類似地可以用exec >$filename將文件重定向到標準輸出。我們可以在命令行上做一個試驗:

[Sat Nov 10 12:26 AM] [kodango@devops] ~ 
$ exec 3>&1                   # 首先將fd 3重定向到標準輸出,作爲標準輸出的一個備份
 
$ ls /proc/629/fd/{1,3} -l    # 現在fd 3和fd 1指向同一個設備文件
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /dev/pts/1
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/3 -> /dev/pts/1
 
$ exec >stdout               # 現在把標準輸出重定向到stdout這個文件中
 
$ ls /proc/629/fd/1 -l        # 如果你此刻在同一個終端下執行本命令是沒有返回的
 
$ ls /proc/629/fd/1 -l        # 現在重新打開一個終端看看,確實已經重定向到stdout這個文件
l-wx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /home/kodango/stdout
 
$ exec 1>&3                   # 現在重新把標準輸出重定向到之前備份的fd 3上
$ ls /proc/629/fd/{1,3} -l  # 現在屏幕可以看到輸出了,但是fd 3這個描述符還打開,需要關閉
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /dev/pts/1
lrwx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/3 -> /dev/pts/1
 
$ exec 3>&-                   # 關閉fd 3
$ ls /proc/629/fd/3 -l
ls: cannot access /proc/629/fd/3: No such file or directory
 
$ cat stdout                  # 檢查stdout文件,確實有之前被吃掉的輸出
l-wx------ 1 kodango kodango 64 Nov 10 00:26 /proc/629/fd/1 -> /home/kodango/stdout

關於I/O重定向的更詳細的說明,可以看I/O Redirection,這裏有很多例子講解了各種I/O重定向的用法,包括exec來改變重定向。

這一點在while read; do xxx; done < file內部仍需要從標準輸入讀取內容時非常有用,此時必須要將循環外部的重定向和內部的剝離開來。

9. 引號之間的區別

Shell中比較讓人抓狂的是各種引號的處理,其中,反引號(`cmd`)是最容易掌握的,它其實和$(cmd)是差不多的。

引號的作用有幾點,一個是爲了將多個因爲空格或者回車等分隔符隔開的字符串合在一起,避免被命令行解析分開,例如"one two three"就是一整個字符串,而不是像one two three會被解析成三個單獨的字符串;另外一方面,引號可以讓一些特殊符號保持原義。

其中,單引號的處理是比較簡單的,被單引號包括的所有字符都保留原有的意思,例如'$a'不會被展開, '`cmd`'也不會執行命令;而雙引號,則相對比較鬆,在雙引號中,以下幾個字符$, `, \依然有其特殊的含義,比如$可以用於變量展開, 反引號`可以執行命令,反斜槓\可以用於轉義。但是,在雙引號包圍的字符串裏,反斜槓的轉義也是有限的,它只能轉義$, `, ", \或者newline(回車)這幾個字符,後面如果跟着的不是這幾個字符,只不會被黑底,反斜槓會被保留,例如:

$ echo "\$,\",\`,\',\t"
$,",`,\',\t

雙引號內可以直接包含單引號,而且單引號也沒有如上據說的特殊含義,所以像"var='$var'"中$var還是會被展開的,而不要以爲簡單地認爲在單引號內部就不會展開了。如果雙引號內部包含感嘆號!就比較頭痛了,感嘆號是用於命令行歷史展開,例如!!展開爲上一次執行的命令。你可以試試雙引號中包含!:

[Sat Nov 10 07:39 PM] [kodango@devops] ~ 
$ echo "!"
-bash: !: event not found
$ echo "\!"
\!

可見,即使你用反斜槓也沒辦法轉義,除非你把歷史展開功能關閉(在腳本里面是沒有問題的,默認是關閉的)。

[Sat Nov 10 07:50 PM] [kodango@devops] ~ 
$ set +o histexpand 
 
[Sat Nov 10 07:50 PM] [kodango@devops] ~ 
$ echo "!"
!

當然,感嘆號可以用在單引號裏面。

[Sat Nov 10 07:50 PM] [kodango@devops] ~ 
$ set -o histexpand
 
[Sat Nov 10 07:51 PM] [kodango@devops] ~ 
$ echo '!'
!

到此爲止,其實雙引號和單引號的區別已經說得差不多了。不過還可以再說幾個特殊的用法,前面說過可以在雙引號內部使用單引號,你有想過在單引號裏面使用單引號嗎?

$ echo '\''
>

是不是發現不能用,因爲單引號中反斜槓是沒有轉義的效果的,任何字符都沒有特殊的含義。那就沒有辦法了嗎?方法總是有的,可以在第一個單引號前面加個$符號:

$ echo $'\''
'

這又是另外一種神奇的用法了,我放到下一點講。

關於這一點的內容,具體可以看以下兩份資料:
a. http://www.gnu.org/software/bash/manual/html_node/Quoting.html#Quoting
b. http://tldp.org/LDP/abs/html/quoting.html

10. 特殊用法$'string'

前面一點中已經介紹了 $'string'這種用法,比如 $'\'',之所以可以這樣用,通俗地講,就是在這種語法裏一些轉義字符串是被認可的,事實上有效地的轉義底字符串列表可以看這裏,例如\b,\',\n,\f,\nnn,\xhh等等,是不是很熟悉。

$'string'的這個特性,其實爲我們提供了一種很有用的技巧:

$ echo $'\x41'
A

他可以將ASCII對應的字符賦值給某個變量或者輸出。

11. 用雙引號比不用更加安全

雙引號除了前面第10點講到的去除特殊涵義的作用外,還可以避免字符串被分隔解析,例如:

$ echo `ls -l`
total 4.0K -rw-r--r-- 1 kodango kodango 4 Nov 10 20:09 1 -rw-r--r-- 1 kodango kodango 0 Nov 10 20:09 2
$ echo "`ls -l`"
total 4.0K
-rw-r--r-- 1 kodango kodango 4 Nov 10 20:09 1
-rw-r--r-- 1 kodango kodango 0 Nov 10 20:09 2

前者沒有加雙引號,ls -l輸出行之間的回車就被吃掉了。原因是,當ls -l返回的結果傳遞給echo之前,會先被shell進行參數解析,而shell是用IFS定義的分隔符來分隔字符串的,一般包括\n,所以它把解析後的結果再傳遞給echo,就成爲echo "line 1...." "line 2..."這種形式了,結果就像上面一樣。

而用雙引號包括起來可以避開字符串被拆開解析,因爲shell認爲它是一個單獨的字符串。所以一般情況下,多用引號包括變量是好的,"$var"比$var更安全。

12. 顯示一個文件並且在每行開頭添加行號

有兩種做法,第一種藉助cat和nl命令來完成:

$ cat test.txt | nl
     1	line 1 
     2	line 2

另外一種做法是用sed命令:

$ sed '=' test.txt | sed 'N;s/\n/\t/'

13. 命令行鍵映射,編輯模式

命令行下默認是emacs的keymap,對於不會emacs的人來說真是災難,完全不知道各種ctrl+x鍵是做什麼的,可以通過執行以下命令切換到vi模式:

set -o vi

在這種模式下,就可以用熟悉的vi命令了,默認輸入命令的是在insert模式,按ESC鍵可以切換到命令模式,這點和vim是一樣的,熟悉vim的人很快就可以上手。

之前介紹過命令行下使用ctrl+x ctrl+e來編輯當前命令,而在vi模式下,可以在命令模式下直接鍵入v。還有,如果不想執行當前輸入的命令,可以在命令模式下按#號鍵,它會在當前命令當作註釋而不執行(在命令開頭添加#號)。

更多vi模式的介紹可以參見Working Productively in Bash's Vi Command Line Editing Mode,作者還給了一份Vi Editing Mode Cheat Sheet留作參考。

如果你想將vi模式作爲默認的編輯模式,可以將set -o vi寫入到~/.bashrc文件中。當然,在運維的線上生產環境這樣做是不合適的,你只能手動輸入切換了。不過,如果你選擇的ssh管理客戶端比較高級的話,應該可以避免每次手動輸出。比如我用的是xshell,可以通過設置Login script在每次登錄的時候自動執行命令,或者將命令添加到quick command set,然後調出quick command set toolbar,手動點擊按鈕切換。這兩種方法結合起來就幾乎同寫入到~/.bashrc一樣的方便了。

14. 分別輸出兩個文件相同的行和不同的行

假設我們有以下兩個文件:

$ echo test{,2}.txt;paste test{,2}.txt
test.txt test2.txt
line 1 	line 11
line 2	line 2

如果要輸出兩個文件之間相同的行,只有test.txt擁有的行以及只有test2.txt擁有的行,怎麼做?首先可以使用grep -f:

$ grep -f test{,2}.txt
line 2
$ grep -vf test{,2}.txt
line 11
$ grep -vf test{2,}.txt
line 1

還有一種選擇是comm命令,這個命令就是專門用於比較文件的: comm - compare two sorted files line by line
使用方法也很簡單,comm比較兩個排序好的文件返回的結果有三列,第一列是只有在文件A中有的行,第二列是只有在文件B中有的行,第三列則是兩個文件共有的行:

$ comm test.txt test2.txt                
line 1 
	line 11
		line 2

要得到最初要求的結果,則只需要取相應的列就可以了。comm命令非常人性化地考慮到這個需求:

$ comm test.txt test2.txt -1 -2
line 2
$ comm test.txt test2.txt -2 -3
line 1 
$ comm test.txt test2.txt -1 -3
line 11

其中,=1, -2與-3這個參數分別表示不輸出第1、2或者3列。

15. 獲取被source的腳本的名稱

一般的情況下,腳本的名稱可以通過$0獲取,但是這在被source導入的腳本中卻不成立。假設A腳本中source了B腳本,那麼它是把B的代碼導入到A的環境中直接執行的,因此A和B的代碼其實是在同一個執行環境下分不開的,B的代碼中訪問到的$0,甚至$1, $2等位置參數都是與A腳本是一致的。

因此$0並非是被導入的腳本的名稱,實際上,Bash將被source的腳本名稱保存在一個叫BASH_SOURCE的數組中,該數組的第一個元素正是當前被source的腳本的名稱。該變量與我在bash獲取當前函數名中介紹的FUNCNAME是類似的,當一個腳本被source時,它的名稱就被壓入到這個數組的第一個位置上,舉個實際的例子,假設有三個腳本a.sh,b.sh,c.sh,它們的內容如下所示:

$ cat a.sh 
. ./b.sh
echo "\$0=$0"
echo "\${BASH_SOURCE[0]}=${BASH_SOURCE[0]}"
echo "\$BASH_SOURCE=(${BASH_SOURCE[@]})"
 
$ cat b.sh 
. ./c.sh
. ./c.sh
echo "\$0=$0"
echo "\${BASH_SOURCE[0]}=${BASH_SOURCE[0]}"
echo "\$BASH_SOURCE=(${BASH_SOURCE[@]})"
 
$ cat c.sh 
$ cat c.sh 
echo "\$0=$0"
echo "\${BASH_SOURCE[0]}=${BASH_SOURCE[0]}"
echo "\$BASH_SOURCE=(${BASH_SOURCE[@]})"

現在執行a.sh這個腳本,實際的輸出是(爲了方便理解,我在實際的輸出中加了一些註釋和空行):

$ bash a.sh
# c.sh的輸出
$0=a.sh
${BASH_SOURCE[0]}=./c.sh
$BASH_SOURCE=(./c.sh ./b.sh a.sh)
 
# b.sh的輸出
$0=a.sh
${BASH_SOURCE[0]}=./b.sh
$BASH_SOURCE=(./b.sh a.sh)
 
# a.sh的輸出
$0=a.sh
${BASH_SOURCE[0]}=a.sh
$BASH_SOURCE=(a.sh)

此外,我們還可以利用BASH_SOURCE的值,在腳本中判斷是被直接執行還是被導入:

if [ -n "$BASH_SOURCE" -a "$BASH_SOURCE" != "$0" ]
then
    echo "be sourced by other scripts"
else
    echo "be run in shell"
fi

16. ${}參數展開

我們知道${parameter}是展開變量parameter這個值,在上一篇簡潔的bash編程技巧中也曾經介紹過${parameter:-word}這種用法,用於給變量賦一個默認值。

事實上除此之外,參數展開還有許多形式,在此之前,首先要說明一下變量的幾種值的形式:
1. unset: 變量未設置,即變量從未聲明,或者被unset命令重置;
2. null: 變量聲明但未被賦值(var=)或者被賦值成空(var="");
3. not null: 變量被賦值;

unset和null在參數展開的時候還是有很大的區別的,以下是參數展開的各種形式:
1. ${parameter:-word}:假如parameter爲unset或者null,則展開後返回word的值;
2. ${parameter-word}:假如parameter爲unset時,則展開後返回word的值;
3. ${parameter:=word}:假如parameter爲unset或者null,將word賦值給parameter;
4. ${parameter=word}:假如parameter爲unset,將word賦值給parameter;
5. ${parameter:?word}:假如parameter爲unset或者null,則將word作爲錯誤輸出到標準輸出;
6. ${parameter?word}:假如parameter爲unset,則將word作爲錯誤輸出到標準輸出;
7. ${parameter:+word}:假如parameter爲unset或者null,則不做展開,返回爲空;(剛好與:-相反)
8. ${parameter:word}:假如parameter爲unset,則不做展開,返回爲空;(剛好與-相反)

上面其實準確地應該是分成2組,一組帶:,一組不帶:,不帶:的這組更加嚴格,只檢查unset這種情況。以:+爲例子, unset的情況均無返回:

$ unset var && echo ${var:+hello}
 
$ unset var && echo ${var+hello}

當var爲空時:

$ var= && echo "${var:+hello}"
 
$ var= && echo "${var+hello}"
hello

當var爲非空時:

$ var=1 && echo "${var:+hello}"
hello
$ var=1 && echo "${var+hello}"
hello

關於參數展開的具體內容可以參考Bash Man手冊中的Parameter Expansion這一節。

17. 冒號的多種使用場景

冒號是一個比較奇怪的符號,它的用途有很多,這裏介紹幾種常用的:

1. 內置命令null command:nop,表示什麼都不做,也可以被當作true值使用;

$ :
$ echo $?    # return 0

它也可以在循環中當作true值,例如:

while :; do   # 等價於 while true; do
    take-some-action
done
 
if condition
then :
else 
    take-some-action
fi

2. 佔位符

冒號可以在很多場景下充當佔位符,例如之前介紹的${parameter=var},如果直接執行會報錯,表示找不到命令;這時可以借用冒號來完成賦值:

: ${parameter=var}

同樣地,可以來判斷變量是否賦值:

: ${parameter1?} ${parameter2?}

更多其它用法可以看ABS的Special Characters這一節。

18. 擴展的括號展開功能

這個功能不能說雞肋,也可以瞭解下:

$ echo {0..3}
0 1 2 3
$ echo {z..a}
z y x w v u t s r q p o n m l k j i h g f e d c b a
$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z

19. [[]]比[]作條件測試更安全

[[]]的功能比[]更加多,使用起來也更加安全。

1. 首先[[]]內部不會發生文件名展開和單詞分隔。

例如:

$ touch hello\ world
$ [[ -f $file ]] && echo yes
yes
$ [ -f $file ] && echo yes
-bash: [: hello: binary operator expected

2. 進制之間自動轉化

當一個十進制與八進制做比較時,會自動計算兩個數的值,統一後做比較:

$ o=017
$ h=0x0f
$ [[ $o -eq $h ]] && echo yes
yes
$ [[ $o -eq 15 ]] && echo yes
yes

3. [[]]支持&&,||等運算符

$ a=1;b=3
$ [[ $a > 0 && $b < 4 ]] && echo yes
yes

20. 獲取Bash腳本的最後一個參數

我們都知道可以用$0,$1等來獲取傳遞給腳本或者函數的參數,也可以用$*或者$@獲取所有的參數,但是如果我只想要獲取最後一個參數呢?

首先,你可能想到用遍歷地方法(這裏爲了方便,我們使用set命令來設置位置參數):

$ set -- arg1 arg2 arg3
$ for i in $@; do :; done
$ echo $i
arg3

這裏的循環什麼事情都沒做,我用冒號(:)完成這個任務;循環結束後, $i就是保存着最後一個參數的值。

下面是兩種更加簡單的方法的:

$ echo ${@: -1}
$ echo ${!#}

上面的第一種方法事實上就是Parameter Expansion中的${parameter:offset:length}這種形式,只不過offset爲-1表示最後一個元素,忽略length表明是從offset開始往後直到最後一個元素,即只取最後一個元素。這裏要注意的一點是,在冒號和短橫之間的空格不能少,否則就變成15. ${}參數展開中介紹的${parameter:-var}這種用法了。

而第二種方法則是indirect referencing的一種表現,#這個特殊的變量存放參數的個數,!#則是對最後一個變量的引用。

21. Bash中的引用(indirect referencing)

有沒有想法在Bash中也可以達到C++引用的效果?你可能不知道,但是你可能曾經有這種需求,我就有過:

有時候,我想要一個變量存放另外一個變量的名稱,然後在後面我想通過這個變量的名稱引用它的值

例子是這樣的:

$ a=b
$ b=1
$ echo $a
b
$ eval "echo \$$a"
1

但是利用indirect referencing的用法,你可以這樣獲取b的值:

$ echo ${!a}
1
$ b=2
$ echo ${!a}
2

很奇怪的一種用法,關於indirect referencing你可以查看這裏或者這裏

22. 未完待續

 

http://kodango.me/simple-bash-programming-skills-2 

發佈了108 篇原創文章 · 獲贊 3 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章