探索 Shell 函數與腳本庫 | Shell 結構化 & 函數

探索 Shell 函數與腳本庫 | Shell 結構化 & 函數

Overview of Content

這篇文章將帶您深入了解Shell腳本中的函數定義、返回值、參數與變量管理,以及如何在命令行中使用函數和建立腳本庫。從創建和呼叫函數到函數遞迴,我們將探索Shell腳本中函數的各個方面

此外,您將學會如何在命令行中創建和使用函數,以及如何管理腳本庫來提高代碼重用性和維護性

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


定義 Shell 函數

Shell 也可以創建函數(像 C 語言),讓同段代碼被重複利用

函數是一個腳本代碼區塊,可以放置在腳本任意位置; Shell 函數有兩種格式(都可以)如下


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

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

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

創建、呼叫函數:範例

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


#! /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 呼叫函數還有幾個特性如下

A. 順序性:同命令式設計的 C 語言,強調順序性,如果呼叫函數時,該函數尚未定義,則會出錯(command not found 錯誤)

在尚未定義函數時,呼叫函數的(順序)錯誤範例…


#! /bin/bash

echo "Before call the function"

func2

echo "After call the function"

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

B. 可覆蓋性:如果出現相同命名的函數,那會出現 後蓋前 的問題(因為分析的順序是由上至下…)


#! /bin/bash

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

echo "Before redefined func."
func2

function func2 {
    echo "What up man~"
}

echo "After redefined func."
func2

函數遞迴

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


function MyFuncion {
    MyFunction
}

遞迴是邏輯複雜、又危險的

遞迴的優點在於程式碼的複用,但相對起來確實是邏輯較為複雜、當操作不慎時確實危險(容易造成無窮迴圈)

當每次操作時我們都必須判斷何時該停止遞迴,並請再遞迴前也須改變傳入的參數

● 能夠體現遞迴最經典的就是 階乘factorial),階乘的概念如下


N! = N * (N-1)!

# 舉例
5! = 5 * (4)!
5 = 5 * 4 * 3 * 2 * 1

Shell 函數可以使用遞迴,接下來我們就使用 Shell Function 來達成遞迴計算


# !/bin/bash

function factorial {
    local curValue=$1

    if [ $curValue -eq 1 ] ; then
        #透過 echo 方式回傳
        echo 1
    else 
        # 記得修改傳入變數!
        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 腳本中的函數,退出狀態碼是函數最後一條命令返回的退出碼

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


#! /bin/bash

func() {
    echo "Hello function"
}

echo "Start..."

func

echo "Finish... exit code: $?"

● 以默認最後一行命令的作為退出碼的方案有些危險,有可能在函數中,其他指令出錯,它也會以最後一行退出碼為主(沒有好好處理,會不容易找到 Bug 的根源)


#! /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;但指令仍算是正確執行(因為語法沒有錯誤,只是沒有批配到)


# 有該字面
grep 'Hello' tempFile

# 返回 0
echo $?

# 無該字面
grep 'World' tempFile

# 返回 1
echo $?

最準確的還是使用 man 查看所需指令的返回意義

使用 return 命令

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


return [整數]

使用 return 命令的範例如下


#!/bin/bash

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

    return $(( value * 2 ))

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

# 呼叫函數
func

echo "$?"

返回的整數範圍是 0 ~ 255 之間,如果超過則是返回值除以 256 取餘數(MOD)

echo 函數輸出:使用「$()

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


#!/bin/bash

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

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

echo "The new value is $result"

用這個方法就不會限定於一定要數字,可以返回字串、浮點數


Shell 函數的參數、變量

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

傳遞參數給函數

Shell 函數無法直接接收參數,它是透過幾個特殊的環境變量來取得傳入的參數(完整請參考 建構腳本 - 特殊參數、互動輸入

特殊變量說明
$#取得參數總數
$*取得全部傳入的參數(整體)
$@取得全部傳入的參數(個別分開)
$0腳本自身名稱
$1 ~ $n取得參數 1 ~ n

#! /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),所以在函數中取外部變數,就會判斷錯誤


#! /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),函數就可以正常運作


#! /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...)開發一樣,變量基本有分為兩種

全局變量:函數內、函數外都可以使用

這種方案你要清楚知道該全局變量在哪裡被調用,哪裡可能有做值得修改、讀取,才能安全使用


#! /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 關鍵字


#! /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 變數

● 對函數傳入數組的問題點,範例如下


#! /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 的元素!

● 解決方式:分批傳入重組

A. 分批傳入:傳入參數的方式不對,我們必須使用 $Array[*] 的方式將參數傳入


#! /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 仍錯誤

B. 重組:透過 ("$@") 方式重組 Array 的元素


#! /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 命令作為函數的返回


#! /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 會解釋用戶輸入的命令,所以可以直接在命令行中定義;有兩個方案可用(重點是花括號 { }

A. 單行定義:操作如同在 Shell 中定義參數,定義出來後在該 Shell 進程內都可以使用(如果需要給 sub shell 使用記得 export


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

# 呼叫 Function
add 100 200

B. 多行定義:多行定義使用 { 開頭,並使用 } 結尾;在這 花括號{ }中間我們可以定義多個命令


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

在命令行創建函數有幾個注意點

A. 如果在同一個 Shell 中定義同名函數,仍會後蓋前

B. Shell 結束,則命令行函數也會消失

.bashrc 中創建函數

● 在 .bashrc 中創建函數之後,在每個命令行中都可以使用;以下我們在 .bashrc 之後創建函數(不要刪除 .bashrc 中任何資料

A. .bashrc 中創建函數


# 添加在最後一行
function showTime {
    echo -n "Hello $USER, current time is "
    date
}

執行測試(以下在同個 Shell 中執行函數)


# 重新加載檔案
source ~/.bashrc

showTime

B. .bashrc 中加載腳本庫:使用 source 命令做函數加載

可以先看下面 創建庫 Library 小節

A. 首先創建一個腳本庫:以下創建名為 showTimeLib.sh 的腳本庫


function showTime2 {
    echo -n "Hello $USER, current time is "
    date
}

B. 設定腳本庫加載進 .bashrc這邊請記得使用全路徑!

● 編輯 .bashrc


# 使用 source + 使用全路徑
source /home/alien/Desktop/shell/chapter_17/showTimeLib.sh

● 測試使用 .bashrc 加載的庫


source ~/.bashrc

showTime2


腳本庫創建、使用

庫(Library)可擁有一系列可以重用方法的函數,可以加速開發,而腳本也可以製作腳本庫

創建、加載腳本庫 Library:source

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

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


# 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
}

B. 使用 source 命令引用庫:引入後就可以使用庫中的函數,而不需重複創建

source 的使用方式有兩種,如下


# 使用方式一
source <檔案>

# 使用方式二:點操作符(dot operator)
. <檔案>

其中 檔案 可以是 相對路徑、絕對路徑


#!/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 shtoolGNU shtool 提供了一些簡單的 shell 腳本函數給我們復用

A. 下載 shtool 包


# 下載
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

下載網址 ftp://ftp.gnu.org/gnu/shtool/shtool-2.0.8.tar.gz

B. 建構 shtool 庫

shtool 文件必須針對特定的 Linux 環境進行配置,配置需使用到 configuremake 命令

命令功能
configure檢查 shtool 庫文件所必須的應用,當發現所需的工具,它會使用工具路徑修改配置文件
make負責建構 shtool 庫,最終將它打包為一個完整的軟體包
make test 用於測試軟體包
make install 用於安裝包道系統中

cd shtool-2.0.8.tar/

./configure

make

sudo make install

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

image

使用 shtool 庫

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


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 顯示版本訊息


#! /bin/bash

shtool platform


更多 Linux Shell 知識

探索 Debian 系統上的套件管理系統(PKMS 概念)| 套件倉庫

Linux 環境、服務:用戶、權限管理

Linux 環境、服務、管理

在這個主題中,你可以探索各種 Linux 環境、服務和管理相關的重要概念…

了解 Linux 組成:內核責任、GNU 工具、桌面環境 | 基礎版到發行版

在這裡,你將了解 Linux 系統的基本組成部分,包括內核、GNU 工具和桌面環境,並了解不同 Linux 發行版之間的差異

探索 X Window 服務與桌面環境:Linux 視窗 XWindow 服務、D-Bus 機制

這裡探討了 Linux 系統中 X Window 服務和 D-Bus 機制的工作原理,讓你深入了解桌面環境的背後運作方式

Linux 系統管理入門:安全性、用戶管理與權限設定指南

在這個指南中,你將學習如何管理 Linux 系統,包括安全性、用戶管理和權限設定,以確保系統的安全和有效運作

認識身份驗證與權限管理:UNIX UID、使用者驗證與 PAM | PAM 設定

這裡介紹了 UNIX 系統中的身份驗證機制和權限管理方式,包括 UNIX UID、使用者驗證和 PAM(Pluggable Authentication Modules)設定

Linux Shell 相關知識

Linux Shell 相關知識

這個主題涵蓋了各種與 Linux Shell 相關的知識,從基礎到進階都有…

Shell 基礎知識:包括 Shell 的基礎使用以及差異,還有變數以及計算

認識命令行:Shell 類型、命令差異 | Sub Shell 關係 | Builtin 命令

這裡介紹了命令行中不同類型的 Shell,以及各種 Shell 命令之間的區別和內建命令的用法

Shell 全局及區域變數、特殊變數及環境變數 | Shell 啟動順序 | Array 變數

學習如何在 Shell 中使用全局變數、特殊變數和環境變數,以及如何設置和管理這些變數

探索腳本與命令:Shell 腳本的必備相關知識 | 腳本的數學運算

這裡介紹了 Shell 腳本的基本知識和常用技巧,包括如何執行腳本和進行數學運算

Shell 結構化

Shell 腳本程式中的條件語句和高級特性 | Shell 結構化 & 判斷

該篇文章是介紹結構化腳本的基礎以及特殊的高級技巧,轉著於腳本中的邏輯判斷

掌握 Bash 腳本中的迴圈與循環控制技巧 | Shell 結構化 & 循環

接著是腳本中的循環與控制的技巧,它可以讓我們在 Shell 腳本中有更多的判斷與技巧

探索 Shell 函數與腳本庫 | Shell 結構化 & 函數

這裡你將會學在到結構化腳本的重要技巧「函數」,它可以讓你建立可重複利用的腳本,以節省我們之後開發的時間

Shell 掌握參數處理與用戶輸入技巧 | Shell 結構化 &輸入互動

不可互動的腳本有時候相對無趣,這篇文章有分享如何讓腳本與使用者產生交互互動,並依照使用者輸入的參數做讀取

Shell script 進階

Shell 命令輸入與輸出指南:掌握標準文件描述符與重定向 | 臨時文件

掌控 Shell、指令的輸出輸入是成為進階使用 CLI 之人必經的一課,透過這篇文章可以了解到輸出輸入、臨時文件等等資訊

探索 Linux 訊號與後台進程管理:安排定期啟動腳本 | 運行時啟動腳本

信號可以算是 Shell 腳本與 Linux 系統之間的通訊方式,透過這個文章,我們可以了解到信號與 Shell 腳本的安排時間

Linux 硬體規劃:檔案系統、分區

Linux 硬體規劃:檔案系統、分區

最後,這個主題涵蓋了 Linux 系統中硬體結構相關的重要概念…

Shell 文件和目錄操作的常用命令和技巧 | 尋找檔案

這裡介紹了 Shell 中常用的文件和目錄操作命令,包括如何尋找文件和管理文件系統

深入探索 Linux 文件系統與硬體管理 | 分區、檔案系統、邏輯卷 | inode

學習如何管理 Linux 文件系統,包括分區、文件系統和邏輯卷等相關概念,以及文件系統中的 inode 機制

理解 Unix/Linux 設備偵測、建立 | 認識與使用 udevd | SCSI 與 Linux 核心

這裡介紹了 Unix/Linux 系統中的設備偵測和管理,包括如何使用 udevd 和 SCSI 相關的 Linux 核心知識


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響