一、什么是Shell腳本
示例
看個(gè)例子吧:
#!/bin/sh
cd ~
mkdir shell_tut
cd shell_tut
for ((i=0; i<10; i++)); do
touch test_$i.txt
done
示例解釋
- 第1行:指定腳本解釋器,這里是用/bin/sh做解釋器的
- 第2行:切換到當(dāng)前用戶的home目錄
- 第3行:創(chuàng)建一個(gè)目錄shell_tut
- 第4行:切換到shell_tut目錄
- 第5行:循環(huán)條件,一共循環(huán)10次
- 第6行:創(chuàng)建一個(gè)test_0…9.txt文件
- 第7行:循環(huán)體結(jié)束
mkdir, touch都是系統(tǒng)自帶的程序,一般在/bin或者/usr/bin目錄下。for, do, done是sh腳本語言的關(guān)鍵字。
shell和shell腳本的概念
shell是指一種應(yīng)用程序,這個(gè)應(yīng)用程序提供了一個(gè)界面,用戶通過這個(gè)界面訪問操作系統(tǒng)內(nèi)核的服務(wù)。Ken Thompson的sh是第一種Unix Shell,Windows Explorer是一個(gè)典型的圖形界面Shell。
shell腳本(shell script),是一種為shell編寫的腳本程序。業(yè)界所說的shell通常都是指shell腳本,但讀者朋友要知道,shell和shell script是兩個(gè)不同的概念。由于習(xí)慣的原因,簡(jiǎn)潔起見,本文出現(xiàn)的“shell編程”都是指shell腳本編程,不是指開發(fā)shell自身(如Windows Explorer擴(kuò)展開發(fā))。
二、環(huán)境
shell編程跟java、php編程一樣,只要有一個(gè)能編寫代碼的文本編輯器和一個(gè)能解釋執(zhí)行的腳本解釋器就可以了。
操作系統(tǒng)
當(dāng)前主流的操作系統(tǒng)都支持shell編程,本文檔所述的shell編程是指Linux下的shell,講的基本都是POSIX標(biāo)準(zhǔn)下的功能,所以,也適用于Unix及BSD(如Mac OS)。
Linux
Linux默認(rèn)安裝就帶了shell解釋器。
Mac OS
Mac OS不僅帶了sh、bash這兩個(gè)最基礎(chǔ)的解釋器,還內(nèi)置了ksh、csh、zsh等不常用的解釋器。
Windows上的模擬器
windows出廠時(shí)沒有內(nèi)置shell解釋器,需要自行安裝,為了同時(shí)能用grep, awk, curl等工具,最好裝一個(gè)cygwin或者mingw來模擬linux環(huán)境。
腳本解釋器
sh
即Bourne shell,POSIX(Portable Operating System Interface)標(biāo)準(zhǔn)的shell解釋器,它的二進(jìn)制文件路徑通常是/bin/sh,由Bell Labs開發(fā)。
本文講的是sh,如果你使用其它語言用作shell編程,請(qǐng)自行參考相應(yīng)語言的文檔。
bash
Bash是Bourne shell的替代品,屬GNU Project,二進(jìn)制文件路徑通常是/bin/bash。業(yè)界通常混用bash、sh、和shell,比如你會(huì)經(jīng)常在招聘運(yùn)維工程師的文案中見到:熟悉Linux Bash編程,精通Shell編程。
在CentOS里,/bin/sh是一個(gè)指向/bin/bash的符號(hào)鏈接:
[root@centosraw ~]# ls -l /bin/*sh
-rwxr-xr-x. 1 root root 903272 Feb 22 05:09 /bin/bash
-rwxr-xr-x. 1 root root 106216 Oct 17 2012 /bin/dash
lrwxrwxrwx. 1 root root 4 Mar 22 10:22 /bin/sh -> bash
但在Mac OS上不是,/bin/sh和/bin/bash是兩個(gè)不同的文件,盡管它們的大小只相差100字節(jié)左右:
iMac:~ wuxiao$ ls -l /bin/*sh
-r-xr-xr-x 1 root wheel 1371648 6 Nov 16:52 /bin/bash
-rwxr-xr-x 2 root wheel 772992 6 Nov 16:52 /bin/csh
-r-xr-xr-x 1 root wheel 2180736 6 Nov 16:52 /bin/ksh
-r-xr-xr-x 1 root wheel 1371712 6 Nov 16:52 /bin/sh
-rwxr-xr-x 2 root wheel 772992 6 Nov 16:52 /bin/tcsh
-rwxr-xr-x 1 root wheel 1103984 6 Nov 16:52 /bin/zsh
高級(jí)編程語言
理論上講,只要一門語言提供了解釋器(而不僅是編譯器),這門語言就可以勝任腳本編程,常見的解釋型語言都是可以用作腳本編程的,如:Perl、Tcl、Python、PHP、Ruby。Perl是最老牌的腳本編程語言了,Python這些年也成了一些linux發(fā)行版的預(yù)置解釋器。
編譯型語言,只要有解釋器,也可以用作腳本編程,如C shell是內(nèi)置的(/bin/csh),Java有第三方解釋器Jshell,Ada有收費(fèi)的解釋器AdaScript。
三、如何選擇shell編程語言
簡(jiǎn)單 vs 高級(jí)
如果你覺得自己熟悉的語言(如Java、C)寫shell腳本實(shí)在太啰嗦,你只是想做一些備份文件、安裝軟件、下載數(shù)據(jù)之類的事情,學(xué)著使用sh,bash會(huì)是一個(gè)好主意。
shell只定義了一個(gè)非常簡(jiǎn)單的編程語言,所以,如果你的腳本程序復(fù)雜度較高,或者要操作的數(shù)據(jù)結(jié)構(gòu)比較復(fù)雜,那么還是應(yīng)該使用Python、Perl這樣的腳本語言,或者是你本來就已經(jīng)很擅長的高級(jí)語言。因?yàn)閟h和bash在這方面很弱,比如說:
- 它的函數(shù)只能返回字串,無法返回?cái)?shù)組
- 它不支持面向?qū)ο螅銦o法實(shí)現(xiàn)一些優(yōu)雅的設(shè)計(jì)模式
- 它是解釋型的,一邊解釋一邊執(zhí)行,連PHP那種預(yù)編譯都不是,如果你的腳本包含錯(cuò)誤(例如調(diào)用了不存在的函數(shù)),只要沒執(zhí)行到這一行,就不會(huì)報(bào)錯(cuò)
環(huán)境兼容性
如果你的腳本是提供給別的用戶使用,使用sh或者bash,你的腳本將具有最好的環(huán)境兼容性,perl很早就是linux標(biāo)配了,python這些年也成了一些linux發(fā)行版的標(biāo)配,至于mac os,它默認(rèn)安裝了perl、python、ruby、php、java等主流編程語言。
四、第一個(gè)shell腳本
1. 編寫
打開文本編輯器,新建一個(gè)文件,擴(kuò)展名為sh(sh代表shell),擴(kuò)展名并不影響腳本執(zhí)行,見名知意就好,如果你用php寫shell 腳本,擴(kuò)展名就用php好了。
輸入一些代碼,第一行一般是這樣:
#!/bin/bash
#!/usr/bin/php
“#!”是一個(gè)約定的標(biāo)記,它告訴系統(tǒng)這個(gè)腳本需要什么解釋器來執(zhí)行。
2.運(yùn)行
運(yùn)行Shell腳本有兩種方法:
作為可執(zhí)行程序
chmod +x test.sh
./test.sh
注意,一定要寫成./test.sh,而不是test.sh,運(yùn)行其它二進(jìn)制的程序也一樣,直接寫test.sh,linux系統(tǒng)會(huì)去PATH里尋找有沒有叫test.sh的,而只有/bin, /sbin, /usr/bin,/usr/sbin等在PATH里,你的當(dāng)前目錄通常不在PATH里,所以寫成test.sh是會(huì)找不到命令的,要用./test.sh告訴系統(tǒng)說,就在當(dāng)前目錄找。
通過這種方式運(yùn)行bash腳本,第一行一定要寫對(duì),好讓系統(tǒng)查找到正確的解釋器。
這里的"系統(tǒng)",其實(shí)就是shell這個(gè)應(yīng)用程序(想象一下Windows Explorer),但我故意寫成系統(tǒng),是方便理解,既然這個(gè)系統(tǒng)就是指shell,那么一個(gè)使用/bin/sh作為解釋器的腳本是不是可以省去第一行呢?是的。
作為解釋器參數(shù)
這種運(yùn)行方式是,直接運(yùn)行解釋器,其參數(shù)就是shell腳本的文件名,如:
/bin/sh test.sh
/bin/php test.php
這種方式運(yùn)行的腳本,不需要在第一行指定解釋器信息,寫了也沒用。
五、shell變量
Shell編程中,可以使用變量,這充分體現(xiàn)了他的靈活性。對(duì)Shell來講,所有的變量的取值都是一個(gè)字符串。Shell腳本中主要有以下幾種變量:
- 系統(tǒng)變量
- 環(huán)境變量
- 用戶變量
1. 系統(tǒng)變量
以下是一些常用到的Shell系統(tǒng)變量以及其含義:
$#
:獲取命令行參數(shù)的個(gè)數(shù)
$0
:獲取當(dāng)前的程序名
$*
:獲取所有命令行參數(shù)(以字符串的形式)
$@
:獲取所有命令行參數(shù)(以字符串?dāng)?shù)組的形式)
$n
:獲取命令行的第n個(gè)參數(shù)
2. 環(huán)境變量
Shell環(huán)境變量是所有Shell程序都會(huì)接受的參數(shù),Shell程序運(yùn)行時(shí),都會(huì)接受一組變量,這組變量就是環(huán)境變量,常用的Shell環(huán)境變量如下:
$PATH
:決定了Shell將到哪個(gè)目錄中尋找命令或程序(查看環(huán)境變量的配置)
$HOME
:當(dāng)前用戶主目錄的完全路徑名
$HISTSIZE
:歷史紀(jì)錄條數(shù)
$LOGNAME
:當(dāng)前用戶的登錄名
$SHELL
:當(dāng)前用戶Shell類型
$HOSTNAME
:指主機(jī)的名稱
$UID
:獲取當(dāng)前用戶的 ID
$PWD
:當(dāng)前工作目錄的絕對(duì)路徑名
* 環(huán)境變量可以通過echo、unset來顯示和設(shè)置
* env可以顯示所有環(huán)境變量
3. 用戶變量
3.1 定義變量
定義變量時(shí),變量名不加美元符號(hào)($),如:
your_name="qinjx"
注意,變量名和等號(hào)之間不能有空格,這可能和你熟悉的所有編程語言都不一樣。
除了顯式地直接賦值,還可以用語句給變量賦值,如:
for file in `ls /etc`
- declare / typeset
declare或typeset是一樣的功能,就是聲明變量的類型。
declare [-aixr] variable
-a:將變量定義為數(shù)組類型
-i:將變量定義為整數(shù)類型
-x:將變量定義為環(huán)境變量
-r:將變量設(shè)置為readonly類型
3.2 使用變量
使用一個(gè)定義過的變量,只要在變量名前面加美元符號(hào)即可,如:
your_name="qinjx"
echo $your_name
echo ${your_name}
變量名外面的花括號(hào)是可選的,加不加都行,加花括號(hào)是為了幫助解釋器識(shí)別變量的邊界,比如下面這種情況:
for skill in Ada Coffe Action Java; do
echo "I am good at ${skill}Script"
done
如果不給skill變量加花括號(hào),寫成echo "I am good at $skillScript",解釋器就會(huì)把$skillScript當(dāng)成一個(gè)變量(其值為空),代碼執(zhí)行結(jié)果就不是我們期望的樣子了。
推薦給所有變量加上花括號(hào),這是個(gè)好的編程習(xí)慣。IntelliJ IDEA編寫shell script時(shí),IDE就會(huì)提示加花括號(hào)。
3.3 重定義變量
已定義的變量,可以被重新定義,如:
your_name="qinjx"
echo $your_name
your_name="alibaba"
echo $your_name
這樣寫是合法的,但注意,第二次賦值的時(shí)候不能寫$your_name="alibaba",使用變量的時(shí)候才加美元符。
3.4 變量鍵盤讀取
- read
read [-pt] variable
-p:后面可以接提示符
-t:后面可以接等待的秒數(shù)
例如:
read -p “Please keyin your name” -t 30 named
六、注釋
以“#”開頭的行就是注釋,會(huì)被解釋器忽略。
多行注釋
sh里沒有多行注釋,只能每一行加一個(gè)#號(hào)。就像這樣:
##### 用戶配置區(qū) 開始 #####
#
#
# 項(xiàng)目根目錄,推薦將此腳本放在項(xiàng)目的根目錄,這里就不用改了
# 應(yīng)用名,確保和Xcode里Product下的target_name.app名字一致
#
##### 用戶配置區(qū) 結(jié)束 #####
如果在開發(fā)過程中,遇到大段的代碼需要臨時(shí)注釋起來,過一會(huì)兒又取消注釋,怎么辦呢?每一行加個(gè)#符號(hào)太費(fèi)力了,可以把這一段要注釋的代碼用一對(duì)花括號(hào)括起來,定義成一個(gè)函數(shù),沒有地方調(diào)用這個(gè)函數(shù),這塊代碼就不會(huì)執(zhí)行,達(dá)到了和注釋一樣的效果。
七、字符串
字符串是shell編程中最常用最有用的數(shù)據(jù)類型(除了數(shù)字和字符串,也沒啥其它類型好用了,哈哈),字符串可以用單引號(hào),也可以用雙引號(hào),也可以不用引號(hào)。單雙引號(hào)的區(qū)別跟PHP類似。
單引號(hào)
str='this is a string'
單引號(hào)字符串的限制:
- 單引號(hào)里的任何字符都會(huì)原樣輸出,單引號(hào)字符串中的變量是無效的
- 單引號(hào)字串中不能出現(xiàn)單引號(hào)(對(duì)單引號(hào)使用轉(zhuǎn)義符后也不行)
雙引號(hào)
your_name='qinjx'
str="Hello, I know your are \"$your_name\"! \n"
- 雙引號(hào)里可以有變量
- 雙引號(hào)里可以出現(xiàn)轉(zhuǎn)義字符
字符串操作
- 拼接字符串
your_name="qinjx"
greeting="hello, "$your_name" !"
greeting_1="hello, ${your_name} !"
echo $greeting $greeting_1
- 獲取字符串長度
string="abcd"
echo ${#string} #輸出:4
- 提取子字符串
string="alibaba is a great company"
echo ${string:1:4} #輸出:liba
- 查找子字符串
string="alibaba is a great company"
echo `expr index "$string" is`#輸出:3,這個(gè)語句的意思是:找出字母i在這名話中的位置,要在linux下運(yùn)行,mac下會(huì)報(bào)錯(cuò)
八、流程控制語句
和Java、PHP等語言不一樣,sh的流程控制不可為空,如:
<?php
if (isset($_GET["q"])) {
search(q);
}
else {
//do nothing
}
在sh/bash里可不能這么寫,如果else分支沒有語句執(zhí)行,就不要寫這個(gè)else。
還要注意,sh里的if [ $foo -eq 0 ],這個(gè)方括號(hào)跟Java/PHP里if后面的圓括號(hào)大不相同,它是一個(gè)可執(zhí)行程序(和ls, grep一樣),想不到吧?在CentOS上,它在/usr/bin目錄下:
ll /usr/bin/[
-rwxr-xr-x. 1 root root 33408 6月 22 2012 /usr/bin/[
正因?yàn)榉嚼ㄌ?hào)在這里是一個(gè)可執(zhí)行程序,方括號(hào)后面必須加空格,不能寫成if [$foo -eq 0]
1. if else
if
if condition
then
command1
command2
...
commandN
fi
寫成一行(適用于終端命令提示符):
if `ps -ef | grep ssh`; then echo hello; fi
末尾的fi就是if倒過來拼寫,后面還會(huì)遇到類似的
if else
if condition
then
command1
command2
...
commandN
else
command
fi
if else-if else
if condition1
then
command1
elif condition2
command2
else
commandN
fi
2. for while
for
在開篇的示例里演示過了:
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done
寫成一行:
for var in item1 item2 ... itemN; do command1; command2… done;
C風(fēng)格的for
for (( EXP1; EXP2; EXP3 ))
do
command1
command2
command3
done
while
while condition
do
command
done
無限循環(huán)
while :
do
command
done
或者
while true
do
command
done
或者
for (( ; ; ))
until
until condition
do
command
done
3. case
case "${opt}" in
"Install-Puppet-Server" )
install_master $1
exit
;;
"Install-Puppet-Client" )
install_client $1
exit
;;
"Config-Puppet-Server" )
config_puppet_master
exit
;;
"Config-Puppet-Client" )
config_puppet_client
exit
;;
"Exit" )
exit
;;
* ) echo "Bad option, please choose again"
esac
case的語法和C family語言差別很大,它需要一個(gè)esac(就是case反過來)作為結(jié)束標(biāo)記,每個(gè)case分支用右圓括號(hào),用兩個(gè)分號(hào)表示break
九、重定向
- 標(biāo)準(zhǔn)輸入:代碼為0,使用
<
或<<
; - 標(biāo)準(zhǔn)輸出:代碼為1,使用
>
或>>
; - 標(biāo)準(zhǔn)錯(cuò)誤輸出:代碼為2,使用
2>
或2>>
; - 如果想將正確與錯(cuò)誤的數(shù)據(jù)同時(shí)寫入同一個(gè)文件中,使用
&>
。
輸出:
可以將命令的輸出內(nèi)容保存至文件,若文件不存在,系統(tǒng)將會(huì)自動(dòng)創(chuàng)建。
若使用的是>,原有的文件將會(huì)被覆蓋;
若使用的是>>,將會(huì)添加到文件末尾。
輸入:
將原本需要鍵盤輸入的數(shù)據(jù)改由文件內(nèi)容來替代。
<<當(dāng)鍵盤輸入指定內(nèi)容時(shí),輸入就結(jié)束。
十、命令執(zhí)行判斷依據(jù)
當(dāng)我們需要一次執(zhí)行很多命令時(shí),我們有一下幾種方法:
cmd ; cmd
可以一次執(zhí)行多個(gè)命令,但是不考慮命令之間的相關(guān)性,也就是說前面的命令不能影響后面的命令是否執(zhí)行。
- 與
&&
或||
若一個(gè)命令執(zhí)行的結(jié)果正確,在Linux下面會(huì)回傳一個(gè)$?=0
(命令回傳碼)的值;
我們可以通過邏輯運(yùn)算符&&
、||
來進(jìn)行判斷:
cmd1 && cmd2
若cmd1支持正確,則開始執(zhí)行cmd2,否則不執(zhí)行;
cmd1 || cmd2
若cmd1支持錯(cuò)誤,則開始執(zhí)行cmd2,否則不執(zhí)行;
十一、管道命令
管道命令“ | ”能將前一個(gè)命令傳來的正確信息作為輸入傳遞給后一個(gè)命令。
常用的管道命令:
1. cut
將同一行里面的數(shù)據(jù)進(jìn)行分解,用于分析一些數(shù)據(jù)或文字
2. grep
利用正則表達(dá)式分析一行數(shù)據(jù)
3. sort
可幫助我們進(jìn)行排序
4. uniq
刪除重復(fù)數(shù)據(jù)
5. wc
計(jì)算輸出內(nèi)容的相關(guān)信息
6. tee
同時(shí)將數(shù)據(jù)流送到文件和屏幕
7. tr
刪除或替換一段信息中的文字
8. expand
將tab按鍵轉(zhuǎn)換成空格鍵
9. split
將大文件切割為小文件
10.xargs
以空格符作為分割,產(chǎn)生某個(gè)命令的參數(shù)
十二、文件包含
可以使用source和.關(guān)鍵字,如:
source ./function.sh
. ./function.sh
在bash里,source和.是等效的,他們都是讀入function.sh的內(nèi)容并執(zhí)行其內(nèi)容(類似PHP里的include),為了更好的可移植性,推薦使用第二種寫法。
包含一個(gè)文件和執(zhí)行一個(gè)文件一樣,也要寫這個(gè)文件的路徑,不能光寫文件名,比如上述例子中:
. ./function.sh
不可以寫作:
. function.sh
如果function.sh是用戶傳入的參數(shù),如何獲得它的絕對(duì)路徑呢?方法是:
real_path=`readlink -f $1`#$1是用戶輸入的參數(shù),如function.sh
. $real_path
十三、函數(shù)
函數(shù)是存在內(nèi)存里的一組代碼的命名的元素。
函數(shù)的語法結(jié)構(gòu)為:
function <function-name> {
<code to execute>
}
我們使用位置參數(shù)$1來接收命令行里傳遞的參數(shù),需要注意的是$1 用大括號(hào)來括起來。
shell 腳本是從上至下來執(zhí)行的,如果在一個(gè)函數(shù)中調(diào)用了另一個(gè)函數(shù),那么被調(diào)用的函數(shù)要定義在調(diào)用函數(shù)的前面。
當(dāng)然,除了函數(shù)可以傳遞參數(shù)外,還可以有返回值。默認(rèn)情況下,在執(zhí)行完函數(shù)內(nèi)的最后一行代碼后,方法會(huì)返回一個(gè)狀態(tài)的數(shù)字,你可以使用$?變量來查看函數(shù)執(zhí)行的狀態(tài)。如果返回值為0,表示方法正常退出,非0表示程序發(fā)生錯(cuò)誤或其他非正常退出。
我們使用return關(guān)鍵字來返回一個(gè)整數(shù)。這里需要注意兩點(diǎn):
- 函數(shù)一旦執(zhí)行完就會(huì)返回狀態(tài)代碼。狀態(tài)代碼的范圍為0到255。
- 如果不想返回函數(shù)的狀態(tài)代碼,而是想返回一個(gè)字符串或是其他類型,可以使用 echo 變量的方式來返回值。