第二部分:Shell編程(二)
十一、Shell數組:Shell數組定義以及獲取數組元素
和其他編程語言一樣,Shell 也支持數組。數組(Array)是若干數據的集合,其中的每一份數據都稱為元素(Element)。
Shell 并且沒有限制數組的大小,理論上可以存放無限量的數據。和C++、Java、C# 等類似,Shell 數組元素的下標也是從 0 開始計數。
獲取數組中的元素要使用下標[ ]
,下標可以是一個整數,也可以是一個結果為整數的表達式;當然,下標必須大于等于 0。
遺憾的是,常用的 Bash Shell 只支持一維數組,不支持多維數組。
1、Shell 數組的定義
在 Shell 中,用括號( )
來表示數組,數組元素之間用空格來分隔。由此,定義數組的一般形式為:
array_name=(ele1 ?ele2 ?ele3 ... elen)
注意,賦值號=
兩邊不能有空格,必須緊挨著數組名和數組元素。
下面是一個定義數組的實例:
nums=(29 100 13 8 91 44)
Shell 是弱類型的,它并不要求所有數組元素的類型必須相同,例如:
arr=(20 56 "http://c.biancheng.net/shell/")
第三個元素就是一個“異類”,前面兩個元素都是整數,而第三個元素是字符串。
Shell 數組的長度不是固定的,定義之后還可以增加元素。例如,對于上面的 nums 數組,它的長度是 6,使用下面的代碼會在最后增加一個元素,使其長度擴展到 7:
nums[6]=88
此外,你也無需逐個元素地給數組賦值,下面的代碼就是只給特定元素賦值:
ages=([3]=24 [5]=19 [10]=12)
以上代碼就只給第 3、5、10 個元素賦值,所以數組長度是 3。
2、獲取數組元素
獲取數組元素的值,一般使用下面的格式:
${array_name[index]}
其中,array_name 是數組名,index 是下標。例如:
n=${nums[2]}
表示獲取 nums 數組的第二個元素,然后賦值給變量 n。再如:
echo ${nums[3]}
表示輸出 nums 數組的第 3 個元素。
使用@
或*
可以獲取數組中的所有元素,例如:
${nums[*]}
${nums[@]}
兩者都可以得到 nums 數組的所有元素。
完整的演示:
#!/bin/bash
nums=(29 100 13 8 91 44)
echo ${nums[@]}? ? ?#輸出所有數組元素
nums[10]=66? ? ? ? ? ??#給第10個元素賦值(此時會增加數組長度)
echo ${nums[*]}? ? ? ?#輸出所有數組元素
echo ${nums[4]}? ? ? #輸出第4個元素
運行結果:
29 100 13 8 91 44
29 100 13 8 91 44 66
91
十二、Shell獲取數組長度
所謂數組長度,就是數組元素的個數。
利用@
或*
,可以將數組擴展成列表,然后使用#
來獲取數組元素的個數,格式如下:
${#array_name[@]}
${#array_name[*]}
其中 array_name 表示數組名。兩種形式是等價的,選擇其一即可。
如果某個元素是字符串,還可以通過指定下標的方式獲得該元素的長度,如下所示:
${#arr[2]}
獲取 arr 數組的第 2 個元素(假設它是字符串)的長度。
1、回憶字符串長度的獲取
回想一下 Shell 是如何獲取字符串長度的呢?其實和獲取數組長度如出一轍,它的格式如下:
${#string_name}
string_name 是字符串名。
2、實例演示
下面我們通過實際代碼來演示一下如何獲取數組長度。
#!/bin/bash
nums=(29 100 13)
echo ${#nums[*]}#向數組中添加元素
nums[10]="http://c.biancheng.net/shell/"
echo ${#nums[@]}
echo ${#nums[10]}
#刪除數組元素
unset nums[1]
echo ${#nums[*]}
運行結果:
3
4
29
3
十三、Shell數組拼接,Shell數組合并
所謂 Shell 數組拼接(數組合并),就是將兩個數組連接成一個數組。
拼接數組的思路是:先利用@
或*
,將數組擴展成列表,然后再合并到一起。具體格式如下:
array_new=(${array1[@]} ?${array2[@]})
array_new=(${array1[*]} ?${array2[*]})
兩種方式是等價的,選擇其一即可。其中,array1 和 array2 是需要拼接的數組,array_new 是拼接后形成的新數組。
下面是完整的演示代碼:
#!/bin/bash
array1=(23 56)
array2=(99 "http://c.biancheng.net/shell/")
array_new=(${array1[@]} ${array2[*]})
echo ${array_new[@]} #也可以寫作 ${array_new[*]}
運行結果:
23 56 99 http://c.biancheng.net/shell/
十四、Shell刪除數組元素(也可以刪除整個數組)
在 Shell 中,使用 unset 關鍵字來刪除數組元素,具體格式如下:
unset array_name[index]
其中,array_name 表示數組名,index 表示數組下標。
如果不寫下標,而是寫成下面的形式:
unset array_name
那么就是刪除整個數組,所有元素都會消失。
下面我們通過具體的代碼來演示:
#!/bin/bash
arr=(23 56 99 "http://c.biancheng.net/shell/")
unset arr[1]
echo ${arr[@]}
unset arr
echo ${arr[*]}
運行結果:
23 99 http://c.biancheng.net/shell/
?
注意最后的空行,它表示什么也沒輸出,因為數組被刪除了,所以輸出為空。
十五、Shell關聯數組(下標是字符串的數組)
現在最新的 Bash Shell 已經支持關聯數組了。關聯數組使用字符串作為下標,而不是整數,這樣可以做到見名知意。
關聯數組也稱為“鍵值對(key-value)”數組,鍵(key)也即字符串形式的數組下標,值(value)也即元素值。
例如,我們可以創建一個叫做 color 的關聯數組,并用顏色名字作為下標。
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"
也可以在定義的同時賦值:
declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff")
不同于普通數組,關聯數組必須使用帶有-A
選項的 declare 命令創建。
1、訪問關聯數組元素
訪問關聯數組元素的方式幾乎與普通數組相同,具體形式為:
array_name["index"]
例如:
color["white"]="#ffffff"
color["black"]="#000000"
加上$()
即可獲取數組元素的值:
$(array_name["index"])
例如:
echo $(color["white"])
white=$(color["black"])
2、獲取所有元素的下標和值
使用下面的形式可以獲得關聯數組的所有元素值:
${array_name[@]}
${array_name[*]}
使用下面的形式可以獲取關聯數組的所有下標值:
${!array_name[@]}
${!array_name[*]}
3、獲取關聯數組長度
使用下面的形式可以獲得關聯數組的長度:
${#array_name[*]}
${#array_name[@]}
關聯數組實例演示:
#!/bin/bash
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"
color["white"]="#ffffff"
color["black"]="#000000"
#獲取所有元素值
for value in ${color[*]}
do
????????echo $value
done
echo "****************"
#獲取所有元素下標(鍵)
for key in ${!color[*]}
do
????????echo $key
done
echo "****************"
#列出所有鍵值對
for key in ${!color[@]}
do
????????echo "${key} -> ${color[$key]}"
done
運行結果:
#ff0000
#0000ff
#ffffff
#000000
#00ff00
****************
red
blue
white
black
green
****************
red -> #ff0000
blue -> #0000ff
white -> #ffffff
black -> #000000
green -> #00ff00
十六、Shell內建命令(內置命令)
所謂 Shell 內建命令,就是由 Bash 自身提供的命令,而不是文件系統中的某個可執行文件。
例如,用于進入或者切換目錄的 cd 命令,雖然我們一直在使用它,但如果不加以注意很難意識到它與普通命令的性質是不一樣的:該命令并不是某個外部文件,只要在 Shell 中你就一定可以運行這個命令。
可以使用 type 來確定一個命令是否是內建命令:
[root@localhost ~]# type cd
cd is a Shell builtin
[root@localhost ~]# type ifconfig
ifconfig is /sbin/ifconfig
由此可見,cd 是一個 Shell 內建命令,而 ifconfig 是一個外部文件,它的位置是/sbin/ifconfig
。
還記得系統變量$PATH?嗎?$PATH 變量包含的目錄中幾乎聚集了系統中絕大多數的可執行命令,它們都是外部命令。
通常來說,內建命令會比外部命令執行得更快,執行外部命令時不但會觸發磁盤 I/O,還需要 fork 出一個單獨的進程來執行,執行完成后再退出。而執行內建命令相當于調用當前 Shell 進程的一個函數。
下表列出了 Bash Shell 中直接可用的內建命令。
命令 | 說明 |
---|---|
: | 擴展參數列表,執行重定向操作 |
. | 讀取并執行指定文件中的命令(在當前 shell 環境中) |
alias | 為指定命令定義一個別名 |
bg | 將作業以后臺模式運行 |
bind | 將鍵盤序列綁定到一個 readline 函數或宏 |
break | 退出 for、while、select 或 until 循環 |
builtin | 執行指定的 shell 內建命令 |
caller | 返回活動子函數調用的上下文 |
cd | 將當前目錄切換為指定的目錄 |
command | 執行指定的命令,無需進行通常的 shell 查找 |
compgen | 為指定單詞生成可能的補全匹配 |
complete | 顯示指定的單詞是如何補全的 |
compopt | 修改指定單詞的補全選項 |
continue | 繼續執行 for、while、select 或 until 循環的下一次迭代 |
declare | 聲明一個變量或變量類型。 |
dirs | 顯示當前存儲目錄的列表 |
disown | 從進程作業表中刪除指定的作業 |
echo | 將指定字符串輸出到 STDOUT |
enable | 啟用或禁用指定的內建shell命令 |
eval | 將指定的參數拼接成一個命令,然后執行該命令 |
exec | 用指定命令替換 shell 進程 |
exit | 強制 shell 以指定的退出狀態碼退出 |
export | 設置子 shell 進程可用的變量 |
fc | 從歷史記錄中選擇命令列表 |
fg | 將作業以前臺模式運行 |
getopts | 分析指定的位置參數 |
hash | 查找并記住指定命令的全路徑名 |
help | 顯示幫助文件 |
history | 顯示命令歷史記錄 |
jobs | 列出活動作業 |
kill | 向指定的進程 ID(PID) 發送一個系統信號 |
let | 計算一個數學表達式中的每個參數 |
local | 在函數中創建一個作用域受限的變量 |
logout | 退出登錄 shell |
mapfile | 從 STDIN 讀取數據行,并將其加入索引數組 |
popd | 從目錄棧中刪除記錄 |
printf | 使用格式化字符串顯示文本 |
pushd | 向目錄棧添加一個目錄 |
pwd | 顯示當前工作目錄的路徑名 |
read | 從 STDIN 讀取一行數據并將其賦給一個變量 |
readarray | 從 STDIN 讀取數據行并將其放入索引數組 |
readonly | 從 STDIN 讀取一行數據并將其賦給一個不可修改的變量 |
return | 強制函數以某個值退出,這個值可以被調用腳本提取 |
set | 設置并顯示環境變量的值和 shell 屬性 |
shift | 將位置參數依次向下降一個位置 |
shopt | 打開/關閉控制 shell 可選行為的變量值 |
source | 讀取并執行指定文件中的命令(在當前 shell 環境中) |
suspend | 暫停 Shell 的執行,直到收到一個 SIGCONT 信號 |
test | 基于指定條件返回退出狀態碼 0 或 1 |
times | 顯示累計的用戶和系統時間 |
trap | 如果收到了指定的系統信號,執行指定的命令 |
type | 顯示指定的單詞如果作為命令將會如何被解釋 |
typeset | 聲明一個變量或變量類型。 |
ulimit | 為系統用戶設置指定的資源的上限 |
umask | 為新建的文件和目錄設置默認權限 |
unalias | 刪除指定的別名 |
unset | 刪除指定的環境變量或 shell 屬性 |
wait | 等待指定的進程完成,并返回退出狀態碼 |
接下來的幾節我們將重點講解幾個常用的 Shell 內置命令。
十七、Shell alias:給命令創建別名
alisa 用來給命令創建一個別名。若直接輸入該命令且不帶任何參數,則列出當前 Shell 進程中使用了哪些別名。現在你應該能理解類似ll
這樣的命令為什么與ls -l
的效果是一樣的吧。
下面讓我們來看一下有哪些命令被默認創建了別名:
[mozhiyan@localhost ~]$ alias
alias cp='cp -i'
alias l.='ls -d .* --color=tty'
alias ll='ls -l --color=tty'
alias ls='ls --color=tty'
alias mv='mv -i'
alias rm='rm -i'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
你看,為了讓我們使用方便,Shell 會給某些命令默認創建別名。
1、使用 alias 命令自定義別名
使用 alias 命令自定義別名的語法格式為:
alias new_name='command'
比如,一般的關機命令是shutdown-h now
,寫起來比較長,這時可以重新定義一個關機命令,以后就方便多了。
alias myShutdown='shutdown -h now'
再如,通過 date 命令可以獲得當前的 UNIX 時間戳,具體寫法為date +%s
,如果你嫌棄它太長或者不容易記住,那可以給它定義一個別名。
alias timestamp='date +%s'
在《三、Shell命令替換》一節中,我們使用date +%s
計算腳本的運行時間,現在學了 alias,就可以簡化代碼了。
#!/bin/bash
alias timestamp='date +%s'
begin=`timestamp`
sleep 20s
finish=$(timestamp)
difference=$((finish - begin))
echo "run time: ${difference}s"
運行腳本,20 秒后看到輸出結果:
run time: 20s
別名只是臨時的
在代碼中使用 alias 命令定義的別名只能在當前 Shell 進程中使用,在子進程和其它進程中都不能使用。當前 Shell 進程結束后,別名也隨之消失。
要想讓別名對所有的 Shell 進程都有效,就得把別名寫入 Shell 配置文件。Shell 進程每次啟動時都會執行配置文件中的代碼做一些初始化工作,將別名放在配置文件中,那么每次啟動進程都會定義這個別名。
2、使用 unalias 命令刪除別名
使用 unalias 內建命令可以刪除當前 Shell 進程中的別名。unalias 有兩種使用方法:
- 第一種用法是在命令后跟上某個命令的別名,用于刪除指定的別名。
- 第二種用法是在命令后接
-a
參數,刪除當前 Shell 進程中所有的別名。
同樣,這兩種方法都是在當前 Shell 進程中生效的。要想永久刪除配置文件中定義的別名,只能進入該文件手動刪除。
# 刪除 ll 別名
[mozhiyan@localhost ~]$ unalias ll
# 再次運行該命令時,報“找不到該命令”的錯誤,說明該別名被刪除了
[mozhiyan@localhost ~]$ ll
-bash: ll: command not found
十八、Shell echo命令:輸出字符串
echo 是一個Shell內建明命令?,用來在終端輸出字符串,并在最后默認加上換行符。請看下面的例子:
#!/bin/bash
name="Shell教程"
url="http://c.biancheng.net/shell/"
echo "讀者,你好! "? ? ? ? ? ? ? ? ? ? ? ? ? ? ?#直接輸出字符串
echo $url #輸出變量
echo "${name}的網址是:${url}"? ? ? ? #雙引號包圍的字符串中可以解析變量
echo '${name}的網址是:${url}'? ? ? ? ?#單引號包圍的字符串中不能解析變量
運行結果:
讀者,你好!
http://c.biancheng.net/shell/
Shell教程的網址是:http://c.biancheng.net/shell/
${name}的網址是:${url}
1、不換行
echo 命令輸出結束后默認會換行,如果不希望換行,可以加上-n
參數,如下所示:
#!/bin/bash
name="Tom"
age=20
height=175
weight=62
echo -n "${name} is ${age} years old, "
echo -n "${height}cm in height "
echo "and ${weight}kg in weight."
echo "Thank you!"
運行結果:
Tom is 20 years old, 175cm in height and 62kg in weight.
Thank you!
2、輸出轉義字符
默認情況下,echo 不會解析以反斜杠\
開頭的轉義字符。比如,\n
表示換行,echo 默認會將它作為普通字符對待。請看下面的例子:
[root@localhost ~]# echo "hello \nworld"
hello \nworld
我們可以添加-e
參數來讓 echo 命令解析轉義字符。例如:
[root@localhost ~]# echo -e "hello \nworld"
hello
world
\c 轉義字符
有了-e
參數,我們也可以使用轉義字符\c
來強制 echo 命令不換行了。請看下面的例子:
#!/bin/bash
name="Tom"
age=20
height=175
weight=62
echo -e "${name} is ${age} years old, \c"
echo -e "${height}cm in height \c"
echo "and ${weight}kg in weight."
echo "Thank you!"
運行結果:
Tom is 20 years old, 175cm in height and 62kg in weight.
Thank you!
十九、Shell read命令:讀取從鍵盤輸入的數據
read 是Shell內置明命令,用來從標準輸入中讀取數據并賦值給變量。如果沒有進行重定向,默認就是從鍵盤讀取用戶輸入的數據;如果進行了重定向,那么可以從文件中讀取數據。
后續我們會在《Linux Shell重定向》一節中深入講解重定向的概念,不了解的讀者可以不用理會,暫時就認為:read 命令就是從鍵盤讀取數據。
read 命令的用法為:
read [-options] [variables]
options
表示選項,如下表所示;variables
表示用來存儲數據的變量,可以有一個,也可以有多個。
options
和variables
都是可選的,如果沒有提供變量名,那么讀取的數據將存放到環境變量 REPLY 中。
選項 | 說明 |
---|---|
-a array | 把讀取的數據賦值給數組 array,從下標?0 開始。 |
-d delimiter | 用字符串 delimiter 指定讀取結束的位置,而不是一個換行符(讀取到的數據不包括 delimiter)。 |
-e | 在獲取用戶輸入的時候,對功能鍵進行編碼轉換,不會直接顯式功能鍵對應的字符。 |
-n num | 讀取 num 個字符,而不是整行字符。 |
-p prompt | 顯示提示信息,提示內容為 prompt。 |
-r | 原樣讀取(Raw mode),不把反斜杠字符解釋為轉義字符。 |
-s | 靜默模式(Silent mode),不會在屏幕上顯示輸入的字符。當輸入密碼和其它確認信息的時候,這是很有必要的。 |
設置超時時間,單位為秒。如果用戶沒有在指定時間內輸入完成,那么 read 將會返回一個非 0 的退出狀態,表示讀取失敗。 | |
-u fd | 使用文件描述符 fd 作為輸入源,而不是標準輸入,類似于重定向。 |
【實例1】使用 read 命令給多個變量賦值。
#!/bin/bash
read -p "Enter some information > " name url age
echo "網站名字:$name"
echo "網址:$url"
echo "年齡:$age"
運行結果:
Enter some information > C語言中文網 http://c.biancheng.net 7↙
網站名字:C語言中文網
網址:http://c.biancheng.net
年齡:7
注意,必須在一行內輸入所有的值,不能換行,否則只能給第一個變量賦值,后續變量都會賦值失敗。
本例還使用了-p
選項,該選項會用一段文本來提示用戶輸入。
【示例2】只讀取一個字符。
#!/bin/bash
read -n 1 -p "Enter a char > " char
printf "\n" #換行
echo $char
運行結果:
Enter a char > 1
1
-n 1
表示只讀取一個字符。運行腳本后,只要用戶輸入一個字符,立即讀取結束,不用等待用戶按下回車鍵。printf "\n"
語句用來達到換行的效果,否則 echo 的輸出結果會和用戶輸入的內容位于同一行,不容易區分。
【實例3】在指定時間內輸入密碼。
#!/bin/bash
if
????????read -t 20 -sp "Enter password in 20 seconds(once) > " pass1 && printf "\n" && #第一次輸入密碼
????????read -t 20 -sp "Enter password in 20 seconds(again)> " pass2 && printf "\n" && #第二次輸入密碼
????????[ $pass1 == $pass2 ] #判斷兩次輸入的密碼是否相等
then
????????echo "Valid password"
else
????????echo "Invalid password"
fi
這段代碼中,我們使用&&
組合了多個命令,這些命令會依次執行,并且從整體上作為 if 語句的判斷條件,只要其中一個命令執行失敗(退出狀態為非 0 值),整個判斷條件就失敗了,后續的命令也就沒有必要執行了。
如果兩次輸入密碼相同,運行結果為:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)>
Valid password
如果兩次輸入密碼不同,運行結果為:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)>
Invalid password
如果第一次輸入超時,運行結果為:
Enter password in 20 seconds(once) > Invalid password
如果第二次輸入超時,運行結果為:
Enter password in 20 seconds(once) >
Enter password in 20 seconds(again)> Invalid password
二十、Shell exit命令:退出當前進程
exit 是一個Shell內置明命令?,用來退出當前 Shell 進程,并返回一個退出狀態;使用$?
可以接收這個退出狀態,這一點已在《七、Shell $?:獲取函數返回值或者上一個命令的退出狀態》一節中進行了講解。
exit 命令可以接受一個整數值作為參數,代表退出狀態。如果不指定,默認狀態值是 0。
一般情況下,退出狀態為 0 表示成功,退出狀態為非 0 表示執行失敗(出錯)了。
exit 退出狀態只能是一個介于 0~255 之間的整數,其中只有 0 表示成功,其它值都表示失敗。
Shell 進程執行出錯時,可以根據退出狀態來判斷具體出現了什么錯誤,比如打開一個文件時,我們可以指定 1 表示文件不存在,2 表示文件沒有讀取權限,3 表示文件類型不對。
編寫下面的腳本,并命名為 test.sh:
#!/bin/bash
echo "befor exit"
exit 8
echo "after exit"
運行該腳本:
[mozhiyan@localhost ~]$ bash ./test.sh
befor exit
可以看到,"after exit"
并沒有輸出,這說明遇到 exit 命令后,test.sh 執行就結束了。
注意, exit 表示退出當前 Shell 進程,我們必須在新進程中運行 test.sh,否則當前 Shell 會話(終端窗口)會被關閉,我們就無法看到輸出結果了。
我們可以緊接著使用$?
來獲取 test.sh 的退出狀態:
[mozhiyan@localhost ~]$ echo $?
8