--- 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" ``` >  * 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." } ``` >  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 ``` >  ### 函數遞迴 * 遞迴就是函數呼叫自身多次,概念程式如下 ```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" ``` >  ## Shell 函數:返回值 **bash shell 會把函數當作一個小型腳本**,**函數運行完後會 返回一個 ++退出狀態碼++**(返回給父進程);函數以下有種方式產生退出狀態碼 ### 取得默認退出狀態碼:「`$?`」 * Shell 腳本中的函數,退出狀態碼是函數最後一條命令返回的退出碼 :::success * 函數執行結束後可以使用 `$?` 來取得最後的狀態碼 ::: ```shell= #! /bin/bash func() { echo "Hello function" } echo "Start..." func echo "Finish... exit code: $?" ``` >  :::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: $?" ``` >  * 當然 **並非所有返回非 `0` 都代表命令錯誤**! 像是 `grep` 如果匹配到數據返回 0,沒匹配到則返回 1;但指令仍算是正確執行(因為語法沒有錯誤,只是沒有批配到) > 最準確的還是使用 `man` 查看所需指令的返回意義 ```shell= # 有該字面 grep 'Hello' tempFile # 返回 0 echo $? # 無該字面 grep 'World' tempFile # 返回 1 echo $? ``` >  ::: ### 使用 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 "$?" ``` >  :::warning * **返回的整數範圍是 `0` ~ `255` 之間,如果超過則是返回值除以 256 取餘數**(MOD) >  ::: ### 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 **用這個方法就不會限定於一定要數字,可以返回字串、浮點數** ::: >  ## 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" ``` >  * 另外 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 ``` >  * **修正後的範例**:透過手動將參數傳入(`$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 ``` >  ### 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" ``` 可以看到,函數內操控全域變數會影響外部 >  * **局部變量關鍵字 `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" ``` >  ## 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 的元素! ::: >  * 解決方式:**`分批傳入`、`重組`** 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 仍錯誤 > >  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[*]} ``` >  ### 函數返回數組 * 如果 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[*]}" # 解構陣列 ``` >  ## 命令行中使用函數 Shell 函數不一定要在腳本內創建,也可以在 CLI 命令行創建並使用 ### 命令行中創建 * Shell 會解釋用戶輸入的命令,所以可以直接在命令行中定義;有兩個方案可用(重點是花括號 `{ }`) 1. **單行定義**:操作如同在 Shell 中定義參數,定義出來後在該 Shell 進程內都可以使用(如果需要給 sub shell 使用記得 `export`) ```shell= # 每個命令後後記得 `;` function add { echo $[ $1 + $2]; } # 呼叫 Function add 100 200 ``` >  2. **多行定義**:多行定義使用 `{` 開頭,並使用 `}` 結尾;在這 花括號`{ }`中間我們可以定義多個命令 ```shell= function double { read -p "Enter value: " value echo $[ $value * 2] } ``` >  :::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 ``` >  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 ``` >  ## 腳本庫創建、使用 庫(`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)" ``` >  ### 下載 & 建構 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 ``` >  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 ``` >  ### 使用 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 ``` >  ## Appendix & FAQ :::info ::: ###### tags: `Linux Shell`
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up