前端學習 linux - shell 程式設計
shell
原意是“外殼”,與 kernel
(核心)相對應,比喻核心外的一層,是使用者和核心溝通的橋樑。shell 有很多種,國內通常使用 bash
。
第一個 shell 指令碼
建立 hello-world.sh
檔案,內容如下:
test11@pjl:~/tmp$ vim hello-world.sh
#!/bin/bash
echo 'hello world'
第一行指定 shell
的型別:
test11@pjl:~/tmp$ echo $SHELL
/bin/bash
Tip:通常約定以 sh
結尾。提前劇透:
test11@pjl:~/tmp$ sh hello-world.xx
hello world
執行 sh
檔案,提示許可權不夠:
test11@pjl:~/tmp$ ./hello-world.sh
-bash: ./hello-world.sh: 許可權不夠
test11@pjl:~/tmp$ ll
-rw-rw-r-- 1 test11 test11 31 6月 17 16:18 hello-world.sh
增加可執行許可權:
test11@pjl:~/tmp$ chmod u+x hello-world.sh
test11@pjl:~/tmp$ ll
# hello-world.sh 變綠了
-rwxrw-r-- 1 test11 test11 31 6月 17 16:18 hello-world.sh*
使用相對路徑方式再次執行即可:
test11@pjl:~/tmp$ ./hello-world.sh
hello world
也可以使用絕對路徑執行:
test11@pjl:~/tmp$ /home/test11/tmp/hello-world.sh
hello world
通過 sh xx.sh
無需可執行許可權也可以執行。
Tip:下文還會使用 bash xx.sh
的執行方式。
首先刪除可執行許可權:
test11@pjl:~/tmp$ chmod u-x hello-world.sh
test11@pjl:~/tmp$ ll
總用量 20
-rw-rw-r-- 1 test11 test11 31 6月 17 16:18 hello-world.sh
test11@pjl:~/tmp$ sh hello-world.sh
hello world
shell 註釋
-
單行註釋:
# 內容
-
多行註釋:
:<<!
內容1
內容2
...
內容N
!
變數
系統變數
例如 $SHELL
就是系統變數:
test11@pjl:~/tmp$ echo $SHELL
/bin/bash
可以通過 set
檢視系統變數。例如過濾 SHELL
系統變數:
test11@pjl:~/tmp$ set |more |grep SHELL
SHELL=/bin/bash
SHELLOPTS=braceexpand:emacs:hashall:histexpand:history:interactive-comments:monitor
自定義變數
定義變數age
並輸出:
test11@pjl:~/tmp$ vim demo.sh
test11@pjl:~/tmp$ sh demo.sh
age=18
age=18
內容如下:
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
age=18
echo age=$age
echo "age=$age"
注:1. 定義變數不要在等號前後加空格
;2. 使用變數要使用 $
;3. 最後兩行輸出效果相同
# `age=18` 改為 `age= 18`
test11@pjl:~/tmp$ sh demo.sh
demo.sh: 2: 18: not found
age=
age=
使用 unset
可以銷燬變數。請看示例:
test11@pjl:~/tmp$ vim demo.sh
test11@pjl:~/tmp$ sh demo.sh
age=18
age=
# 指令碼內容
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
age=18
echo age=$age
unset age
echo age=$age
注:銷燬變數 age 後再使用該變數,沒有報錯。
通過 readonly
定義靜態變數,不能 unset
。請看示例:
test11@pjl:~/tmp$ vim demo.sh
test11@pjl:~/tmp$ sh demo.sh
age=18
demo.sh: 4: unset: age: is read only
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
readonly age=18
echo age=$age
unset age
變數定義規則:
- 字母數字下劃線,不能以
數字
開頭 - 等號兩側不能有
空格
- 變數名習慣
大寫
可以將命令執行結果賦予變數。請看示例:
命令 date
:
test11@pjl:~/tmp$ date
2022年 06月 17日 星期五 16:52:57 CST
test11@pjl:~/tmp$ vim demo.sh
test11@pjl:~/tmp$ sh demo.sh
date1=2022年 06月 17日 星期五 16:54:02 CST
date2=2022年 06月 17日 星期五 16:54:02 CST
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
date1=`date`
date2=$(date)
echo date1=$date1
echo date2=$date2
環境變數
比如我在多個 .sh
檔案中需要使用一個公共的變數,這時就可以使用環境變數,或稱之為全域性變數
。
環境變數通過 export=value
定義在 /etc/profile
檔案中。請看第 30
行:
test11@pjl:~$ cat -n /etc/profile
1 # /etc/profile: system-wide .profile file for the Bourne shell (sh(1))
2 # and Bourne compatible shells (bash(1), ksh(1), ash(1), ...).
3
4 if [ "${PS1-}" ]; then
5 if [ "${BASH-}" ] && [ "$BASH" != "/bin/sh" ]; then
6 # The file bash.bashrc already sets the default PS1.
7 # PS1='\h:\w\$ '
8 if [ -f /etc/bash.bashrc ]; then
9 . /etc/bash.bashrc
10 fi
11 else
12 if [ "`id -u`" -eq 0 ]; then
13 PS1='# '
14 else
15 PS1='$ '
16 fi
17 fi
18 fi
19
20 if [ -d /etc/profile.d ]; then
21 for i in /etc/profile.d/*.sh; do
22 if [ -r $i ]; then
23 . $i
24 fi
25 done
26 unset i
27 fi
28
29
30 export ANDROID_HOME=/home/pjl/software/android-studio-2021.1.1.22-linux/android-studio/bin
31 export PATH=$PATH:$ANDROID_HOME
這裡定義了一個環境變數 ANDROID_HOME
,將其輸出看一下:
test11@pjl:~$ echo $ANDROID_HOME
/home/pjl/software/android-studio-2021.1.1.22-linux/android-studio/bin
現在我們定義一個環境變數 EVN-VAR-TEST=pjl
:
# 檢視檔案最後兩行
root@pjl:/home/test11# tail -2 /etc/profile
export PATH=$PATH:$ANDROID_HOME
export EVN_VAR_TEST=pjl
新的環境變數需要執行 source
才能立即生效。請看例項:
# 新的環境變數未生效
root@pjl:/home/test11# echo $EVN_VAR_TEST
# 修改後的配置資訊立即生效
root@pjl:/home/test11# source /etc/profile
# 新的環境變數已生效
root@pjl:/home/test11# echo $EVN_VAR_TEST
pjl
嘗試在 demo.sh
中使用新增環境變數。請看示例:
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
:<<!
date1=`date`
date2=$(date)
echo date1=$date1
echo date2=$date2
!
echo env_var_test=$EVN_VAR_TEST
執行 demo.sh
,發現變數為空,讓配置立即生效即可:
test11@pjl:~/tmp$ sh demo.sh
env_var_test=
test11@pjl:~/tmp$ echo $EVN_VAR_TEST
test11@pjl:~/tmp$ source /etc/profile
test11@pjl:~/tmp$ echo $EVN_VAR_TEST
pjl
test11@pjl:~/tmp$ sh demo.sh
env_var_test=pjl
注:筆者以 root
使用者新增環境變數,並讓配置生效,接著切換到 test11
使用者,需要再次讓配置生效。
位置引數變數
請先看示例:
test11@pjl:~/tmp$ sh demo.sh 100 200
demo.sh 100 200
100 200
100 200
2
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
echo $0 $1 $2
echo $*
echo $@
echo $#
語法介紹:
$0
- 命令本身$1
- 第一個引數。第10個引數需要寫成${10}
$*
- 命令列中所有引數。所有引數看做一個整體$@
- 命令列中所有引數。把每個引數區分對待$#
- 引數個數
預定義變數
shell
設計者預先定義變數,可以在 shell
指令碼中直接使用。
Tip:用得不多,僅做瞭解。
語法介紹:
$$
- 當前程式的程式號
$!
- 後臺執行的最後一個程式的程式號
$?
- 最後一次執行的命名的返回狀態。0
表示執行成功。
請看示例:
test11@pjl:~/tmp$ sh demo.sh
29174
test11@pjl:~/tmp$ cat demo.sh
#!/bin/bash
echo $$
運算子
請看示例:
test11@pjl:~/tmp$ ./demo.sh 1 8
v1=18
v2=18
v3=18
v4=9
test11@pjl:~/tmp$ cat -n demo.sh
1 #!/bin/bash
2 v1=$(((1+8)*2))
3 echo v1=$v1
4 # 推薦
5 v2=$[(1+8)*2]
6 echo v2=$v2
7
8 tmp=`expr 1 + 8`
9 v3=`expr $tmp \* 2`
10 echo v3=$v3
11
12 v4=$[$1+$2]
13 echo v4=$v4
語法介紹:
- 有三種運算的方式:
$((運算子))
、$[運算式]
、expr a + b
。推薦使用$[]
expr
運算子需要有空格,例如expr 1+8
就沒有空格,而且乘號需要加一個轉義符\*
Syntax error: "(" unexpected
test11@pjl:~/tmp$ sh demo2.sh
demo2.sh: 2: Syntax error: "(" unexpected
test11@pjl:~/tmp$ cat -n demo2.sh
1 #!/bin/bash
2 v1=$[(1+8)*2]
3 echo v1=$v1
據網友介紹:ubuntu 上 sh 命令預設是指向 dash,而不是 bash。dash 比 bash 更輕量,只支援基本的 shell 功能,有些語法不識別。可以直接用 bash a.sh
,或者./a.sh
來執行指令碼。
改為 bash
或 ./
的方式執行,確實可以。請看示例:
test11@pjl:~/tmp$ bash demo2.sh
v1=18
if
語法有點怪,先看示例:
test11@pjl:~/tmp$ sh demo2.sh
abc 等於 abc
100 大於等於 99
存在 /home/test11/tmp/demo2.sh
test11@pjl:~/tmp$ cat -n demo2.sh
1 #!/bin/bash
2 if [ 'abc'='abc' ]
3 then
4 echo 'abc 等於 abc'
5 fi
6
7 if [ 100 -ge 99 ]
8 then
9 echo '100 大於等於 99'
10 fi
11
12 if [ -f /home/test11/tmp/demo2.sh ]
13 then
14 echo '存在 /home/test11/tmp/demo2.sh'
15 fi
語法介紹:
- if 判斷使用
[ 條件 ]
語法,[]
前後要有空格 - 字串比較用
=
。非空返回true
- 數字比較:
-lt
小於、-le
小於等於、-eq
等於、-gt
大於、-ge
大於等於、-ne
不等於 - 檔案許可權進行判斷:
-r
有讀的許可權、-w
有寫的許可權、-e
有執行的許可權 - 檔案型別進行判斷:
-f
檔案存在且是一個常規檔案、-e
檔案存在、-d
檔案存在並是一個目錄
注:[]
前後要有空格,否則會報錯。請看示例:
test11@pjl:~/tmp$ sh demo2.sh
demo2.sh: 2: []: not found
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
if []
then
echo '空字元'
fi
增加一個空格,由於空字元是假值,所以不會有輸出:
test11@pjl:~/tmp$ sh demo2.sh
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
if [ ]
then
echo '空字元'
fi
elseif
請看示例:
test11@pjl:~/tmp$ sh demo2.sh dog
狗,100塊
test11@pjl:~/tmp$ sh demo2.sh cat
貓,102塊
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
# echo 引數1=$1
if [ $1 = "dog" ]
then
echo '狗,100塊'
elif [ $1 = "cat" ]
then
echo '貓,102塊'
fi
類似前端的 if...elseif...elseif
。
case
請看示例,如果傳參是 dog ,輸出 '狗':
test11@pjl:~/tmp$ sh demo2.sh dog
狗
test11@pjl:~/tmp$ sh demo2.sh cat
貓
test11@pjl:~/tmp$ sh demo2.sh xx
其他動物
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
case $1 in
"dog")
echo '狗'
;;
"cat")
echo '貓'
;;
*)
echo '其他動物'
;;
esac
語法介紹:
case $變數名 in
"值1")
變數的值等於“值1”,執行程式1
;;
"值2")
變數的值等於“值2”,執行程式2
;;
*)
都不滿足,執行
;;
esac
迴圈
for
// 具體的幾個值
for i in v1 v2 v3 ...
do
程式
done
以下示例演示了 $@
和 $*
的區別:
test11@pjl:~/tmp$ sh demo2.sh a b c
num1=a
num1=b
num1=c
num2=a b c
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
for i in "$@"
do
echo num1=$i
done
for i in "$*"
do
echo num2=$i
done
你有幾個引數,$@ 就把你當做幾個;$* 只會把你當做一個整體;
語法二
for (( 初始值;迴圈控制條件;變數變化))
do
程式
done
請看示例:
test11@pjl:~/tmp$ bash demo2.sh 100
1 到 100 的和:5050
test11@pjl:~/tmp$ cat -n demo2.sh
1 #!/bin/bash
2
3 sum=0
4 for(( i=1; i <= $1; i++))
5 do
6 sum=$[$sum+$i]
7 done
8 echo 1 到 $1 的和:$sum
注:第6行不要寫成 $sum=$[$sum+$i]
while
請看示例:
test11@pjl:~/tmp$ bash demo2.sh 100
1 到 100 的和:5050
test11@pjl:~/tmp$ cat -n demo2.sh
1 #!/bin/bash
2
3 sum=0
4 i=1
5
6 # while [ $i <= $1 ]
7 while [ $i -le $1 ]
8 do
9 sum=$[$sum+$i]
10 i=$[$i+1]
11 done
12
13 echo 1 到 $1 的和:$sum
假如把第 6 行放開,報錯如下:
test11@pjl:~/tmp$ bash demo2.sh 100
demo2.sh: 行 6: =: 沒有那個檔案或目錄
1 到 100 的和:0
語法介紹:
while [ 條件判斷 ]
do
程式
done
Tip:while
和 [
之間有空格;條件判斷
與 ]
有空格。例如刪除一個空格就會報錯 while[ $i -le $1 ]
:
test11@pjl:~/tmp$ bash demo2.sh 100
demo2.sh: 行 6: while[ 1 -le 100 ]:未找到命令
demo2.sh: 行 7: 未預期的符號“do”附近有語法錯誤
demo2.sh: 行 7: `do'
read 獲取控制檯輸入
test11@pjl:~/tmp$ sh demo2.sh
請輸入你個名字:
程式會阻塞,當你輸入後會繼續執行:
test11@pjl:~/tmp$ sh demo2.sh
請輸入你個名字:pjl
name=pjl
通過 -t
引數能指定等待時間(秒),例如 5 秒內如果沒有輸入,程式會繼續執行:
test11@pjl:~/tmp$ bash demo2.sh
請輸入你個名字:name=
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
read -t 5 -p 請輸入你個名字: name
語法:read 選項 引數
。
test11@pjl:~/tmp$ read -h
-bash: read: -h:無效選項
read:用法: read [-ers] [-a 陣列] [-d 分隔符] [-i 緩衝區文字] [-n 讀取字元數] [-N 讀取字元數] [-p 提示符] [-t 超時] [-u 檔案描述符] [名稱 ...]
test11@pjl:~/tmp$ read --help
read: read [-ers] [-a 陣列] [-d 分隔符] [-i 緩衝區文字] [-n 讀取字元數] [-N 讀取字元數] [-p 提示符] [-t 超時] [-u 檔案描述符] [名稱 ...]
從標準輸入讀取一行並將其分為不同的域。
從標準輸入讀取單獨的一行,或者如果使用了 -u 選項,從檔案描述符 FD 中讀取。
該行被分割成域,如同詞語分割一樣,並且第一個詞被賦值給第一個 NAME 變數,第二
個詞被賦值給第二個 NAME 變數,如此繼續,直到剩下所有的詞被賦值給最後一個 NAME
變數。只有 $IFS 變數中的字元被認作是詞語分隔符。
如果沒有提供 NAME 變數,則讀取的行被存放在 REPLY 變數中。
選項:
-a array 將詞語賦值給 ARRAY 陣列變數的序列下標成員,從零開始
-d delim 持續讀取直到讀入 DELIM 變數中的第一個字元,而不是換行符
-e 使用 Readline 獲取行
-i text 使用 TEXT 文字作為 Readline 的初始文字
-n nchars 讀取 nchars 個字元之後返回,而不是等到讀取換行符。
但是分隔符仍然有效,如果遇到分隔符之前讀取了不足 nchars 個字元。
-N nchars 在準確讀取了 nchars 個字元之後返回,除非遇到檔案結束符或者讀超時,
任何的分隔符都被忽略
-p prompt 在嘗試讀取之前輸出 PROMPT 提示符並且不帶
換行符
-r 不允許反斜槓轉義任何字元
-s 不回顯終端的任何輸入
-t timeout 如果在 TIMEOUT 秒內沒有讀取一個完整的行則超時並且返回失敗。
TMOUT 變數的值是預設的超時時間。TIMEOUT 可以是小數。
如果 TIMEOUT 是 0,那麼僅當在指定的檔案描述符上輸入有效的時候,
read 才返回成功;否則它將立刻返回而不嘗試讀取任何資料。
如果超過了超時時間,則返回狀態碼大於 128
-u fd 從檔案描述符 FD 中讀取,而不是標準輸入
退出狀態:
返回碼為零,除非遇到了檔案結束符、讀超時(且返回碼不大於128)、
出現了變數賦值錯誤或者無效的檔案描述符作為引數傳遞給了 -u 選項。
函式
請看示例:
test11@pjl:~/tmp$ bash demo2.sh
sum=300
test11@pjl:~/tmp$ cat demo2.sh
#!/bin/bash
# 定義函式
function sum() {
# 第一個引數為 $1
sum=$[$1+$2]
echo sum=$sum
}
# 執行函式
sum 100 200
語法介紹:
[ function ] funname [()]
{
action;
[return int;]
}
系統函式
shell 中也有系統函式。我們介紹兩個拋磚引玉一下:
basename
,返回檔名dirname
,返回路徑
test11@pjl:~/tmp$ basename /a/b/c/a.txt
a.txt
test11@pjl:~/tmp$ basename /a/b/c/a.txt .txt
a
test11@pjl:~/tmp$ dirname /a/b/c/a.txt
/a/b/c
shell 綜合練習
需求:每天凌晨 3 點備份資料庫。
實現如下:
假如 test.txt
就是我們備份完成的資料庫:
root@pjl:/home/test11/tmp# ls
backup_database.sh test.txt
執行三次寫好的備份資料庫的指令碼:
root@pjl:/home/test11/tmp# bash backup_database.sh
DATETIME=2022-06-21_200903
2022-06-21_200903/
2022-06-21_200903/2022-06-21_200903.txt.gz
root@pjl:/home/test11/tmp# bash backup_database.sh
DATETIME=2022-06-21_200904
2022-06-21_200904/
2022-06-21_200904/2022-06-21_200904.txt.gz
root@pjl:/home/test11/tmp# bash backup_database.sh
DATETIME=2022-06-21_200905
2022-06-21_200905/
2022-06-21_200905/2022-06-21_200905.txt.gz
我們需要將資料備份到 /data/backup/db/
目錄中,現已生成3個備份:
root@pjl:/home/test11/tmp# ls /data/backup/db/
2022-06-21_200903.tar.gz 2022-06-21_200904.tar.gz 2022-06-21_200905.tar.gz
最後看一下備份指令碼內容:
root@pjl:/home/test11/tmp# cat -n backup_database.sh
1 #!/bin/bash
2
3 # 將資料備份到這 db 目錄
4 BACKDIR=/data/backup/db
5
6 # 當前時間
7 # 輸出:DATETIME=2022-06-21_110318
8 DATETIME=$(date +%Y-%m-%d_%H%M%S)
9 echo DATETIME=$DATETIME
10
11 # 建立備份目錄。如果不存在,則建立
12 [ ! -d "${BACKDIR}/${DATETIME}" ] && mkdir -p "${BACKDIR}/${DATETIME}"
13
14 # 備份資料。讀取 text.txt 傳給 gzip 壓縮,在重定向到指定目錄
15 cat test.txt | gzip > ${BACKDIR}/${DATETIME}/$DATETIME.txt.gz
16
17 # 將檔案處理成 tar.gz
18 cd ${BACKDIR}
19 tar -zcvf $DATETIME.tar.gz ${DATETIME}
20
21 # 刪除對應的備份目錄
22 rm -rf ${BACKDIR}/${DATETIME}
Tip:${}
通常用於劃定變數名的邊界,例如:
test11@pjl:~$ a=1
test11@pjl:~$ aa=2
test11@pjl:~$ echo $aa
2
test11@pjl:~$ echo ${a}a
1a
test11@pjl:~$ echo "${a}a"
1a