01.02 Bash技巧:實例介紹複用外部 shell 腳本代碼的幾種方法


Bash技巧:實例介紹複用外部 shell 腳本代碼的幾種方法

本篇文章介紹在 Linux bash shell 腳本中如何執行外部腳本文件,以及如何調用其他腳本里面的函數,以便複用外部腳本代碼。

在 shell 腳本中執行外部腳本文件

在 bash shell 腳本文件中,如果需要執行其他的外部腳本文件,跟執行外部命令類似,直接通過外部腳本文件名來執行即可,可能需要提供尋址路徑。

例如,要在 a.sh 腳本文件中執行當前目錄下的 b.sh 腳本文件,直接在 a.sh 文件裡面寫上 ./b.sh 語句,就可以執行 b.sh 腳本文件。

如果需要提供參數,在 ./b.sh 後面寫上參數即可。
不同參數之間用空格隔開。如果參數自身帶有空格,要用引號把該參數括起來。
例如,寫為 ./b.sh arg1 arg2。

在執行 a.sh 腳本文件時,a.sh 會運行在一個子 shell 裡面。
這個子 shell 會再啟動一個子 shell 來執行 b.sh 腳本文件。
在 b.sh 裡面執行 exit 命令,只會退出 b.sh 的執行,不會退出 a.sh 的執行。

注意:在 a.sh 腳本中執行 b.sh 腳本文件時,如果 b.sh 腳本文件沒有放在 PATH 環境變量可以尋址的目錄裡面,那麼需要在 a.sh 裡面提供 b.sh 腳本文件的尋址路徑。
建議寫為絕對路徑,以保證總是能尋址到 b.sh 腳本文件。

如果寫為相對路徑,會被執行 a.sh 的工作目錄所影響。
基於所給的相對路徑,可能會尋址不到 b.sh 腳本文件。

假設有一個 a.sh 腳本文件,其內容如下

#!/bin/bash

echo "$0: this is a.sh"

./b.sh

有一個 b.sh 腳本文件,其內容如下:

#!/bin/bash

echo " $0: this is b.sh"

把 a.sh 和 b.sh 放到同一個目錄下,都添加可執行權限。
在這兩個腳本文件所在的目錄開始測試:

$ ./a.sh

./a.sh: this is a.sh

./b.sh: this is b.sh

$ mkdir subdir

$ cd subdir

$ ../a.sh

../a.sh: this is a.sh

../a.sh: line 4: ./b.sh: No such file or directory

可以看到,在這兩個腳本文件所在的目錄,執行 ./a.sh,可以正常尋址到 b.sh,並執行 b.sh。

但是進入到該目錄下的 subdir 目錄,通過 ../a.sh 執行父目錄的 a.sh 腳本文件,會提示找不到 ./b.sh 腳本文件,無法執行 b.sh。

即,在 a.sh 腳本里面執行 b.sh 腳本時,是基於當前工作目錄來尋址到 b.sh,而不是基於 a.sh 腳本文件所在的目錄來尋址 b.sh。

當前工作目錄可能不固定,在 a.sh 裡面寫為相對路徑來尋址 b.sh,後續執行可能導致找不到 b.sh。
寫為絕對路徑,就不會受到工作目錄的影響。

用 source 命令執行其他腳本

在 bash 中,可以使用 source 內置命令、或者 . 內置命令來在當前腳本進程中執行其他腳本文件。

source 命令和 . 命令相互等價。
查看 help source 的幫助信息和 help . 的幫助信息完全一樣。
後面會以 source 命令作為例子,統一說明。

查看 help source 的說明如下:

source filename [arguments]
Execute commands from a file in the current shell.
Read and execute commands from FILENAME in the current shell.
The entries in $PATH are used to find the directory containing FILENAME.
If any ARGUMENTS are supplied, they become the positional parameters when FILENAME is executed.


Exit Status:
Returns the status of the last command executed in FILENAME; fails if FILENAME cannot be read.

即,bash 在執行腳本文件時,默認會啟動子 shell 來執行該腳本,運行在子進程下。
而通過 source 命令執行腳本文件時,直接運行在當前 shell 下。
所提供的參數會作為被執行腳本文件的位置參數。

在 a.sh 腳本文件中,通過 source 命令執行 b.sh 腳本文件,有一個好處是可以在 a.sh 中通過函數名單獨調用到 b.sh 中的函數
這有助於 shell 腳本的代碼複用。

假設有一個 testsource.sh 腳本文件,其內容如下:

#!/bin/bash

echo "$0: this is a.sh"

./utils.sh

utils_print

source ./utils.sh

utils_print

有一個 utils.sh 腳本文件,其內容如下:

(不好意思,近期網頁版文章的代碼塊排版錯亂,後臺諮詢確認網頁版不支持。下面用四個 ‘----’ 代替四個空格來進行縮進和隔行顯示。後面的代碼塊會類似處理。如果需要複製代碼到本地驗證,麻煩以四個 ‘----’為單位,替換成四個空格。非常抱歉。)

#!/bin/bash

utils_print()

{

----echo -e " b.sh: this is utils_print"

}

把這兩個腳本文件放到同一個目錄下,執行 testsource.sh 腳本文件,結果如下:

$ ./testsource.sh

./testsource.sh: this is a.sh

./testsource.sh: line 5: utils_print: command not found

b.sh: this is utils_print

可以看到,在 testsource.sh 中執行 ./utils.sh 語句後,執行 utils_print 報錯,提示找不到這個命令。
這種方式不能通過函數名調用到外部腳本文件裡面的函數。

而在 testsource.sh 中執行 source ./utils.sh 語句後,執行 utils_print 沒有報錯,且確實調用到 utils.sh 裡面的函數。

即,通過 source 命令執行外部腳本後,被執行腳本的全局變量名、函數名會被導出到當前 shell 中。
可以通過函數名調用外部腳本里面的函數,也可以通過變量名來引用外部腳本里面的全局變量。

注意:假設 a.sh 腳本通過 source 命令執行了 b.sh 腳本,在實際使用時有一些需要注意的地方。具體說明如下:

  • 由於 source 命令執行的腳本文件會運行在當前 shell 下,如果 b.sh 腳本執行了 exit 語句,不但該腳本會退出,調用它的 a.sh 腳本也會退出。如果不想 b.sh 腳本的執行狀態影響到 a.sh 腳本的執行狀態,那麼 b.sh 腳本要慎重使用 exit 語句。
  • 在 a.sh 腳本里面要通過絕對路徑來尋址 b.sh 腳本,或者把 b.sh 腳本文件放在 PATH 環境變量可以尋址的目錄裡面,以保證在任意工作目錄下執行 a.sh 腳本時,都能尋址到 b.sh 腳本。
  • 在 a.sh 腳本通過 source 命令執行 b.sh 腳本,那麼 b.sh 腳本代碼裡面的 $# 對應的是 a.sh 的參數個數,還是執行 b.sh 腳本時的參數個數?答案是視情況而定。描述如下:
  1. a.sh 腳本沒有傳遞參數給 b.sh 腳本,那麼在 b.sh 腳本中獲取 $# 的值,對應傳遞給 a.sh 的參數個數。例如執行 source ./b.sh,那麼 b.sh 打印 $# 的值跟 a.sh 打印的值相等。
  2. 如果 a.sh 腳本傳遞了參數給 b.sh 腳本,那麼 b.sh 腳本里面的 $# 對應傳遞給 b.sh 的參數個數。例如執行 source ./b.sh 1 2 3,那麼 b.sh 打印 $# 的值會是 3。
  • 用 source 命令執行 b.sh 腳本,b.sh 腳本的全局變量會導出到當前 shell 下。在當前 shell 沒有退出之前,這個全局變量值會一直存在。即使 b.sh 腳本執行結束,它裡面的全局變量值還是存在。再次使用 source 命令調用 b.sh,如果沒有重新為全局變量賦值,它的值不會變。

修改前面的 utils.sh 腳本為下面的內容:

#!/bin/bash

declare value

if [ $# -eq 1 ]; then

----value="$1"

fi

echo $value

這裡使用 declare value 聲明一個 value 變量,但是沒有為它賦予初值。

用 source 命令多次執行該腳本,結果如下:

$ source ./utils.sh init_value

init_value

$ source ./utils.sh

init_value

執行 source ./utils.sh init_value 命令,utils.sh 腳本會把 value 變量賦值為 "init_value",並打印出這個值。

接著執行 source ./utils.sh 命令,沒有傳參,沒有為 value 變量賦值,但是打印出 value 變量值為 "init_value",保持為之前的值。

即,當多次通過 source 命令執行某個腳本時,如果這個腳本里面使用到全局變量,需要基於實際需求來確認是否需要為全局變量賦予初值,避免之前保存的值帶來干擾。

一個可以通過函數名調用內部函數的腳本模板

前面介紹通過 source 命令執行外部腳本後,可以通過函數名單獨調用外部腳本里面的函數。
由於該腳本會運行在當前 shell 下,會對當前 shell 造成影響,編寫腳本代碼有不少需要注意的地方,否則會引入一些不預期的問題。

其實,不使用 source 命令執行外部腳本,也可以通過函數名來調用該腳本里面的函數,只需要調整一下腳本代碼的寫法。

假設要調用 utils.sh 腳本里面的函數,可以讓該腳本接收一個或多個參數,指定要調用的函數名,以及要傳遞給該函數的參數。
然後 utils.sh 腳本自己執行這個函數即可。
調用該腳本的格式類似如下:

utils.sh function_name [arguments]

此時,當前 shell 會啟動一個子 shell 來運行 utils.sh 腳本。

這個格式類似於 source 命令的格式,只是 source 命令指定要執行的腳本文件名,而這裡指定要調用的函數名。

為了滿足這個需求,修改 utils.sh 腳本為下面的內容:

#!/bin/bash

function show_args()

{

----echo "Enter show_args(): FUNCNAME: $FUNCNAME"

----echo "Enter show_args(): \\$1: $1"

----echo "The arguments are: $@"

}

----

if [ $# -ne 0 ]; then

----funcname="$1"

----shift 1

----$funcname "$@"

fi

當 $# 不等於 0 時,說明提供了參數,那麼第一個參數預期是想要調用的函數名,保存給 funcname 變量。

然後執行 shift 1 命令,向左移動一個位置參數。
原來的 $2 會變成 $1,$3 會變成 $2,依此類推。
這樣做可以跳過傳入腳本的第一個參數。
這個參數是想要調用的函數名,不需要傳遞給被調用的函數。

最後執行 $funcname "$@" 語句,也就是調用 funcname 變量保存的函數,並把移動後的位置參數都傳給該函數。

這裡實現了一個 show_args 函數,裡面打印了當前函數名、傳入的第一個參數、以及所有參數。

具體測試結果如下:

$ ./utils.sh show_args 1 2 3

Enter show_args(): FUNCNAME: show_args

Enter show_args(): $1: 1

The arguments are: 1 2 3

$ value=$(./utils.sh show_args 1 2 3)

$ echo $value

Enter show_args(): FUNCNAME: show_args Enter show_args(): $1: 1 The arguments are: 1 2 3

可以看到,./utils.sh show_args 1 2 3 命令確實調用到 utils.sh 腳本里面的 show_args 函數,並把 1 2 3 作為參數傳遞給該函數。

如果上面不執行 shift 1 命令,那麼 show_args 函數收到的 $1 參數會是 show_args,$2 會是 1。
而預期是把 1 作為該函數的第一個參數,對應 $1。
執行 shift 1 命令可以達到這個效果。

如果想要獲取函數輸出的字符串,那麼在函數里面用 echo 命令打印對應的字符串,然後用 $() 就能獲取這個值。
如上面的 value=$(./utils.sh show_args 1 2 3) 命令所示。
打印 value 變量的值,可以看到它保存了 show_args 函數輸出的所有字符串。

這種情況下,函數里面不能再用 echo 命令打印調試信息,否則調試信息也會被調用者獲取到,影響調用者對這個字符串的解析。

這段 utils.sh 腳本代碼就是一個可以通過函數名調用內部函數的腳本模板。
基於實際需求添加相應的函數,這些函數就可以被複用,方便其他腳本進行調用。


分享到:


相關文章: