---
# System prepended metadata

title: 建構腳本 - 退出碼、函數、庫
tags: [Linux Shell]

---

---
title: '建構腳本 - 退出碼、函數、庫'
disqus: kyleAlien
---

建構腳本 - 退出碼、函數、庫
===

## Overview of Content

Shell 也可以創建函數（像 C 語言），讓同段代碼被重複利用

:::success
* 如果喜歡讀更好看一點的網頁版本，可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
    
    本篇文章對應的是 [**探索 Shell 函數與腳本庫 | Shell 結構化 ＆ 函數**](https://devtechascendancy.com/shell-script-functions/)
:::
[TOC]

## Shell 函數定義

函數是一個腳本代碼區塊，可以放置在腳本任意位置； Shell 函數有兩種格式（都可以）如下

```shell=
# 1. 第一種
function name {
    # other commands
}

# ------------------------------------
# 2. 第二種
name() {
    # other commands
}
```

其中 name 是每個函數的名稱，呼叫函數時需使用 name 呼叫，它也是必須為 **腳本內唯一名稱**（不可重複）

### 創建、呼叫函數 - 範例

* 使用兩種不同格式創建函數

    ```shell=
    #! /bin/bash

    # 格式 1
    function func1 {
        echo "Hello, I am function 1."
    }

    # 格式 2
    func2() {
        echo "Function 2 here."
    }

    echo "Before call the function"

    func1

    func2

    echo "After call the function"
    ```

    > ![](https://hackmd.io/_uploads/BkhCg0LL2.png)

* Shell 呼叫函數還有幾個特性如下
    
    1. **順序性**：同命令式設計的 C 語言，強調順序性，如果呼叫函數時，該函數尚未定義，則會出錯（`command not found` 錯誤）
    
        在尚未定義函數時，呼叫函數的（順序）錯誤範例…

        ```shell=
        #! /bin/bash

        echo "Before call the function"

        func2

        echo "After call the function"

        func2() {
            echo "Function 2 here."
        }
        ```
        > ![](https://hackmd.io/_uploads/H1YymALL2.png)

    2. **可覆蓋性**：如果出現相同命名的函數，那會出現 **後蓋前** 的問題（因為分析的順序是由上至下…）

        ```shell=
        #! /bin/bash

        func2() {
            echo "Function 2 here."
        }

        echo "Before redefined func."
        func2

        function func2 {
            echo "What up man~"
        }

        echo "After redefined func."
        func2
        ```
        > ![](https://hackmd.io/_uploads/H1g_E0UI2.png)



### 函數遞迴

* 遞迴就是函數呼叫自身多次，概念程式如下

    ```shell=
    function MyFuncion {
        MyFunction
    }
    ```
    
    :::danger
    * **遞迴是邏輯複雜、又危險的**？
        
        遞迴的優點在於程式碼的複用，但相對起來確實是邏輯較為複雜、當操作不慎時確實危險（容易造成無窮迴圈）；但是相對來說，它也是一種「**高內聚**」的表現，它會把所有邏輯集中到一處
        
        > 當每次操作時我們都必須判斷何時該停止遞迴，並請再遞迴前也須改變傳入的參數
    :::

* 能夠體現遞迴最經典的就是 **階乘**（`factorial`），階乘的概念如下

    ```shell=
    # factorial 概念
    N! = N * (N-1)!

    # 舉例
    5! = 5 * (4)!
    5 = 5 * 4 * 3 * 2 * 1
    ```
    
    Shell 函數可以使用遞迴，接下來我們就使用 Shell Function 來達成遞迴計算

    ```shell=
    # !/bin/bash

    function factorial {
        local curValue=$1

        if [ $curValue -eq 1 ] ; then
            #透過 echo 方式回傳
            echo 1
        else 
            # 記得修改傳入變數！這邊是將 curValue 數值做計算（減一）
            local tmp=$[ $curValue - 1 ]
            # 遞迴呼叫
            local res=$(factorial $tmp)
            echo $[ $res * $1 ]
        fi
    }

    read -p "Enter value: " value

    finalRes=$(factorial $value)
    echo "The factorial of $value is $finalRes"
    ```

    > ![](https://hackmd.io/_uploads/H1n3035Lh.png)



## Shell 函數：返回值

**bash shell 會把函數當作一個小型腳本**，**函數運行完後會 返回一個 ++退出狀態碼++**（返回給父進程）；函數以下有種方式產生退出狀態碼

### 取得默認退出狀態碼：「`$?`」

* Shell 腳本中的函數，退出狀態碼是函數最後一條命令返回的退出碼

    :::success
    * 函數執行結束後可以使用 `$?` 來取得最後的狀態碼
    :::

    ```shell=
    #! /bin/bash

    func() {
        echo "Hello function"
    }

    echo "Start..."

    func

    echo "Finish... exit code: $?"
    ```
    > ![](https://hackmd.io/_uploads/BkaftRUU2.png)
    
    :::danger
    * 以默認最後一行命令的作為退出碼的方案有些危險，有可能在函數中，其他指令出錯，它也會以最後一行退出碼為主（沒有好好處理，會不容易找到 Bug 的根源）

        ```shell=
        #! /bin/bash

        func() {
            # The file 123 is not exist.
            ls -laF 123
            echo "Hello function"
        }

        echo "Start..."

        func

        echo "Finish... exit code: $?"
        ```
        > ![](https://hackmd.io/_uploads/HkBhFRUI2.png)
    
    * 當然 **並非所有返回非 `0` 都代表命令錯誤**！
        
        像是 `grep` 如果匹配到數據返回 0，沒匹配到則返回 1；但指令仍算是正確執行（因為語法沒有錯誤，只是沒有批配到）

        > 最準確的還是使用 `man` 查看所需指令的返回意義
        
        ```shell=
        # 有該字面
        grep 'Hello' tempFile
        
        # 返回 0
        echo $?
        
        # 無該字面
        grep 'World' tempFile
        
        # 返回 1
        echo $?
        ```
    
        > ![](https://hackmd.io/_uploads/SJWxo94sh.png)

    :::

### 使用 return 命令

* Bash shell 允許我們使用 **return 命令指定一個「整數」來作為退出碼**，這時使用 `$?` 符號，就可以取得指定的退出碼；其格式如下

    ```shell=
    return [整數]
    ```
    
    使用 `return` 命令的範例如下
    
    ```shell=
    #!/bin/bash

    func() {
        # 讀取輸入
        read -p "Enter a value: " value
            echo "The double value..."

        return $(( value * 2 ))

        # 這行不會被執行到
        echo "After return."
    }

    # 呼叫函數
    func

    echo "$?"
    ```
    > ![](https://hackmd.io/_uploads/H1tB3R8I2.png)

    :::warning
    * **返回的整數範圍是 `0` ~ `255` 之間，如果超過則是返回值除以 256 取餘數**（MOD）
        
        > ![](https://hackmd.io/_uploads/rJZOnCLUn.png)
    :::
    
### echo 函數輸出：使用「`$()`」

* 如同把命令的輸出存到一個變量一樣，我們也可以 **++透過 `$()` 符號++** 來呼叫函數，並把函數最終結果賦予到 Shell 變量中（使用 `$()` 接收到返回值）

    ```shell=
    #!/bin/bash

    function myFunc {
        read -p "Enter a value: " value
        echo $[ $value * 2 ]
    }

    # 使用 $() 符號
    result=$(myFunc)

    echo "The new value is $result"
    ```

    :::success
    **用這個方法就不會限定於一定要數字，可以返回字串、浮點數**
    :::
    
    > ![](https://hackmd.io/_uploads/rJDd7mPI2.png)


## Shell 函數的參數、變量

定義 Shell 變量是腳本中容易出錯的部分


### 傳遞參數給函數

* **Shell 函數無法直接接收參數**，它是透過幾個特殊的環境變量來取得傳入的參數（完整請參考 [**建構腳本 - 特殊參數、互動輸入**](https://devtechascendancy.com/shell-variables_environment-shell-loading/)）

    | 特殊變量 | 說明 |
    | -------- | -------- |
    | `$#` | 取得參數總數 |
    | `$*` | 取得全部傳入的參數（整體） |
    | `$@` | 取得全部傳入的參數（個別分開） |
    | `$0` | 腳本自身名稱（特殊的參數，位置 `0` 都是腳本名稱） |
    | `$1` ~ `$n` | 取得參數 1 ~ n |

    ```shell=
    #! /bin/bash

    # 定義 add 函數
    function add {
        # 判斷參數數量
        case $# in

        1) 
            echo $[ $1 + $1 ] ;;
        2) 
            echo $[ $1 + $2 ];;
        *) 
            echo "Invalid input";;

        esac
    }

    # 對 add 函數傳入變數 1
    res=$(add 1)
    echo "1 + 1: $res" 


    # 對 add 函數傳入變數 4、7
    res=$(add 4 7)
    echo "4 + 7: $res"

    res=$(add)
    echo "--: $res"
    ```
    > ![](https://hackmd.io/_uploads/S1BDi7vUh.png)

* 另外 Shell 函數有個特點，它會隔離在自己的小世界中，**無法接收到腳本外部傳入的參數**；範例如下

    * **錯誤範例**：由於函數無法接收到外部腳本的參數（`$1`、`$2`），所以在函數中取外部變數，就會判斷錯誤
    
        ```shell=
        #! /bin/bash

        function add {
            case $# in

            1) 
                echo $[ $1 + $1 ] ;;    # 無法獲取腳本自身的參數
            2) 
                echo $[ $1 + $2 ] ;;    # 無法獲取腳本自身的參數
            *) 
                echo "Invalid input" ;;

            esac
        }

        if [ $# -eq 2 ] ; then
            # 錯誤點：這時呼叫時沒有傳遞參數
            value=$(add)
            echo "result: $value"
        fi
        ```

        > ![](https://hackmd.io/_uploads/BJ7mpXPUh.png)

    * **修正後的範例**：透過手動將參數傳入（`$1`、`$2`），函數就可以正常運作

        ```shell=
        #! /bin/bash

        function add {
            case $# in

            1) 
                echo $[ $1 + $1 ] ;;       # 獲取呼叫函數時的參數（不是獲取呼叫腳本時的參數）
            2) 
                echo $[ $1 + $2 ] ;;
            *) 
                echo "Invalid input" ;;

            esac
        }

        if [ $# -eq 2 ] ; then
            # 修正：手動傳入參數
            value=$(add $1 $2)
            echo "result: $value"
        fi
        ```

        > ![](https://hackmd.io/_uploads/HkOVpXDL3.png)


### Shell 函數中的變量：區域變數 `local`

* 如同我們在使用其他程式語言（C, C++, Java...）開發一樣，變量基本有分為兩種「全局變量」、「區域變量」
    
    * **全局變量**：函數內、函數外都可以使用
        
        :::warning
        這種方案你要清楚知道該全局變量在哪裡被調用，哪裡可能有做值得修改、讀取，才能安全使用
        :::

        ```shell=
        #! /bin/bash

        globalVar=100

        function myFunc {
            (( globalVar -= 89 ))
            echo "func var is $globalVar"
        }

        echo "Before call func, external var is $globalVar"

        myFunc 

        echo "Before call func, external var is $globalVar"
        ```
        可以看到，函數內操控全域變數會影響外部
        > ![](https://hackmd.io/_uploads/rkGdbVDIh.png)

    * **局部變量關鍵字 `local`**：只有函數內可用，在 Shell 函數中要加上 `local` 關鍵字（如果與全域參數名相同，它會覆蓋全域參數，直到離開局部區域）

        ```shell=
        #! /bin/bash

        globalVar=100

        function myFunc {
            # 差異在這，宣告 local 參數
            # global 沒有定義，預設為 0（隱式宣告）
            local globalVar=$[ $global - 89 ]

            echo "func var is $globalVar"
        }

        echo "Before call func, external var is $globalVar"

        myFunc 

        echo "Before call func, external var is $globalVar"

        ```
        > ![](https://hackmd.io/_uploads/HJkGMEDU3.png)

## Shell 函數：傳入、返回數組


### 函數傳入數組

* 如果你傳遞的變數（`Array`）是數組變量，那要 **做特別處理**，否則函數內部只能收到第一個變量（更多數組操作，請看 [**Shell 全局及區域變數、特殊變數及環境變數 | Shell 啟動順序 | Array 變數**](https://devtechascendancy.com/shell-variables_environment-shell-loading/)）

    * 對函數傳入數組的問題點，範例如下

        ```shell=
        #! /bin/bash

        function myFunc {
            echo "Function get array: $@"
            getArray=$1
            echo "Receive array is: ${getArray[*]}"
        }

        myArray=(1 2 3 4 5 6)
        echo "External array is: ${myArray[*]}"

        myFunc $myArray
        ```
        
        :::danger
        可以看到函數內部只接收到一個元素，而並非整個 Array 的元素！
        :::

        > ![](https://hackmd.io/_uploads/HJMqt4wU3.png)

    * 解決方式：**`分批傳入`、`重組`**

        1. **分批傳入**：傳入參數的方式不對，我們必須使用 `$Array[*]` 的方式將參數傳入（這個行爲就像是為將陣列解構）

            ```shell=
            #! /bin/bash

            function myFunc {
                echo "Function get array: $@"
                getArray=$1
                echo "Receive array is: ${getArray[*]}"
            }

            myArray=(1 2 3 4 5 6)
            echo "External array is: ${myArray[*]}"

            # 修正點
            myFunc ${myArray[*]}
            ```
            > 我們可以看到分批傳入成功，但函數接收全部 Array 仍錯誤
            > 
            > ![](https://hackmd.io/_uploads/r14znNP8n.png)

        2. **重組**：透過 `("$@")` 方式重組 Array 的元素

            ```shell=
            #! /bin/bash

            function myFunc {
                echo "Function get array: $@"
                
                # 修正點
                getArray=("$@")
                echo "Receive array is: ${getArray[*]}"
            }

            myArray=(1 2 3 4 5 6)
            echo "External array is: ${myArray[*]}"

            myFunc ${myArray[*]}
            ```
            > ![](https://hackmd.io/_uploads/HkNinEP8n.png)


### 函數返回數組

* 如果 Shell 函數要返回數組，那可以將 ^1.^**數組分批傳出**（`${Array[*]}`）再 ^2.^**透過 echo 命令作為函數的返回**

    ```shell=
    #! /bin/bash

    function getArray {
        local array
        array=(1 2 3 4 5)
        echo "${array[*]}"
    }

    res=($(getArray))

    echo "external get array: $res"
    echo "external get array: ${res[*]}"        # 解構陣列
    ```
    > ![](https://hackmd.io/_uploads/ryXjxSPIn.png)

## 命令行中使用函數

Shell 函數不一定要在腳本內創建，也可以在 CLI 命令行創建並使用

### 命令行中創建

* Shell 會解釋用戶輸入的命令，所以可以直接在命令行中定義；有兩個方案可用（重點是花括號 `{ }`）

    1. **單行定義**：操作如同在 Shell 中定義參數，定義出來後在該 Shell 進程內都可以使用（如果需要給 sub shell 使用記得 `export`）

        ```shell=
        # 每個命令後後記得 `;`
        function add { echo $[ $1 + $2]; }

        # 呼叫 Function
        add 100 200
        ```
        > ![](https://hackmd.io/_uploads/Bydfvp9L2.png)


    2. **多行定義**：多行定義使用 `{` 開頭，並使用 `}` 結尾；在這 花括號`{ }`中間我們可以定義多個命令

        ```shell=
        function double {
            read -p "Enter value: " value
            echo $[ $value * 2]
        }
        ```
        > ![](https://hackmd.io/_uploads/Sk4QPaqU3.png)

        :::warning
        * **在命令行創建函數有幾個注意點**
            
            1. 如果在同一個 Shell 中定義同名函數，仍會後蓋前
            
            2. Shell 結束，則命令行函數也會消失
        :::
        
### `.bashrc` 中創建函數

* 在 `.bashrc` 中創建函數之後，在每個命令行中都可以使用；以下我們在 `.bashrc` 之後創建函數（**不要刪除 `.bashrc` 中任何資料**）

    1. **在 `.bashrc` 中創建函數**
    
        ```shell=
        # 添加在最後一行
        function showTime {
            echo -n "Hello $USER, current time is "
            date
        }
        ```
        
        執行測試（以下在同個 Shell 中執行函數）
        
        ```shell=
        # 重新加載檔案
        source ~/.bashrc
        
        showTime
        ```
        > ![](https://hackmd.io/_uploads/ByWccT582.png)

    2. **在 `.bashrc` 中加載腳本庫**：使用 source 命令做函數加載
        
        > 可以先看下面 `創建庫 Library` 小節

        1. 首先創建一個腳本庫：以下創建名為 `showTimeLib.sh` 的腳本庫

            ```shell=
            function showTime2 {
                echo -n "Hello $USER, current time is "
                date
            }
            ```
            
        2. **設定腳本庫加載進 `.bashrc`**：**這邊請記得使用全路徑！**

            * 編輯 `.bashrc`
                
                ```shell=
                # 使用 source + 使用全路徑
                source /home/alien/Desktop/shell/chapter_17/showTimeLib.sh
                ```
                
            * 測試使用 `.bashrc` 加載的庫
                
                ```shell=
                source ~/.bashrc
                
                showTime2
                ```
                
                > ![](https://hackmd.io/_uploads/H1Yfn6582.png)

## 腳本庫創建、使用

庫（`Library`）可擁有一系列可以重用方法的函數，可以加速開發，而腳本也可以製作腳本庫

### 創建、加載腳本庫 Library：`source`

* 當同一段函數有許多檔案都用的到相同邏輯時，就可以創建一個腳本庫（`shell library`） 將同邏輯的函數放置於其中，在需要時再引入即可；方式如下

    1. **創建腳本庫**：以下會在腳本庫裡面存放共同邏輯的 function

        ```shell=
        # MyLib.sh. library

        function add {
            echo $[ $1 + $2 ]
        }

        function minus {
            echo $[ $1 - $2 ]
        }

        function mul {
            echo $[ $1 * $2 ]
        }

        function div {
            if [ $2 -ne 0 ] ; then 
                echo $[ $1 / $2]
            else 
                echo -1
            fi
        }
        ```
        
    2. **使用 `source` 命令引用庫**：引入後就可以使用庫中的函數，而不需重複創建

        :::success
        * `source` 的使用方式有兩種，如下
            
            ```shell=
            # 使用方式一
            source <檔案>

            # 使用方式二：點操作符（dot operator）
            . <檔案>
            ```
            
            其中 **檔案** 可以是 **相對路徑、絕對路徑**
        :::
        
        ```shell=
        #!/bin/bash

        source ./MyLib.sh

        read -p "Enter 2 value: " val1 val2

        echo "add res: $(add $val1 $val2)"

        echo "minus res: $(minus $val1 $val2)"

        echo "mul res: $(mul $val1 $val2)"

        echo "div res: $(div $val1 $val2)"
        ```
        > ![](https://hackmd.io/_uploads/B1KH7p5U3.png)

### 下載 & 建構 shtool 包

* **GNU `shtool` 庫**：[**GNU shtool**](https://www.gnu.org/software/shtool/) 提供了一些簡單的 shell 腳本函數給我們復用


    1. **下載 shtool 包**
    
        > 下載網址 ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz

        ```shell=
        # 下載
        wget ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz

        # 解壓
        gunzip shtool-2.0.8.tar.gz

        # 解歸檔
        tar -xvf shtool-2.0.8.tar
        ```
        > ![](https://hackmd.io/_uploads/HkVEl09In.png)

    2. **建構 shtool 庫**
        
        shtool 文件必須針對特定的 Linux 環境進行配置，**配置需使用到 `configure`、`make` 命令**
        
        | 命令 | 功能 |
        | -------- | -------- |
        | `configure` | 檢查 shtool 庫文件所必須的應用，當發現所需的工具，它會使用工具路徑修改配置文件 |
        | `make` | 負責建構 shtool 庫，最終將它打包為一個完整的軟體包 |
        |  | `make test` 用於測試軟體包 |
        |  | `make install` 用於安裝包道系統中 |

        > 現在請移動到你解壓後的資料夾中操作

        ```shell=
        cd shtool-2.0.8.tar/
        
        ./configure
        
        make
        
        sudo make install
        ```
        > ![image](https://hackmd.io/_uploads/BJq_1C8gA.png)


### 使用 shtool 庫

* 以下列出 shtool 庫中可用的幾個函數；使用 shtool 指令格式如下

    ```shell=
    shtool [options] [function [options] [args]]
    ```

    | 函數 | 說明 |
    | -------- | -------- |
    | Arx | 創建歸檔文件(包含一些擴展功能) |
    | Echo | 顯示字符串，並提供了一些擴展構件 |
    | fixperm | 改變目錄樹中的文件權限 |
    | install | 安裝腳本或文件 |
    | mdate | 顯示文件或目錄的修改時間 |
    | mkdir | 創建一個或更多目錄 |
    | Mkln | 使用相對路徑創建鏈接 |
    | mkshadow | 創建一棵陰影樹 |
    | move | 帶有替換功能的文件移動 |
    | Path | 處理程序路徑 |
    | platform | 顯示平台標識 |
    | Prop | 顯示一個帶有動畫效果的進度條 |
    | rotate | 轉置日誌文件 |
    | Scpp | 共享的 C 預處理器 |
    | Slo | 根據庫的類別，分離鏈接器選項 |
    | Subst | 使用sed的替換操作 |
    | Table | 以表格的形式顯示由字段分隔(field-separated)的數據 |
    | tarball | 從文件和目錄中創建 tar 文件 |
    | version | 創建版本信息文件 |

* **shtool 使用範例**：**`platform` 顯示版本訊息**

    ```shell=
    #! /bin/bash

    shtool platform
    ```
    > ![](https://hackmd.io/_uploads/B1fBSRcIh.png)


## Appendix & FAQ

:::info
:::

###### tags: `Linux Shell`
