Linux shell 腳本編程-基礎篇 (一)

1. 構建基本腳本



1.1 使用多個命令


shell 腳本的關鍵在於輸入多個命令並處理每個命令的結果,甚至需要將一個命令的結果傳給另一個命令。shell可以讓將多個命令串起來,一次執行完成。
如果要兩個命令一起運行,可以把它們放在同一行中,彼此間用分號隔開。

    [root@devalone testdir]# date; who
    2018年 07月 04日 星期三 19:56:07 CST
    devalone pts/0        2018-07-04 09:17 (192.168.1.101)

使用這種辦法就能將任意多個命令串連在一起使用了,只要不超過最大命令行字符數 255 就行。

這種技術對於小型腳本尚可,但它有一個很大的缺陷:每次運行之前,都必須在命令提示符下輸入整個命令。可以將這些命令組合成一個簡單的文本文件,
這樣就不需要在命令行中手動輸入了。在需要運行這些命令時,只用運行這個文本文件就行了。


1.2 創建 shell 腳本文件
-----------------------------------------------------------------------------------------------------------------------------------------
要將 shell 命令放到文本文件中,需要用文本編輯器來創建一個文件,然後將命令輸入到文件中。

通常以 .sh 作爲文件後綴名,也可以沒有後綴名。

在創建shell腳本文件時,必須在文件的第一行指定要使用的shell。其格式爲:

#!/bin/bash

在通常的shell腳本中,井號(#)用作註釋行。shell 並不會處理 shell 腳本中的註釋行。然而,shell腳本文件的第一行是個例外,# 後面的驚歎號會告訴
shell 用哪個 shell 來運行腳本(可以使用 bash shell,也可以使用另一個 shell 來運行腳本)。

註釋內容:建議養成良好的腳本編寫習慣,在每個 script 文件頭處記錄好如下內容:
    
    ● script 功能
    ● script 版本信息
    ● script 作者與聯繫方式
    ● script 版權聲明
    ● script 歷史版本信息
    ● script 內較特殊的指令,使用絕對路徑表達
    ● script 運行時所需的環境變量聲明與設置

在指定了 shell 之後,就可以在文件的每一行中輸入命令,然後加一個回車符。註釋可用#添加。
示例:

    #!/bin/bash
    # This script displays the date and who's log on
    echo -n  "The time and date are: "
    date
    echo "Let's see who's logged into the system:"
    who

可以根據需要,使用分號將兩個命令放在一行上,但在 shell 腳本中,可以在獨立的行中書寫命令。shell 會按根據命令在文件中出現的順序進行處理。

要注意另有一行也以 # 開頭,並添加了一個註釋。shell 不會解釋以 # 開頭的行(除了以 #! 開頭的第一行)。留下注釋來說明腳本做了什麼,這種方法
非常好。

要執行腳本文件需要處理兩件事:

    
    第一,要讓 shell 找到腳本文件
    -------------------------------------------------------------------------------------------------------------------------------------
    要讓 bash shell 能找到要執行的腳本文件。需採取以下兩種做法之一:

        ● 將 shell 腳本文件所處的目錄添加到 PATH 環境變量中;
        ● 在提示符中用絕對或相對文件路徑來引用 shell 腳本文件。

    爲了引用當前目錄下的文件,可以在 shell 中使用單點操作符。

    
    第二,要保證腳本文件對執行者有執行權限
    -------------------------------------------------------------------------------------------------------------------------------------
    通過 chmod 命令賦予用戶執行文件的權限:

    [devalone@devalone shell-script]$ chmod a+x test1.sh
    [devalone@devalone shell-script]$ ls -l test1.sh
    -rwxrwxr-x. 1 devalone devalone 152 1月   2 2018 test1.sh


    
■ 腳本的執行規則
-----------------------------------------------------------------------------------------------------------------------------------------
腳本執行遵循如下規則:

    (1) 指令的執行從上而下、從左而右分析執行;
    (2) 指令、選項與參數間的多個空白會被忽略掉;
    (3) 空白行也被忽略掉,[tab] 鍵空白被視爲空格;    
    (4) 讀到 Enter 鍵符號(CR)時,嘗試開始執行該行或該串命令;
    (5) 如果一行內容太多,可以使用 "\Enter" 符號來延伸至下一行;
    (6) "#" 被視爲註釋,任何加在 # 後面的文本被視爲註釋而被忽略。


■ 腳本執行方法
-----------------------------------------------------------------------------------------------------------------------------------------
    ● 直接下達指令:在子進程中執行腳本
        
        絕對路徑:/home/devalone/shell.sh
        相對路徑:./shell.sh
        環境變量 "PATH" 所包含的路徑:shell.sh

    ● 以 bash 程序來執行: 在子進程中執行腳本
            
        bash shell.sh
        sh shell.sh
    
        觀查:
            [devalone@devalone 19]$ which sh
            /usr/bin/sh
            [devalone@devalone 19]$ ll /usr/bin/sh
            lrwxrwxrwx. 1 root root 4 9月  30 2016 /usr/bin/sh -> bash
        
            sh 是 bash 的連接,因此 sh 可以直接執行腳本。
    
    ● source 或 .    :在父進程中執行腳本
    
        source shell.sh
        . shell.sh

        
■ 腳本的追蹤與 debug
-----------------------------------------------------------------------------------------------------------------------------------------    
script 在執行之前,通過 bash 相關參數判斷可以判斷腳本中是否有語法錯誤。

[devalone@devalone shell-script]$ sh [-nvx] scripts.sh

選項:
    -n :不執行 script,僅檢查語法問題;
    -v :在執行 sccript 之前,先將 scripts 的內容輸出到屏幕上;
    -x :將 script 的執行過程顯示到屏幕上。
    
示例: 檢查 test1.sh 是否有語法問題

    [devalone@devalone shell-script]$ sh -n test1.sh
    [devalone@devalone shell-script]$ 語法沒有問題,不會顯示任何信息
    
    
示例:使用 -v 選項顯示腳本內容及其執行過程

    [devalone@devalone shell-script]$ sh -v test1.sh
    module () {  eval `/usr/bin/modulecmd bash $*`
    }
    scl () {  local CMD=$1;
     if [ "$CMD" = "load" -o "$CMD" = "unload" ]; then
     eval "module $@";
     else
     /usr/bin/scl "$@";
     fi
    }
    #!/bin/bash

    # This script displays the date and who's log on
    echo -n  "The time and date are: "
    The time and date are: date
    2018年 07月 10日 星期二 15:53:27 CST
    echo "Let's see who's logged into the system:"
    Let's see who's logged into the system:
    who
    devalone pts/0        2018-07-10 10:18 (192.168.1.101)


示例: 將 test1.sh 的執行過程顯示到屏幕上:

    [devalone@devalone shell-script]$ sh -x test1.sh
    + echo -n 'The time and date are: '
    The time and date are: + date
    2018年 07月 10日 星期二 15:50:31 CST
    + echo 'Let'\''s see who'\''s logged into the system:'
    Let's see who's logged into the system:
    + who
    devalone pts/0        2018-07-10 10:18 (192.168.1.101)

 


1.3 顯示消息
-----------------------------------------------------------------------------------------------------------------------------------------
在 echo 命令後面加上了一個字符串,該命令就能顯示出這個文本字符串。

    [devalone@devalone shell-script]$ echo this is a test
    this is a test

默認情況下,不需要使用引號將要顯示的文本字符串劃定出來。但有時在字符串中出現引號的話就比較麻煩了。echo 命令可用單引號或雙引號來劃定文本
字符串。如果在字符串中用到了它們,需要在文本中使用其中一種引號,而用另外一種來將字符串劃定起來。

    [devalone@devalone shell-script]$ echo "This is a test to see if you're paying attention"
    This is a test to see if you're paying attention

可以將 echo 語句添加到 shell 腳本中任何需要顯示額外信息的地方。

    [devalone@devalone shell-script]$ cat test1.sh
    #!/bin/bash
    
    # This script displays the date and who's log on
    echo -n  "The time and date are: "
    date
    echo "Let's see who's logged into the system:"
    who

設置 PATH 環境變量:將當前工作目錄加入 PATH 路徑中。
    
    [devalone@devalone shell-script]$ PATH=$PATH:.
    
運行:
    [devalone@devalone shell-script]$ test1.sh
    The time and date are: 2018年 07月 04日 星期三 20:23:44 CST
    Let's see who's logged into the system:
    devalone pts/0        2018-07-04 09:17 (192.168.1.101)

如果想把文本字符串和命令輸出顯示在同一行中,可以用 echo 語句的 -n 參數。只要將第一個 echo 語句改成這樣就行:
如上例所示。

 

1.4 使用變量
-----------------------------------------------------------------------------------------------------------------------------------------
變量允許臨時性地將信息存儲在 shell 腳本中,以便和腳本中的其他命令一起使用。


1.4.1 環境變量
-----------------------------------------------------------------------------------------------------------------------------------------
Linux 系統的環境變量,也可以在腳本中訪問。

shell 維護着一組環境變量,用來記錄特定的系統信息。比如系統的名稱、登錄到系統上的用戶名、用戶的系統ID(也稱爲UID)、用戶的默認主目錄以及
shell 查找程序的搜索路徑。可以用 set 命令來顯示一份完整的當前環境變量列表。

    [devalone@devalone ~]$ set
    BASH=/bin/bash
    BASHOPTS=checkwinsize:cmdhist:complete_fullquote:expand_aliases:extglob:extquote:force_fignore:histappend:interactive_comments
    :login_shell:progcomp:promptvars:sourcepath
    BASH_ALIASES=()
    BASH_ARGC=()
    BASH_ARGV=()
    BASH_CMDS=()
    BASH_COMPLETION_COMPAT_DIR=/etc/bash_completion.d
    BASH_LINENO=()
    BASH_REMATCH=()
    BASH_SOURCE=()
    BASH_VERSINFO=([0]="4" [1]="3" [2]="42" [3]="1" [4]="release" [5]="x86_64-redhat-linux-gnu")
    BASH_VERSION='4.3.42(1)-release'
    CATALINA_HOME=/home/devalone/programs/apache-tomcat-8.5.11
    COLUMNS=130
    COMP_WORDBREAKS=$' \t\n"\'><=;|&(:'
    DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
    DIRSTACK=()
    EUID=1000
    GROUPS=()
    HISTCONTROL=ignoredups
    HISTFILE=/home/devalone/.bash_history
    HISTFILESIZE=1000
    HISTSIZE=1000
    HOME=/home/devalone
    HOSTNAME=devalone.sansovo.org
    ...

在腳本中,可以在環境變量名稱之前加上美元符($)來使用這些環境變量:
    
    [devalone@devalone shell-script]$ cat test2.sh
    #!/bin/bash
    # display user information from the system.
    echo "User info for userid: $USER"
    echo UID: $UID
    echo HOME: $HOME

$USER、$UID 和 $HOME 環境變量用來顯示已登錄用戶的有關信息。腳本輸出如下:

    [devalone@devalone shell-script]$ ./test2.sh
    User info for userid: devalone
    UID: 1000
    HOME: /home/devalone

注意,echo 命令中的環境變量會在腳本運行時替換成當前值。另外,在第一個字符串中可以將 $USER 系統變量放置到雙引號中,而 shell 依然能夠知道
我們的意圖。


    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    可以通過 ${variable} 形式引用的變量。變量名兩側額外的花括號通常用來幫助識別美元符後的變量名。
    
    
1.4.2 用戶變量
-----------------------------------------------------------------------------------------------------------------------------------------
除了環境變量,shell 腳本還允許在腳本中定義和使用自己的變量。定義變量允許臨時存儲數據並在整個腳本中使用,從而使shell腳本看起來更像一個真正
的計算機程序。

用戶變量可以是任何由字母、數字或下劃線組成的文本字符串,長度不超過20個。用戶變量區分大小寫。

使用等號將值賦給用戶變量。在變量、等號和值之間不能出現空格。

示例:

    var1=10
    var2=-57
    var3=testing
    var4="still more testing"

shell 腳本會自動決定變量值的數據類型。在腳本的整個生命週期裏,shell 腳本中定義的變量會一直保持着它們的值,但在shell腳本結束時會被刪除掉。

與系統變量類似,用戶變量可通過美元符 ($) 引用。

示例:
    [devalone@devalone shell-script]$ cat test3.sh
    #!/bin/bash
    # testing variables
    days=10
    guest="Katie"
    echo "$guest checked in $days days ago"

    days=5
    guest="Jessical"
    echo "$guest checked in $days days ago"

運行:
    [devalone@devalone shell-script]$ ./test3.sh
    Katie checked in 10 days ago
    Jessical checked in 5 days ago

變量每次被引用時,都會輸出當前賦給它的值。重要的是要記住,引用一個變量值時需要使用美元符,而引用變量來對其進行賦值時則不要使用美元符。

示例:
    [devalone@devalone shell-script]$ cat test4.sh
    #!/bin/bash
    # assigning a variable value to another variable

    value1=10
    value2=$value1
    echo The resulting value is $value2

在賦值語句中使用value1變量的值時,仍然必須用美元符。

運行:
    [devalone@devalone shell-script]$ ./test4.sh
    The resulting value is 10

沒有美元符,shell 會將變量名解釋成普通的文本字符串,通常這並不是想要的結果。


1.4.3 命令替換
-----------------------------------------------------------------------------------------------------------------------------------------
shell 腳本中最有用的特性之一就是可以從命令輸出中提取信息,並將其賦給變量。把輸出賦給變量之後,就可以隨意在腳本中使用了。這個特性在處理腳本
數據時尤爲方便。

有兩種方法可以將命令輸出賦給變量:

    □ 反引號字符(`)
    □ $()格式

命令替換允許用戶將 shell 命令的輸出賦給變量。

用一對反引號把整個命令行命令圍起來:

    testing='date'
    
使用$()格式:

    testing=$(date)

shell 會運行命令替換符號中的命令,並將其輸出賦給變量 testing。注意,賦值等號和命令替換字符之間沒有空格。

    [devalone@devalone shell-script]$ cat test5.sh
    #!/bin/bash
    testing=$(date)
    echo "The date and time are: " $testing

變量 testing 獲得了 date 命令的輸出,然後使用 echo 語句顯示出它的值。

運行:
    [devalone@devalone shell-script]$ ./test5.sh
    The date and time are:  2018年 07月 05日 星期四 11:31:37 CST

示例:
    [devalone@devalone shell-script]$ cat test6.sh
    #!/bin/bash
    # copy the /usr/bin directory listing to a log file
    today=$(date +%y%m%d)
    ls /usr/bin -al > log.$today

在腳本中通過命令替換獲得當前日期並用它來生成唯一文件名。today 變量是被賦予格式化後的 date 命令的輸出。這是提取日期信息來生成日誌文件名常用
的一種技術。+%y%m%d格式告訴date命令將日期顯示爲兩位數的年月日的組合。

運行:
    [devalone@devalone shell-script]$ ./test6.sh
    [devalone@devalone shell-script]$ ls -l log*
    -rw-rw-r--. 1 devalone devalone 122402 7月   5 11:45 log.180705

生成了以當前日期作爲文件名後綴的文件:log.180705


    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    命令替換會創建一個子shell 來運行對應的命令。子shell(subshell)是由運行該腳本的 shell 所創建出來的一個獨立的子shell(child shell)。
    正因如此,由該子shell 所執行命令是無法使用腳本中所創建的變量的。
    
    在命令行提示符下使用路徑./運行命令的話,也會創建出子shell;要是運行命令的時候不加入路徑,就不會創建子shell。如果使用的是內建的 shell
    命令,並不會涉及子shell。
    
    
1.5 重定向輸入和輸出
-----------------------------------------------------------------------------------------------------------------------------------------
有些時候想要保存某個命令的輸出而不僅僅只是讓它顯示在顯示器上。bash shell提供了幾個操作符,可以將命令的輸出重定向到另一個位置(比如文件)。
重定向可以用於輸入,也可以用於輸出,可以將文件重定向到命令輸入。


1.5.1 輸出重定向
-----------------------------------------------------------------------------------------------------------------------------------------
最基本的重定向將命令的輸出發送到一個文件中。bash shell 用大於號(>)來完成這項功能:

    command > outputfile

示例:
    [devalone@devalone shell-script]$ date > testdate
    [devalone@devalone shell-script]$ ls -l testdate
    -rw-rw-r--. 1 devalone devalone 43 7月   5 11:58 testdate

    [devalone@devalone shell-script]$ cat testdate
    2018年 07月 05日 星期四 11:58:19 CST

重定向操作符創建了一個文件 testdate(通過默認的 umask 設置),並將 date 命令的輸出重定向到該文件中。如果輸出文件已經存在了,重定向操作符
會用新的文件數據覆蓋已有文件。    

示例:
    [devalone@devalone shell-script]$ who >testdate
    [devalone@devalone shell-script]$ cat testdate
    devalone pts/0        2018-07-05 11:05 (192.168.1.101)

testdate 內容被覆蓋。

如果不想覆蓋文件原有內容,而是想要將命令的輸出追加到已有文件中,比如正在創建一個記錄系統上某個操作的日誌文件。在這種情況下,可以用雙大於號
>> 來追加數據。

示例:
    [devalone@devalone shell-script]$ date >> testdate
    [devalone@devalone shell-script]$ cat testdate
    devalone pts/0        2018-07-05 11:05 (192.168.1.101)
    2018年 07月 05日 星期四 12:03:51 CST


1.5.2 輸入重定向
-----------------------------------------------------------------------------------------------------------------------------------------
輸入重定向和輸出重定向正好相反。輸入重定向將文件的內容重定向到命令,而非將命令的輸出重定向到文件。

輸入重定向符號是小於號(<):

    command < inputfile

一個簡單的記憶方法就是:在命令行上,命令總是在左側,而重定向符號 "指向" 數據流動的方向。小於號說明數據正在從輸入文件流向命令。

示例:
    [devalone@devalone shell-script]$ wc < testdate
     2 11 98

wc 命令可以對對數據中的文本進行計數。默認情況下,它會輸出3個值:

    □ 文本的行數
    □ 文本的詞數
    □ 文本的字節數
    
通過將文本文件重定向到 wc 命令,立刻就可以得到文件中的行、詞和字節的計數。這個例子說明 testdate 文件有2行、11個 單詞以及 98 字節。

還有另外一種輸入重定向的方法,稱爲內聯輸入重定向(inline input redirection)。這種方法無需使用文件進行重定向,只需要在命令行中指定用於輸入
重定向的數據就可以了。

內聯輸入重定向符號是遠小於號(<<)。除了這個符號,必須指定一個文本標記來劃分輸入數據的開始和結尾。任何字符串都可作爲文本標記,但在數據的
開始和結尾文本標記必須一致。

命令格式:

    command << marker
    data
    marker
    
在命令行上使用內聯輸入重定向時,shell 會用 PS2 環境變量中定義的次提示符來提示輸入數據。下面是它的使用情況。    

示例:

    [devalone@devalone shell-script]$ wc << EOF
    > test string 1
    > test string 2
    > test string 3
    > EOF
     3  9 42

次提示符會持續提示,以獲取更多的輸入數據,直到輸入了作爲文本標記的那個字符串。wc 命令會對內聯輸入重定向提供的數據進行行、詞和字節計數。


1.6 管道
-----------------------------------------------------------------------------------------------------------------------------------------
有時需要將一個命令的輸出作爲另一個命令的輸入。這可以用重定向來實現,只是有些笨拙。

如:
    [devalone@devalone shell-script]$ rpm -qa > rpm.list
    [devalone@devalone shell-script]$ sort < rpm.list
    aajohan-comfortaa-fonts-2.004-6.fc24.noarch
    abattis-cantarell-fonts-0.0.24-1.fc24.noarch
    abrt-2.8.2-1.fc24.x86_64
    abrt-addon-ccpp-2.8.2-1.fc24.x86_64
    abrt-addon-coredump-helper-2.8.2-1.fc24.x86_64
    abrt-addon-kerneloops-2.8.2-1.fc24.x86_64
    abrt-addon-pstoreoops-2.8.2-1.fc24.x86_64
    abrt-addon-python3-2.8.2-1.fc24.x86_64
    abrt-addon-vmcore-2.8.2-1.fc24.x86_64
    abrt-addon-xorg-2.8.2-1.fc24.x86_64
    abrt-cli-2.8.2-1.fc24.x86_64
    abrt-dbus-2.8.2-1.fc24.x86_64
    abrt-desktop-2.8.2-1.fc24.x86_64
    ...

這種方法的確管用,但仍然是一種比較繁瑣的信息生成方式。我們用不着將命令輸出重定向到文件中,可以將其直接重定向到另一個命令。這個過程叫作
管道連接(piping)。

管道被放在命令之間,將一個命令的輸出重定向到另一個命令中:

    command1 | command2
    
不要以爲由管道串起的兩個命令會依次執行。Linux 系統實際上會同時運行這兩個命令,在系統內部將它們連接起來。在第一個命令產生輸出的同時,輸出
會被立即送給第二個命令。數據傳輸不會用到任何中間文件或緩衝區。

    示例:
    [devalone@devalone shell-script]$ rpm -qa | sort
    aajohan-comfortaa-fonts-2.004-6.fc24.noarch
    abattis-cantarell-fonts-0.0.24-1.fc24.noarch
    abrt-2.8.2-1.fc24.x86_64
    abrt-addon-ccpp-2.8.2-1.fc24.x86_64
    abrt-addon-coredump-helper-2.8.2-1.fc24.x86_64
    abrt-addon-kerneloops-2.8.2-1.fc24.x86_64
    abrt-addon-pstoreoops-2.8.2-1.fc24.x86_64
    abrt-addon-python3-2.8.2-1.fc24.x86_64
    abrt-addon-vmcore-2.8.2-1.fc24.x86_64
    abrt-addon-xorg-2.8.2-1.fc24.x86_64
    abrt-cli-2.8.2-1.fc24.x86_64
    abrt-dbus-2.8.2-1.fc24.x86_64
    abrt-desktop-2.8.2-1.fc24.x86_64
    abrt-gui-2.8.2-1.fc24.x86_64
    abrt-gui-libs-2.8.2-1.fc24.x86_64
    ...


可以在一條命令中使用任意多條管道。可以持續地將命令的輸出通過管道傳給其他命令來細化操作。

示例:
    
    [devalone@devalone shell-script]$ rpm -qa | sort | less
    aajohan-comfortaa-fonts-2.004-6.fc24.noarch
    abattis-cantarell-fonts-0.0.24-1.fc24.noarch
    abrt-2.8.2-1.fc24.x86_64
    abrt-addon-ccpp-2.8.2-1.fc24.x86_64
    abrt-addon-coredump-helper-2.8.2-1.fc24.x86_64
    abrt-addon-kerneloops-2.8.2-1.fc24.x86_64
    abrt-addon-pstoreoops-2.8.2-1.fc24.x86_64
    abrt-addon-python3-2.8.2-1.fc24.x86_64
    abrt-addon-vmcore-2.8.2-1.fc24.x86_64
    abrt-addon-xorg-2.8.2-1.fc24.x86_64
    abrt-cli-2.8.2-1.fc24.x86_64
    abrt-dbus-2.8.2-1.fc24.x86_64
    abrt-desktop-2.8.2-1.fc24.x86_64
    abrt-gui-2.8.2-1.fc24.x86_64
    abrt-gui-libs-2.8.2-1.fc24.x86_64
    ...


也可以搭配使用重定向和管道來將輸出保存到文件中:

    [devalone@devalone shell-script]$ rpm -qa | sort | less
    [devalone@devalone shell-script]$ rpm -qa | sort > rpm.list
    [devalone@devalone shell-script]$ less rpm.list
    aajohan-comfortaa-fonts-2.004-6.fc24.noarch
    abattis-cantarell-fonts-0.0.24-1.fc24.noarch
    abrt-2.8.2-1.fc24.x86_64
    abrt-addon-ccpp-2.8.2-1.fc24.x86_64
    abrt-addon-coredump-helper-2.8.2-1.fc24.x86_64
    abrt-addon-kerneloops-2.8.2-1.fc24.x86_64
    abrt-addon-pstoreoops-2.8.2-1.fc24.x86_64
    abrt-addon-python3-2.8.2-1.fc24.x86_64
    abrt-addon-vmcore-2.8.2-1.fc24.x86_64
    abrt-addon-xorg-2.8.2-1.fc24.x86_64
    abrt-cli-2.8.2-1.fc24.x86_64
    abrt-dbus-2.8.2-1.fc24.x86_64
    abrt-desktop-2.8.2-1.fc24.x86_64
    abrt-gui-2.8.2-1.fc24.x86_64
    abrt-gui-libs-2.8.2-1.fc24.x86_64
    ...

rpm.list文件中的數據現在已經排好序了。


1.7 執行數學運算
-----------------------------------------------------------------------------------------------------------------------------------------
另一個對任何編程語言都很重要的特性是操作數字的能力。遺憾的是,對 shell 腳本來說,這個處理過程會比較麻煩。在 shell 腳本中有兩種途徑來進行
數學運算。


1.7.1 expr 命令
-----------------------------------------------------------------------------------------------------------------------------------------
最開始,Bourne shell 提供了一個特別的命令用來處理數學表達式。expr 命令允許在命令行上處理數學表達式,但是特別笨拙。

    [devalone@devalone shell-script]$ expr 1 + 5
    6

expr 命令能夠識別少數的數學和字符串操作符,見下表:

    expr 命令操作符
    +---------------------------+-----------------------------------------------------------------------
    | 操 作 符                    | 描 述
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 | ARG2                | 如果ARG1既不是null也不是零值,返回ARG1;否則返回ARG2
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 & ARG2                | 如果沒有參數是null或零值,返回ARG1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 < ARG2                | 如果ARG1小於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 <= ARG2                | 如果ARG1小於或等於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 = ARG2                | 如果ARG1等於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 != ARG2                | 如果ARG1不等於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 >= ARG2                | 如果ARG1大於或等於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 > ARG2                | 如果ARG1大於ARG2,返回1;否則返回0
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 + ARG2                | 返回ARG1和ARG2的算術運算和
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 - ARG2                | 返回ARG1和ARG2的算術運算差
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 * ARG2                | 返回ARG1和ARG2的算術乘積
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 / ARG2                | 返回ARG1被ARG2除的算術商
    +---------------------------+-----------------------------------------------------------------------
    | ARG1 % ARG2                | 返回ARG1被ARG2除的算術餘數
    +---------------------------+-----------------------------------------------------------------------
    | STRING : REGEXP            | 如果REGEXP匹配到了STRING中的某個模式,返回該模式匹配
    +---------------------------+-----------------------------------------------------------------------
    | match STRING REGEXP        | 如果REGEXP匹配到了STRING中的某個模式,返回該模式匹配
    +---------------------------+-----------------------------------------------------------------------
    | substr STRING POS LENGTH    | 返回起始位置爲POS(從1開始計數)、長度爲LENGTH個字符的子字符串        
    +---------------------------+-----------------------------------------------------------------------
    | index STRING CHARS        | 返回在STRING中找到CHARS字符串的位置;否則,返回0
    +---------------------------+-----------------------------------------------------------------------
    | length STRING                | 返回字符串STRING的數值長度
    +---------------------------+-----------------------------------------------------------------------
    | + TOKEN                    | 將TOKEN解釋成字符串,即使是個關鍵字
    +---------------------------+-----------------------------------------------------------------------
    | (EXPRESSION)                | 返回EXPRESSION的值
    +---------------------------+-----------------------------------------------------------------------
    
expr 支持模式匹配和字符串操作。字符串表達式的優先級高於數值表達式和邏輯關係表達式。
 
'STRING : REGEX'
     執行模式匹配。兩端參數會轉換爲字符格式,且第二個參數被視爲正則表達式(GNU基本正則),它默認會隱含前綴"^"。隨後將第一個參數和正則模式做匹配。
 
     如果匹配成功,且REGEX使用了'\('和'\)',則此表達式返回匹配到的,如果未使用'\('和'\)',則返回匹配的字符數。
 
     如果匹配失敗,如果REGEX中使用了'\('和'\)',則此表達式返回空字符串,否則返回爲0。
 
     只有第一個'\(...\)'會引用返回的值;其餘的'\(...\)'只在正則表達式分組時有意義。
 
     在正則表達式中,'\+','\?'和'\|'分表代表匹配一個或多個,0個或1個以及兩端任選其一的意思。
 
'match STRING REGEX'
     等價於'STRING : REGEX'。
 
'substr STRING POSITION LENGTH'
     返回STRING字符串中從POSITION開始,長度最大爲LENGTH的子串。如果POSITION或LENGTH爲負數,0或非數值,則返回空字符串。
 
'index STRING CHARSET'
     CHARSET中任意單個字符在STRING中最前面的字符位置。如果在STRING中完全不存在CHARSET中的字符,則返回0。見後文示例。
    
'length STRING'
     返回STRING的字符長度。
 
'+ TOKEN'
     將TOKEN解析爲普通字符串,即使TOKEN是像MATCH或操作符"/"一樣的關鍵字。這使得'expr length + "$x"'或'expr + "$x" : '.*/\(.\)''可以正常被測試,即使"$x"的值可能是'/'或'index'關鍵字。這個操作符是一個GUN擴展。
     通用可移植版的應該使用'" $token" : ' \(.*\)''來代替'+ "$token"'。
 
   要讓expr將關鍵字解析爲普通的字符,必須使用引號包圍。
   

儘管標準操作符在 expr 命令中工作得很好,但在腳本或命令行上使用它們時仍有問題出現。許多 expr 命令操作符在shell中另有含義(比如星號)。
當它們出現在在 expr 命令中時,會得到一些詭異的結果。

示例:
    [devalone@devalone shell-script]$ expr 5 * 2
    expr: 語法錯誤

要解決這個問題,對於那些容易被 shell 錯誤解釋的字符,在它們傳入 expr 命令之前,需要使用 shell 的轉義字符(反斜線)將其標出來。

    [devalone@devalone shell-script]$ expr 5 \* 2
    10

在shell腳本中使用expr命令也同樣複雜:

    [devalone@devalone shell-script]$ cat test6
    #!/bin/bash
    # An example of using the expr command
    var1=10
    var2=20
    var3=$(expr $var2 / $var1)
    echo The result is $var3

要將一個數學算式的結果賦給一個變量,需要使用命令替換來獲取 expr 命令的輸出:

運行:
    [devalone@devalone shell-script]$ test6
    The result is 2


1.7.2 使用方括號[]
-----------------------------------------------------------------------------------------------------------------------------------------
bash shell 爲了保持跟 Bourne shell的兼容而包含了 expr 命令,但它同樣也提供了一種更簡單的方法來執行數學表達式。在 bash 中,在將一個數學運算
結果賦給某個變量時,可以用美元符和方括號($[ operation ])將數學表達式圍起來。

示例:
    [devalone@devalone shell-script]$ var1=$[1 + 5]
    [devalone@devalone shell-script]$ echo $var1
    6
    [devalone@devalone shell-script]$ var2=$[$var1 * 2]
    [devalone@devalone shell-script]$ echo $var2
    12

用方括號執行 shell 數學運算比用 expr 命令方便很多。這種技術也適用於 shell 腳本。

示例:
    [devalone@devalone shell-script]$ cat test7.sh
    #!/bin/bash
    var1=100
    var2=50
    var3=45
    var4=$[$var1 * ($var2 - $var3)]
    echo The final result is $var4

運行:
    [devalone@devalone shell-script]$ chmod a+x test7.sh
    [devalone@devalone shell-script]$ test7.sh
    The final result is 500

在使用方括號來計算公式時,不用擔心 shell 會誤解乘號或其他符號。shell 知道它不是通配符,因爲它在方括號內。

在 bash shell腳本中進行算術運算會有一個主要的限制,bash shell數學運算符只支持整數運算。若要進行任何實際的數學計算,這是一個巨大的限制。

示例:

    [devalone@devalone shell-script]$ cat test8.sh
    #!/bin/bash
    var1=100
    var2=45
    var3=$[$var1 / $var2]
    echo The final result is $var3

運行:
    [devalone@devalone shell-script]$ test8.sh
    The final result is 2

    
    NOTE:
    -------------------------------------------------------------------------------------------------------------------------------------
    z shell(zsh)提供了完整的浮點數算術操作。如果需要在shell腳本中進行浮點數運算,可以考慮看看z shell
    
    
1.7.3 浮點解決方案
-----------------------------------------------------------------------------------------------------------------------------------------
有幾種解決方案能夠克服 bash 中數學運算的整數限制。最常見的方案是用內建的 bash 計算器,叫作bc。


■ bc 的基本用法
-----------------------------------------------------------------------------------------------------------------------------------------
bash 計算器實際上是一種編程語言,它允許在命令行中輸入浮點表達式,然後解釋並計算該表達式,最後返回結果。bash 計算器能夠識別:

    □ 數字(整數和浮點數)
    □ 變量(簡單變量和數組)
    □ 註釋(以#或C語言中的/* */開始的行)
    □ 表達式
    □ 編程語句(例如if-then語句)
    □ 函數

可以在 shell 提示符下通過 bc 命令訪問 bash 計算器:

    [devalone@devalone shell-script]$ bc
    bc 1.06.95
    Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
    This is free software with ABSOLUTELY NO WARRANTY.
    For details type `warranty'.
    12 * 5.4
    64.8
    3.156 * (3 + 5)
    25.248
    quit

要退出 bash 計算器,必須輸入 quit。

浮點運算是由內建變量 scale 控制的。必須將這個值設置爲希望在計算結果中保留的小數位數,否則無法得到期望的結果。

示例:

    [devalone@devalone shell-script]$ bc -q
    3.44 / 5
    0
    scale=4
    3.44/5
    .6880
    quit

scale 變量的默認值是 0。在 scale 值被設置前,bash 計算器的計算結果不包含小數位。在將其值設置成 4 後,bash 計算器顯示的結果包含四位小數。
-q 命令行選項可以不顯示 bash 計算器冗長的歡迎信息。

除了普通數字,bash 計算器還能支持變量。

示例:
    [devalone@devalone shell-script]$ bc -q
    var1=10
    var1 * 4
    40
    var2=var1 / 5
    print var2
    2
    quit

變量一旦被定義,就可以在整個 bash 計算器會話中使用該變量了。print 語句允許打印變量和數字。

 

■ 在腳本中使用 bc
-----------------------------------------------------------------------------------------------------------------------------------------
可以用命令替換運行 bc 命令,並將輸出賦給一個變量。基本格式如下:

    variable=$(echo "options; expression" | bc)

第一部分 options 允許設置變量。如果需要不止一個變量,可以用分號將其分開。expression 參數定義了通過 bc 執行的數學表達式。

示例:

    [devalone@devalone shell-script]$ cat test9.sh
    #!/bin/bash
    var1=$(echo "scale=4; 3.44/5" | bc)
    echo The answer is $var1

運行:
    [devalone@devalone shell-script]$ test9.sh
    The answer is .6880

示例:
    [devalone@devalone shell-script]$ cat test10.sh
    #!/bin/bash
    var1=100
    var2=45
    var3=$(echo "scale=4; $var1/$var2" | bc)
    echo The answer for this is $var3

運行:
    [devalone@devalone shell-script]$ test10.sh
    The answer for this is 2.2222

當然,一旦變量被賦值,那個變量也可以用於其他運算。

示例:
[devalone@devalone shell-script]$ cat test11.sh
#!/bin/bash
var1=20
var2=3.14159
var3=$(echo "scale=4; $var1 * $var1" | bc)
var4=$(echo "scale=4; $var3 * $var2" | bc)
echo The final result is $var4

輸出:
    [devalone@devalone shell-script]$ test11.sh
    The final result is 1256.63600

這個方法適用於較短的運算,但有時會涉及更多的數字。如果需要進行大量運算,在一個命令行中列出多個表達式就會有點麻煩。

有一個方法可以解決這個問題。bc 命令能識別輸入重定向,允許將一個文件重定向到 bc 命令來處理。但這同樣會叫人頭疼,因爲還得將表達式存放到
文件中。


最好的辦法是使用內聯輸入重定向,它允許直接在命令行中重定向數據。在 shell 腳本中,可以將輸出賦給一個變量。
如下形式:

    variable=$(bc << EOF
    options
    statements
    expressions
    EOF
    )

EOF 文本字符串標識了內聯重定向數據的起止。記住,仍然需要命令替換符號將 bc 命令的輸出賦給變量。

現在可以將所有 bash 計算器涉及的部分都放到同一個腳本文件的不同行。

示例:
    
    [devalone@devalone shell-script]$ cat test12.sh
    #!/bin/bash
    var1=10.46
    var2=43.67
    var3=33.2
    var4=71

    var5=$(bc <<EOF
    scale=4
    al=($var1 * $var2)
    b1=($var3 * $var4)
    a1+b1
    EOF
    )

    echo The final answer for this mess is $var5

運行:
    [devalone@devalone shell-script]$ test12.sh
    The final answer for this mess is 2357.2


將選項和表達式放在腳本的不同行中可以讓處理過程變得更清晰,提高易讀性。EOF 字符串標識了重定向給 bc 命令的數據的起止。當然,必須用命令替換符號
標識出用來給變量賦值的命令。

還會注意到,在這個例子中,可以在 bash 計算器中賦值給變量。這一點很重要:在 bash 計算器中創建的變量只在 bash 計算器中有效,不能在 shell
腳本中使用。


1.8 退出腳本
-----------------------------------------------------------------------------------------------------------------------------------------
shell 中運行的每個命令都使用退出狀態碼(exit status)告訴 shell 它已經運行完畢。退出狀態碼是一個 0~255 的整數值,在命令結束運行時由命令
傳給 shell。可以捕獲這個值並在腳本中使用。


1.8.1 查看退出狀態碼
-----------------------------------------------------------------------------------------------------------------------------------------
Linux 提供了一個專門的變量 $? 來保存上個已執行命令的退出狀態碼。對於需要進行檢查的命令,必須在其運行完畢後立刻查看或使用 $? 變量。它的值
會變成由 shell 所執行的最後一條命令的退出狀態碼。

示例:
    [devalone@devalone shell-script]$ who
    devalone pts/0        2018-07-05 11:05 (192.168.1.101)
    [devalone@devalone shell-script]$ echo $?
    0

一個成功結束的命令的退出狀態碼是 0。如果一個命令結束時有錯誤,退出狀態碼就是一個正數值。

示例:
    [devalone@devalone shell-script]$ zzzz
    bash: zzzz: 未找到命令...
    [devalone@devalone shell-script]$ echo $?
    127

無效命令會返回一個退出狀態碼 127。Linux 錯誤退出狀態碼沒有什麼標準可循,但有一些可用的參考,如下表:

        Linux 退出狀態碼
        +-----------+--------------------------------
        | 狀態碼    | 描 述
        +-----------+--------------------------------
        | 0            | 命令成功結束
        +-----------+--------------------------------
        | 1            | 一般性未知錯誤
        +-----------+--------------------------------
        | 2            | 不適合的shell命令
        +-----------+--------------------------------
        | 126        | 命令不可執行
        +-----------+--------------------------------
        | 127        | 沒找到命令
        +-----------+--------------------------------
        | 128        | 無效的退出參數
        +-----------+--------------------------------
        | 128+x        | 與Linux信號x相關的嚴重錯誤
        +-----------+--------------------------------
        | 130        | 通過Ctrl+C終止的命令
        +-----------+--------------------------------
        | 255        | 正常範圍之外的退出狀態碼        
        +-----------+--------------------------------
        
        
退出狀態碼 126 表明用戶沒有執行命令的正確權限:

    [devalone@devalone shell-script]$ testfile
    -bash: ./testfile: Permission denied
    [devalone@devalone shell-script]$ echo $?
    126

另一個會碰到的常見錯誤是給某個命令提供了無效參數:

    [devalone@devalone shell-script]$ date %t
    date: 無效的日期"%t"
    [devalone@devalone shell-script]$ echo $?
    1

    
1.8.2 exit 命令
-----------------------------------------------------------------------------------------------------------------------------------------
默認情況下,shell腳本會以腳本中的最後一個命令的退出狀態碼退出。可以改變這種默認行爲,返回自己的退出狀態碼。exit 命令允許在腳本結束時指定
一個退出狀態碼。

示例:
    [devalone@devalone shell-script]$ cat test13.sh
    #!/bin/bash
    # testing the exit status

    var1=10
    var2=30
    var3=$[$var1 + $var2]
    echo The answer is $var3
    exit 5
    
運行:
    [devalone@devalone shell-script]$ test13.sh
    The answer is 40
    [devalone@devalone shell-script]$ echo $?
    5

    
也可以在exit命令的參數中使用變量:

    [devalone@devalone shell-script]$ cat test14.sh
    #!/bin/bash
    # testing the exit status
    var1=10
    var2=30
    var3=$[$var1 + $var2]
    exit $var3

運行:
    [devalone@devalone shell-script]$ test14.sh
    [devalone@devalone shell-script]$ echo $?
    40

要注意這個功能,退出狀態碼最大隻能 255,如果超出 255,shell 通過模運算得到這個結果。一個值的模就是被除後的餘數。最終的結果是指定的數值
除以 256 後得到的餘數:

示例:
    [devalone@devalone shell-script]$ cat test14b.sh
    #!/bin/bash
    # testing the exit status
    var1=10
    var2=30
    var3=$[$var1 * $var2]
    echo The value is $var3
    exit $var3

運行:
    [devalone@devalone shell-script]$ test14b.sh
    The value is 300
    [devalone@devalone shell-script]$ echo $?
    44

系列目錄:

    Linux shell 腳本編程-基礎篇 (一)

    Linux shell 腳本編程-基礎篇 (二)

    Linux shell 腳本編程-基礎篇 (三)

    Linux shell 腳本編程-基礎篇 (四)

    Linux shell 腳本編程-基礎篇 (五)

    Linux shell 腳本編程-基礎篇 (六)

 

 

-----------------------------------------------------------------------------------------------------------------------------------------
參考:

    《Linux 命令行與 shell 腳本編程大全》 第 3 版 —— 2016.8(美)Richard Blum  Cristine Bresnahan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章