為什麼要在函數中編寫整個 bash 腳本?

為什麼要在函數中編寫整個 bash 腳本?

在工作中,我經常編寫bash腳本。我的主管建議將整個腳本分解為函數,類似於以下範例:

#!/bin/bash

# Configure variables
declare_variables() {
    noun=geese
    count=three
}

# Announce something
i_am_foo() {
    echo "I am foo"
    sleep 0.5
    echo "hear me roar!"
}

# Tell a joke
walk_into_bar() {
    echo "So these ${count} ${noun} walk into a bar..."
}

# Emulate a pendulum clock for a bit
do_baz() {
    for i in {1..6}; do
        expr $i % 2 >/dev/null && echo "tick" || echo "tock"
        sleep 1
    done
}

# Establish run order
main() {
    declare_variables
    i_am_foo
    walk_into_bar
    do_baz
}

main

除了「可讀性」之外,還有什麼理由這樣做嗎?

它是否使腳本運行更有效(我實際上期望相反,如果有的話),或者它是否使修改程式碼超出上述可讀性潛力變得更容易?或者這真的只是一種風格偏好?

請注意,雖然腳本沒有很好地演示它,但我們實際腳本中函數的「運行順序」往往是非常線性的——walk_into_bar取決於i_am_foo已完成的內容,並do_baz作用於設定的內容walk_into_bar——因此能夠任意交換運行順序並不是我們通常會做的事情。例如,你不會突然想把 放在declare_variables後面walk_into_bar,那會破壞事情。

我如何編寫上述腳本的一個範例是:

#!/bin/bash

# Configure variables
noun=geese
count=three

# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"

# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."

# Emulate a pendulum clock for a bit
for i in {1..6}; do
    expr $i % 2 >/dev/null && echo "tick" || echo "tock"
    sleep 1
done

答案1

可讀性是一回事。但還有更多模組化不僅僅是這個。 (半模組化對於函數來說可能更正確。

在函數中,您可以將一些變數保留為局部變量,這會增加可靠性,減少事情變得混亂的機會。

函數的另一個優點是可重複使用性。一旦函數被編碼,它就可以在腳本中多次應用。您也可以將其移植到另一個腳本。

你現在的程式碼可能是線性的,但將來你可能會進入線性的領域多執行緒, 或者多重處理在 Bash 世界中。一旦你學會了在函數中做事,你就已經做好了進入並行的準備。

還有一點要補充。正如 Etsitpab Nioliv 在下面的評論中註意到的那樣,很容易從函數重定向為一個連貫的實體。但是函數重定向還有一個面向。即,可以沿著函數定義設定重定向。例如。

f () { echo something; } > log

現在函數呼叫不需要顯式重定向。

$ f

這可能會避免許多重複,從而再次提高可靠性並有助於保持事情井然有序。

也可以看看

答案2

閱讀後我開始使用同樣風格的 bash 編程Kfir Lavi 的部落格文章“防禦性 Bash 程式設計”。他給了很多很好的理由,但我個人認為這些才是最重要的:

  • 過程變得具有描述性:更容易弄清楚程式碼的特定部分應該做什麼。您看到的不是程式碼牆,而是“哦,該find_log_errors函數會讀取該日誌檔案中的錯誤”。與在冗長的腳本中間找到大量使用上帝知道什麼類型的正則表達式的 awk/grep/sed 行進行比較 - 你不知道它在那裡做什麼,除非有註釋。

  • 您可以透過將set -x和括起來來調試函數set +x。一旦您知道程式碼的其餘部分工作正常,您就可以使用此技巧來專注於僅調試該特定功能。當然,您可以附上部分腳本,但如果它很長怎麼辦?這樣做更容易:

       set -x
       parse_process_list
       set +x
    
  • 列印用法與cat <<- EOF . . . EOF.我已經使用它很多次了,使我的程式碼更加專業。另外,parse_args()getopts功能也相當方便。同樣,這有助於提高可讀性,而不是將所有內容都像巨大的文字牆一樣塞進腳本中。重複使用這些也很方便。

顯然,對於了解 C、Java 或 Vala 但 bash 經驗有限的人來說,這更具可讀性。就效率而言,你能做的並不多——bash 本身並不是最高效的語言,而且在速度和效率方面人們更喜歡 Perl 和 Python。但是,您可以使用nice一個函數:

nice -10 resource_hungry_function

與在每一行程式碼上呼叫nice相比,這減少了大量的輸入,並且當您只想以較低優先權運行腳本的一部分時可以方便地使用。

在我看來,當您想要在背景運行一大堆語句時,在背景運行函數也很有幫助。

我使用這種風格的一些例子:

答案3

在我的評論中,我提到了函數的三個優點:

  1. 它們更容易測試和驗證正確性。

  2. 函數可以在未來的腳本中輕鬆重複使用(來源)

  3. 你的老闆喜歡他們。

並且,永遠不要低估第三點的重要性。

我還想解決一個問題:

……所以能夠任意交換運行順序並不是我們通常會做的事情。例如,你不會突然想把 放在declare_variables後面walk_into_bar,那會破壞事情。

為了獲得將程式碼分解為函數的好處,應該嘗試使函數盡可能獨立。如果walk_into_bar需要一個未在其他地方使用的變量,則應在 中定義該變數並將其設定為本地變數walk_into_bar。將程式碼分成函數並最小化它們的相互依賴性的過程應該會使程式碼更加清晰和簡單。

理想情況下,函數應該易於單獨測試。如果由於交互的原因,它們不容易測試,那麼這表明它們可能會從重構中受益。

答案4

將程式碼分解為函數的原因與對 C/C++、python、perl、ruby 或任何程式語言程式碼進行分解的原因相同。更深層的原因是抽象化——您將較低層級的任務封裝到較高層級的原語(函數)中,這樣您就不需要擔心事情是如何完成的。同時,程式碼變得更具可讀性(和可維護性),程式邏輯也變得更加清晰。

然而,看看你的程式碼,我發現有一個函數來聲明變數很奇怪;這真讓我皺起了眉頭。

相關內容