前端學習 linux —— shell 程式設計

2022-06-22 06:00:34

前端學習 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

Tipwhile[ 之間有空格;條件判斷] 有空格。例如刪除一個空格就會報錯 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