shell編程基礎(轉載)
Shell編程基礎
原作者 Leal;請參閱頁面底部的編者列表。
授權許可:
- 創作共享署名協議
- GNU 自由文檔許可證
註意:本文仍然在持續的修訂之中,且錯漏之處可能較多。如果能夠閱讀英語的話,可以考慮試試較為完善的 Wooledge BashGuide。這個站點除了教程之外,還有一些類似“bash 百科”的內容。Bash 官方手冊也是你的好朋友。
目錄
- 1 從第一行開始
- 2 變量
- 2.1 變量賦值和引用
- 2.2 變量算術
- 3 Shell裏的流程控制
- 3.1 if 語句
- 3.2 && 和 || 操作符
- 3.3 case 語句
- 3.4 select 循環語句
- 3.5 while/for 循環
- 4 Shell裏的一些特殊符號
- 4.1 引號
- 5 Here Document
- 6 Shell裏的函數
- 7 命令行參數
- 8 Shell腳本示例
- 8.1 一般編程步驟
- 8.2 二進制到十進制的轉換
- 8.3 文件循環移動
- 9 腳本調試
從第一行開始
我們可以使用任意一種文字編輯器,比如gedit、kedit、emacs、vi等來編寫shell腳本,它必須以如下行開始(必須放在文件的第一行):
#!/bin/bash
此行稱為 shebang(就是 sharp (#) + bang (!) 的意思),會指引操作系統使用接下來指定的程序運行此文件。此處 /bin/bash 執行我們的文件。
一些人使用 #!/bin/sh 來讓 sh 執行文件,按照習慣這表示任何支持 POSIX shell 命令語言的 sh程序。為了用上我們所喜歡的 bash 拓展語法功能,我們就不這麽用了。如果你使用別的腳本,例如 /bin/tcsh,照著樣子加個 #! 就行。
編輯結束並保存後,如果直接要執行該腳本,必須先使其具有可執行屬性:
chmod +x filename
此後在該腳本所在目錄下,輸入 ./filename<tt> 即可執行該腳本。
變量
Shell 變量默認全都是字符串。
變量賦值和引用
Shell 編程中,使用變量無需事先聲明。變量名的命名遵守正則表達式 <tt>[a-zA-Z_][a-zA-Z0-9_]+
需要給變量賦值時,可以這麽寫:
varname=值 [var2=val2 ....]
請註意這邊等號左右不能有空格。 要取用一個變量的值,只需在變量名前面加一個 $:
# assign
a="hello world" # 等號兩邊均不能有空格存在
# print
printf ‘%s\n‘ "A is: $a"
挑個自己喜歡的編輯器,輸入上述內容,並保存為文件 first,然後執行 chmod +x first 使其可執行,最後輸入 ./first 執行該腳本。其輸出結果如下:
A is: hello world
有時候變量名可能會和其它文字(匹配最長的符合變量名或特殊變量名要求的內容)混淆,比如:
num=2
echo "this is the $numnd" # 輸出 this is the — shell 嘗試尋找 $numnd 的值
echo "this is the ${num}nd" # 輸出 this is the 2nd — 修好了!
# 花括號可以隔開變量名,但是放歪了的話…
echo "this is the {$num}nd" # 輸出 this is the {2}nd — 切是切開了,但是…
變量算術
Shell 變量默認都是字符串。這也就是說,你嘗試這麽做,肯定沒用:
var=1
var=$var+1
echo $var # 輸出 1+1
我們可以用很多方法達成我們的目標。首先是好孩子的方法——C 風格表達式。
var=0
# bash 裏面可以用 (( )) 執行 C 風格的算術表達式。
# 如果你接下來還會讀 if 那一段的話,你還會知道這玩意的返回和 C 的非零真假判斷一致。
(( var += 1 )) # 這是一種,現在 var 是 1
(( var++ )) # 這也是一種自增,2
(( var = var * var )) # 怎麽來乘法了!var 現在是 4。
let ‘var = var / 3‘ # 還是只有 bash 才能用的拓展。除法是整數除法,向 0 舍入,1。
# 來一點不一定要 bash 的方法吧,畢竟 sh 還有很多種,總不能全報錯了吧。
# $(( )) 會展開成為這個 C 表達式求值的結果。以前 bash 有個 $[ ] 一樣,但是別去用。
echo $((var += 2)) # echo 出 3,var 現在是 3。
var=$((var-1)) # 展開成 var=2,var 現在是……還用說嗎,2。
以前還有人用 expr 之類的外部程序來,不過這屬於殺雞用牛刀。並且調用外部程序浪費時間性能差。
var=1
var=$(expr "$var" + 1) # expr 收到三個參數 ‘1‘ ‘+‘ ‘1‘,
# 按照 expr --help 裏面寫的方法運行
# 然後輸出替換掉 $() 這裏變成 var=2。
var=`expr "$var" + 1` # 前面一行的老寫法,千萬千萬不要學。
Shell裏的流程控制
if 語句
if 表達式如果條件命令組為真,則執行 then 後的部分。標準形式:
if
判斷命令,可以有很多個,真假取最後的返回值
then
如果前述為真做什麽
[ # 方括號代表可選,別真打進去了!
elif
可以再來個判斷,如果簽名為假繼續嘗試這裏
then
如果前述為真做什麽 ]
else
如果全都不行做什麽
fi # 結束,就是倒寫的 if 啦。
現實生活中一般判斷只有一個命令,所以你看到的一般是:
if ....; then # 你也可以寫成 if 之後換行,這樣就不用分號了。
....
fi
大多數情況下,可以使用測試命令來對條件進行測試,比如可以比較字符串、判斷文件是否存在及是否可讀等等……在 bash 中一般采用更好用的 ... 語法進行條件測試,而通用方法是 [ ... ]<tt>(相當於 <tt>test ...)。兩者都接納的常用測試語句有:
- -f "filename"
- 判斷是否是一個文件
- -x "/bin/ls"
- 判斷/bin/ls是否存在並有可執行權限
- -n "$var"
- 判斷 $var 變量是否有值
- "$a" == "$b"
- 判斷$a和$b是否相等
前者可以使用 help [[ 查詢用法,後者使用 help [ (bash) 或 man test 查詢。下面的語句用到了這個內容:
if [ "${SHELL}" == "/bin/bash" ]; then
echo "your login shell is the bash (bourne again shell)"
else
echo "your login shell is not bash but ${SHELL}"
fi
變量 $SHELL 包含有登錄shell的名稱,我們拿它和 /bin/bash 進行比較以判斷當前使用的shell是否為bash。你可能會問了,要是 bash 路徑不是這個呢?
&& 和 || 操作符
熟悉C語言的朋友可能會喜歡下面的表達式:
[ -f "/etc/shadow" ] && echo "This computer uses shadow passwords"
這裏的 && 就是一個快捷操作符,如果左邊的表達式為真(返回 0——“成功”)則執行右邊的語句,你也可以把它看作邏輯運算裏的與操作。上述腳本表示如果/etc/shadow文件存在,則打印“This computer uses shadow passwords”。
同樣shell編程中還可以用或操作 (||),例如:
#!/bin/bash
mailfolder=/var/spool/mail/james
[ -r "$mailfolder" ] || { echo "Can not read $mailfolder"; exit 1; }
echo "$mailfolder has mail from:"
grep "^From " $mailfolder
該腳本首先判斷mailfolder是否可讀,如果可讀則打印該文件中以"From"開頭的行。如果不可讀則或操作生效,打印錯誤信息後腳本退出。需要註意的是,這裏我們必須使用如下兩個命令:
{
echo "Can not read $mailfolder"; # 打印錯誤信息
exit 1; # 退出程序
}
我們使用花括號以組合命令的形式將兩個命令放到一起作為一個命令使用。即使不用與和或操作符,我們也可以用if表達式完成任何事情,但是使用與或操作符會更便利很多。
要註意 Shell 中的 && || 程序流操作符不表現任何優先級區別,完全是先看到誰就先處理誰的關系。
case 語句
case表達式可以用來匹配一個給定的字符串,而不是數字(可別和C語言裏的switch...case混淆)。
case ... in
...) do something here
;;
esac
file命令可以辨別出一個給定文件的文件類型,如:file lf.gz,其輸出結果為:
lf.gz: gzip compressed data, deflated, original filename,
last modified: Mon Aug 27 23:09:18 2001, os: Unix
我們利用這點寫了一個名為smartzip的腳本,該腳本可以自動解壓bzip2, gzip和zip 類型的壓縮文件:
#!/bin/bash
ftype="$(file "$1")"
case "$ftype" in
"$1: Zip archive"*)
unzip "$1" ;;
"$1: gzip compressed"*)
gunzip "$1" ;;
"$1: bzip2 compressed"*)
bunzip2 "$1" ;;
*)
echo "File $1 can not be uncompressed with smartzip";;
esac
你可能註意到上面使用了一個特殊變量 $1
,該變量包含有傳遞給該腳本的第一個參數值。也就是說,當我們運行:
smartzip articles.zip
$1
就是字符串 articles.zip。
select 循環語句
select 循環語句是bash的一種擴展應用,擅長於交互式場合。
用戶可以從一組不同的值中進行選擇:
pocket=()
select var in 跳跳糖 糖 很多糖 企鵝糖; do
echo "除了 $var 還要什麽嗎?"
if ((RANDOM%4 == 0)); then
echo "呀!時間不夠了,快上車!"
break # break 還是那個 break
fi
pocket+=("$var")
done
echo "你最後說的那個 $var 弄丟了……"
IFS=‘、‘
echo "現在口袋裏只有:${pocket[*]}。"
IFS=$‘ \t\n‘
下面是一個簡單的示例:
#!/bin/bash
echo "What is your favourite OS?"
select var in "Linux" "Gnu Hurd" "Free BSD" "Other"; do
break;
done
echo "You have selected $var"
該腳本的運行結果如下:
What is your favourite OS?
1) Linux
2) Gnu Hurd
3) Free BSD
4) Other
#? 1
You have selected Linux
while/for 循環
在shell中,可以使用如下循環:
while ...; do
....
done
只要測試表達式條件為真,則while循環將一直運行。關鍵字"break"用來跳出循環,而關鍵字”continue”則可以跳過一個循環的余下部分,直接跳到下一次循環中。
for循環會查看一個字符串列表(字符串用空格分隔),並將其賦給一個變量:
for var in ....; do
....
done
下面的示例會把A B C分別打印到屏幕上:
#!/bin/bash
for var in A B C ; do
echo "var is $var"
done
下面是一個實用的腳本showrpm,其功能是打印一些RPM包的統計信息:
#!/bin/bash
# list a content summary of a number of RPM packages
# USAGE: showrpm rpmfile1 rpmfile2 ...
# EXAMPLE: showrpm /cdrom/RedHat/RPMS/*.rpm
for rpmpackage in "$@"; do
if [ -r "$rpmpackage" ];then
echo "=============== $rpmpackage =============="
rpm -qi -p $rpmpackage
else
echo "ERROR: cannot read file $rpmpackage"
fi
done
這裏出現了第二個特殊變量$@,該變量包含有輸入的所有命令行參數值。如果你運行showrpm openssh.rpm w3m.rpm webgrep.rpm,那麽 "$@"(有引號) 就包含有 3 個字符串,即openssh.rpm, w3m.rpm和 webgrep.rpm。$*的意思是差不多的。但是只有一個字串。如果不加引號,帶空格的參數會被截斷。
Shell裏的一些特殊符號
引號
在向程序傳遞任何參數之前,程序會擴展通配符和變量。這裏所謂的擴展是指程序會把通配符(比如*)替換成適當的文件名,把變量替換成變量值。我們可以使用引號來防止這種擴展,先來看一個例子,假設在當前目錄下有兩個jpg文件:mail.jpg和tux.jpg。
#!/bin/bash
echo *.jpg # => mail.jpg tux.jpg
引號(單引號和雙引號)可以防止通配符*的擴展:
#!/bin/bash
echo "*.jpg" # => *.jpg
echo ‘*.jpg‘ # => *.jpg
其中單引號更嚴格一些,它可以防止任何變量擴展;而雙引號可以防止通配符擴展但允許變量擴展:
#!/bin/bash
echo $SHELL # => /bin/bash
echo "$SHELL" # => /bin/bash
echo ‘$SHELL‘ # => $SHELL
此外還有一種防止這種擴展的方法,即使用轉義字符——反斜杠\
:
echo \*.jpg # => *.jpg
echo \$SHELL # => $SHELL
Here Document
當要將幾行文字傳遞給一個命令時,用here document是一種不錯的方法。對每個腳本寫一段幫助性的文字是很有用的,此時如果使用here document就不必用echo函數一行行輸出。Here document以 << 開頭,後面接上一個字符串,這個字符串還必須出現在here document的末尾。下面是一個例子,在該例子中,我們對多個文件進行重命名,並且使用here document打印幫助:
#!/bin/bash
# we have less than 3 arguments. Print the help text:
if [ $# -lt 3 ] ;; then
cat << HELP
ren -- renames a number of files using sed regular expressions USAGE: ren ‘regexp‘ ‘replacement‘ files...
EXAMPLE: rename all *.HTM files in *.html:
ren ‘HTM$‘ ‘html‘ *.HTM
HELP #這裏HELP要頂格寫,前面不能有空格或者TAB制表符。如果cat一行寫成cat << -HELP,前邊可以帶TAB.
exit 0
fi
OLD="$1"
NEW="$2"
# The shift command removes one argument from the list of
# command line arguments.
shift
shift
# $@ contains now all the files:
for file in "$@"; do
if [ -f "$file" ] ; then
newfile=`echo "$file" | sed "s/${OLD}/${NEW}/g"`
if [ -f "$newfile" ]; then
echo "ERROR: $newfile exists already"
else
echo "renaming $file to $newfile ..."
mv "$file" "$newfile"
fi
fi
done
示例有點復雜,我們需要多花點時間來說明一番。第一個if表達式判斷輸入命令行參數是否小於3個 (特殊變量$# 表示包含參數的個數) 。如果輸入參數小於3個,則將幫助文字傳遞給cat命令,然後由cat命令將其打印在屏幕上。打印幫助文字後程序退出。如果輸入參數等於或大於3個,我們就將第一個參數賦值給變量OLD,第二個參數賦值給變量NEW。下一步,我們使用shift命令將第一個和第二個參數從參數列表中刪除,這樣原來的第三個參數就成為參數列表$*的第一個參數。然後我們開始循環,命令行參數列表被一個接一個地被賦值給變量$file。接著我們判斷該文件是否存在,如果存在則通過sed命令搜索和替換來產生新的文件名。然後將反短斜線內命令結果賦值給newfile。這樣我們就達到了目的:得到了舊文件名和新文件名。然後使用 mv命令進行重命名
Shell裏的函數
如果你寫過比較復雜的腳本,就會發現可能在幾個地方使用了相同的代碼,這時如果用上函數,會方便很多。函數的大致樣子如下:
# 別笑,bash 裏面函數名的確可以這樣……
# (POSIX sh 函數名倒是和變量名要求差不多)
我是一個函數() {
# 函數裏面 $1 $2 對應函數所接受到的第一、第二……個參數。
這裏有很多命令
}
函數沒有必要聲明。只要在執行之前出現定義就行
下面是一個名為xtitlebar的腳本,它可以改變終端窗口的名稱。這裏使用了一個名為help的函數,該函數在腳本中使用了兩次:
#!/bin/bash
help()
{
cat << HELP
xtitlebar -- change the name of an xterm, gnome-terminal or kde konsole
USAGE: xtitlebar [-h] "string_for_titelbar"
OPTIONS: -h help text
EXAMPLE: xtitlebar "cvs"
HELP
exit 0
}
# in case of error or if -h is given we call the function help:
if [[ $1 == ‘‘ || $1 == ‘-h‘ ]]; then
help
fi
# send the escape sequence to change the xterm titelbar:
echo -e "\033]0;$1\007"
#
在腳本中提供幫助是一種很好的編程習慣,可以方便其他用戶(和自己)使用和理解腳本。
命令行參數
我們已經見過 $*
和 $1
, $2 ... $9 等特殊變量,這些特殊變量包含了用戶從命令行輸入的參數。迄今為止,我們僅僅了解了一些簡單的命令行語法(比如一些強制性的參數和查看幫助的-h選項)。但是在編寫更復雜的程序時,您可能會發現您需要更多的自定義的選項。通常的慣例是在所有可選的參數之前加一個減號,後面再加上參數值 (比如文件名)。
有好多方法可以實現對輸入參數的分析,但是下面的使用case表達式的例子無疑是一個不錯的方法。
#!/bin/bash
help()
{
cat << HELP
This is a generic command line parser demo.
USAGE EXAMPLE: cmdparser -l hello -f -- -somefile1 somefile2
HELP
exit 0
}
while [ -n "$1" ]; do
case "$1" in
-h) help;shift 1;; # function help is called
-f) opt_f=1;shift 1;; # variable opt_f is set
-l) opt_l=$2;shift 2;; # -l takes an argument -> shift by 2
--) shift;break;; # end of options
-*) echo "error: no such option $1. -h for help";exit 1;;
*) break;;
esac
done
echo "opt_f is $opt_f"
echo "opt_l is $opt_l"
echo "first arg is $1"
echo "2nd arg is $2"
你可以這樣運行該腳本:
cmdparser -l hello -f -- -somefile1 somefile2
返回結果如下:
opt_f is 1 opt_l is hello first arg is -somefile1 2nd arg is somefile2
這個腳本是如何工作的呢?腳本首先在所有輸入命令行參數中進行循環,將輸入參數與case表達式進行比較,如果匹配則設置一個變量並且移除該參數。根據unix系統的慣例,首先輸入的應該是包含減號的參數。
Shell腳本示例
一般編程步驟
現在我們來討論編寫一個腳本的一般步驟。任何優秀的腳本都應該具有幫助和輸入參數。寫一個框架腳本(framework.sh),該腳本包含了大多數腳本需要的框架結構,是一個非常不錯的主意。這樣一來,當我們開始編寫新腳本時,可以先執行如下命令:
cp framework.sh myscript
然後再插入自己的函數。
讓我們來看看如下兩個示例。
二進制到十進制的轉換
腳本 b2d 將二進制數 (比如 1101) 轉換為相應的十進制數。這也是一個用expr命令進行數學運算的例子:
#!/bin/bash
# vim: set sw=4 ts=4 et:
help()
{
cat << HELP
b2d -- convert binary to decimal
USAGE: b2d [-h] binarynum
OPTIONS: -h help text
EXAMPLE: b2d 111010
will return 58
HELP
exit 0
}
error()
{
# print an error and exit
echo "$1"
exit 1
}
lastchar()
{
# return the last character of a string in $rval
if [ -z "$1" ]; then
# empty string
rval=""
return
fi
# wc puts some space behind the output this is why we need sed:
numofchar=`echo -n "$1" | sed ‘s/ //g‘ | wc -c `
#sed ‘s/ //g‘ 所有空白去掉 sed ‘s/ /\t/g‘ 所有空白用t代替
# now cut out the last char 抓取第numofchar個字節
rval=`echo -n "$1" | cut -b $numofchar`
}
chop()
{
# remove the last character in string and return it in $rval
if [ -z "$1" ]; then
# empty string
rval=""
return
fi
# wc puts some space behind the output this is why we need sed:
numofchar=`echo -n "$1" | wc -c | sed ‘s/ //g‘ `
if [ "$numofchar" = "1" ]; then
# only one char in string
rval=""
return
fi
numofcharminus1=`expr $numofchar "-" 1`
# now cut all but the last char:
rval=`echo -n "$1" | cut -b -$numofcharminus1`
#原來的 rval=`echo -n "$1" | cut -b 0-${numofcharminus1}`運行時出錯.
#原因是cut從1開始計數,應該是cut -b 1-${numofcharminus1}
}
while [ -n "$1" ]; do
case $1 in
-h) help;shift 1;; # function help is called
--) shift;break;; # end of options
-*) error "error: no such option $1. -h for help";;
*) break;;
esac
done
# The main program
sum=0
weight=1
# one arg must be given:
[ -z "$1" ] && help
binnum="$1"
binnumorig="$1"
while [ -n "$binnum" ]; do
lastchar "$binnum"
if [ "$rval" = "1" ]; then
sum=`expr "$weight" "+" "$sum"`
# $expr 10 + 10 20 expr提示是計算操作
fi
# remove the last position in $binnum
chop "$binnum"
binnum="$rval"
weight=`expr "$weight" "*" 2`
done
echo "binary $binnumorig is decimal $sum"
#
該腳本使用的算法是利用十進制和二進制數權值 (1,2,4,8,16,..),比如二進制"10"可以這樣轉換成十進制:
0 * 1 + 1 * 2 = 2
為了得到單個的二進制數我們是用了lastchar 函數。該函數使用wc –c計算字符個數,然後使用cut命令取出末尾一個字符。Chop函數的功能則是移除最後一個字符。
但是還記得前面怎麽說的嗎?進制轉換哪需要這麽麻煩:
#!/bin/bash
while read -p ‘input a binary...‘ num; do
if [[ $num == *[!01]* ]]; then
echo "含有 0 1 之外的字符"
fi
echo "$((0x$num))" # 在 num 頭上糊一個 0x 然後跑數學計算——就完事了!
printf "%d\n" "0x$num" # printf 也可以湊熱鬧啊
done
如果你喜歡自己算的話,其實也可以從左到右來(反正數學計算不要有事沒事玩 expr 啦):
#!/bin/bash
# 人人皆知的 Horner 規則
value=0
echo "寫一堆 1 0 完了回車"
while read -n 1 char; do
case $char in
(0|1) ;; # 好
(‘‘) break;; # 沒了
(*) echo "你說啥?"; break;;
esac
((value *= 2))
((value += char))
done
echo "$value"
文件循環移動
你可能有這樣的需求並一直都這麽做:將所有發出郵件保存到一個文件中。但是過了幾個月之後,這個文件可能會變得很大以至於該文件的訪問速度變慢;下面的腳本 rotatefile 可以解決這個問題。這個腳本可以重命名郵件保存文件(假設為outmail)為outmail.1,而原來的outmail.1就變成了 outmail.2 等等...
#!/bin/bash
# vim: set sw=4 ts=4 et:
ver="0.1"
help()
{
cat << HELP
rotatefile -- rotate the file name
USAGE: rotatefile [-h] filename
OPTIONS: -h help text
EXAMPLE: rotatefile out
This will e.g rename out.2 to out.3, out.1 to out.2, out to out.1
and create an empty out-file
version $ver
HELP
exit 0
}
if [[ $1 == ‘-h‘ || $1 == ‘‘ ]]; then
help
fi
filename=$1
# 我們先找到最大的數字再說。
max=0
while [ -f "$filename.$((++max))" ]; do
: # 什麽都不用做,我們已經順手用 ++max 自增了 max 了。
done
# 然後從最大的一路重命名下來。
for ((i=max; i>0; i--)); do
# 數字加個 1,好給前一個讓位子。
mv "$filename.$i" "$filename.$((i+1))"
done
# 最後我們點名要重命名的:
if [ -f "$filename" ]; then
mv "$filename" "$filename.1"
fi
# 重新創建一下。
: > "$filename"
。
腳本調試
最簡單的調試方法當然是使用echo命令。你可以在任何懷疑出錯的地方用echo打印變量值,這也是大部分shell程序員花費80%的時間用於調試的原因。Shell腳本的好處在於無需重新編譯,而插入一個echo命令也不需要多少時間。
shell也有一個真正的調試模式,如果腳本"strangescript"出錯,可以使用如下命令進行調試:
sh -x strangescript
上述命令會執行該腳本,同時顯示所有變量的值。
shell還有一個不執行腳本只檢查語法的模式,命令如下:
sh -n your_script
這個命令會返回所有語法錯誤。
shell編程基礎(轉載)