影音數位典藏所需的自動化腳本(Bash/Script)--初級篇
影音數位典藏所需的自動化腳本(Bash Script)--初級篇
如前文所提,腳本是一種可編輯與執行的純文字檔,可以搭配運算元和邏輯,用來執行多個指令,因此撰寫時有幾點要注意:
- 腳本內的指令與運作基本上有由上而下,由左至右的方向性。
- 讀取到 [Enter] 符號 (即斷行),就視為下達該行指令。
- 如需將一行指令拆分為多行,需以反斜線 (\) 做為該行結尾,再使用 [Enter] 鍵換行。
- 當果要加入註解或暫時跳過某行指令時,在註解開頭加上 # 符號。在 # 之後的文字會被系統忽略。
- 如同指令中的多個空格會被視為單格,用來縮行的 [tab] 鍵和多個空白鍵會被視為單一空白格,有助於排版和視讀但不會影響指令運作。
- 如同下達指令,大小寫不同,因此輸入時要確認指令和參數不要打錯。
撰寫第一個腳本
在進入自動化腳本世界,當然要從打招呼開始。因此就從顯示「Hello Bash!」這個字眼開始:
bash-4.4$ mkdir bash; cd bash
bash-4.4$ nano hello.sh
一開始,先建立了一個名為 "bash" 的資料夾,並進入這個資料夾開始練習。再使用 nano 這個程式來編輯一個名為 "hello.sh" 的腳本。
GNU nano 2.0.6 File: hello.sh
#!/bin/bash
echo "Hello, Bash!"
在 nano 編輯器中,可以輸入這兩行字,來做為第一個腳本的內容。輸入完按 [ctrl]+[x] 鍵離開並儲存。
Save modified buffer (ANSWERING "No" WILL DESTROY CHANGES) ?
Y Yes
N No ^C Cancel
畫面會詢問是否要進行儲存,按 [y] 鍵確認,並確定檔名即可。
bash-4.4$ ls -al
total 8
drwxr-xr-x 3 mpcb staff 96 7 2 12:30 .
drwxr-xr-x 4 mpcb staff 128 7 2 12:30 ..
-rw-r--r-- 1 mpcb staff 34 7 2 12:29 hello.sh
剛寫完的腳本只是一般純文字文件,並不具備執行權限,如上圖,因此需要將它加上執行權限後才能執行。
bash-4.4$ chmod +x hello.sh
bash-4.4$ ./hello.sh
Hello, Bash!
執行腳本後,便會出現這樣結果!沒有錯誤訊息,可喜可賀!回頭來看這個腳本中的內容代表什麼:
#!/bin/bash
echo "Hello, Bash!"
- 宣告這個腳本所使用的環境(shell)名稱:
#!/bin/bash 代表這個腳本會在 Bash 這個 shell 下工作。如此一來它才能載入 Bash 這個相關環境下的設定。如果沒有這行的話,可能會因為系統無法判斷該使用什麼環境來執行而出現問題。
這樣的宣告有時會因為所使用的程式有所差異,Bash 是 Linux/Mac OS 所內建的 shell,因此在為了這些環境下所設計的程式,通常可以使用這個 shell 工作。如果程式有特別要求使用如 sh,csh,tcsh,zsh 等特定 shell,才會需要修改此行。
有時候這行可能會寫成不同的表現方式,如下:!/bin/sh!/usr/bin/env bash
!/usr/local/bin/bash
- sh 是所有類 Unix 系統的共通 shell,有基本的工作能力,但功能略遜於 bash。
- /usr/bin/env bash 代表使用系統環境預設的 Bash。
- 如果系統中有超過一組 shell ,可以透過使用完整路徑來指定使用特定版本的 shell。例如系統中的 /bin/bash 和 /usr/local/bin/bash 可能分屬不同版本。透過使用完整路徑的表示方式,可以指定使用特定的環境。
- 腳本主文:
echo "Hello, Bash!"。在這個例子中,就很簡單的使用 echo 這個指令來回傳字串。後續在寫腳本時,就是將要讓腳本所執行的工作寫在這個區域囉。
bash-4.4$ /usr/local/bin/bash -version | grep "GNU bash"
GNU bash,版本 4.4.23(1)-release (x86_64-apple-darwin17.5.0)
bash-4.4$ /bin/bash -version | grep "GNU bash"
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin19)
特殊運算符
撰寫腳本和下達指令一樣,除了空白鍵外,還有許多協助進行邏輯判斷的運算符,包括:
- | 運算符,用法:command1 | command2
當第一個指令成功後,將結果輸出給第二個指令繼續執行。bash-4.4$ ls | grep shhello.shversion.sh在使用 ls 顯示資料夾內容後,使用 grep 擷取出內容中有出現 sh 字眼的字段。 - && 運算符,用法:command1 && command2當第一個指令為真時(成功後),則繼續執行第二個指令。bash-4.4$ ls dir && echo "true"truebash-4.4$ ls no_dir && echo "true"ls: no_dir: No such file or directory當資料夾中有名為 dir 的檔案或資料夾時,執行 ls dir 成功,因此會回應第二個指令的輸出 true 的字串。當資料夾中沒有名為 no_dir 的檔案或資料夾時,則無法執行第二段指令。
- || 運算符,用法:command1 || command2與 相反,當第一段指令不成功時,則執行第二個指令。bash-4.4$ ls no_dir || echo "There is no 'no_dir' in here."ls: no_dir: No such file or directoryThere is no 'no_dir' in here.因為資料夾中沒有名為 no_dir 的檔案或資料夾,因此執行第二段指令。
- ( ) 運算符,用法:(command1; command2; command3;)
在指令列中,當多個指令必需放在同一行時,可使用分號 (;) 進行分割,但如果需要合併執行多個指令,可以用小括號合併多個指令。bash-4.4$ ls dir && (ls; echo "There are some other files in here.";)dir hello.sh version.shThere are some other files in here.第一段指令用來尋找資料夾中有沒有名為 dir 的檔案或資料夾,執行成功後執行第二段指令,包括顯示資料夾,與回應字串。
在腳本中還有其他用法,是用來設定函式及陣列,如 verify_input() 和 array = (a1 a2 a3),這後續會再提到。 - { } 運算符,用法:{ command1; command2; },注意大括號與指令間必需有空格。
與小括號會獨立到子 shell 中運算不同,使用大括號時會在同一個 shell 中運算,因此變數會被傳遞。bash-4.4$ ( a=1; echo $a; ); echo $a;1bash-4.4$ { a=1; echo $a; }; echo $a;11
除了指令外,大括號還常用來進行變數內容的替換:bash-4.4$ var="String.txt";echo $var;\> echo '>'{$var}'<';\> echo ${var%.*};echo ${var##*.};String.txt>{String.txt}<Stringtxt第三行中的 % 代表去除右側,# 代表去除左側,可用於擷取變數中的特定部份字串。 - ! 在腳本邏輯中,表相反,例如 != 通常表示"不等於"。或 [!2].txt 表除了含 2 以外的檔案。bash-4.4$ ls *.txt; ls [!2].txt;1.txt 2.txt 3.txt1.txt 3.txt
- " " 與 ' ' :一般狀況下雙引號內的特殊字元,如 $ 等,可保留變數的內含,而單引號則表一般字元 (純文字),如下所示:bbash-4.4$ var="String";echo "$var";echo '$var'
String$var
互動式腳本
很多時候在使用自動化腳本時,會因為工作需求而更改輸入或輸出的目標。因此,這時候就要讓腳本能讀取使用者所定義的變數了。例如寫個顯示程式版本的腳本:
使用此腳本測試 bash 和 ffmpeg 兩個程式的版本會出現:
在腳本主文中的第一行:input="$1" 表示 input 這個變數代表指令的第 2 位置的參數 (指令本身是第 1 位, "$0" )。因此當執行 ./version.sh bash 時,第二行就相當於在指令列直接輸入 bash -version | grep "Copyright" 。
GNU nano 2.0.6 File: version.sh
#!/usr/bin/env bash
# Description:
# Shell shows version informat
# History:
# 2020-07-02 by Mengchun
input="$1"
$input -version | grep "Copyright"
使用此腳本測試 bash 和 ffmpeg 兩個程式的版本會出現:
bash-4.4$ chmod +x version.sh; ./version.sh bash;
Copyright (C) 2016 Free Software Foundation, Inc.
bash-4.4$ ./version.sh ffmpeg
ffmpeg version 4.3 Copyright (c) 2000-2020 the FFmpeg developers
在腳本主文中的第一行:input="$1" 表示 input 這個變數代表指令的第 2 位置的參數 (指令本身是第 1 位, "$0" )。因此當執行 ./version.sh bash 時,第二行就相當於在指令列直接輸入 bash -version | grep "Copyright" 。
使用 if...then 進行條件判斷
當逐一輸入的指令無法滿足需求,或希望腳本能更完善時,如何使用條件判斷便很重要了。最基礎的條件判斷便是 if ... then ,即"若 ... 則 ... "。表示當某個條件成立時,則執行某個特定指令。這個與前述 && 運算符類似,但透過 if 進行判斷時,能有更廣泛的使用方式。
if 判斷式的完整表示方式為:
if condition ; then
command
elif condition_n ; then
command_n
else
command_x
fi
除了亮藍色部份為必備元素,其他額外的判斷階段並非必要,如 elif 與 else 及其所附帶的指令區塊。各部份意義如下:
- if condition ; then:代表當條件 ( condition ) 成立時,則執行指令 ( command )。
- elif condition_n ; then:當條件 ( condition ) 不成立時,則進行第二次判斷,如第二個條件 ( condition_n ) 成立時,則執行第二段指令 ( command_n ) 。
- else:當上述條件皆不成立時,則執行最後指令 ( command_x ) 。
- fi:為迴圈結尾。
可以注意到,判斷式 if 和 elif 的結尾都必需使用 ; then 做結尾,但 else 就不再做判斷了,因此沒有這個部份。
最常使用 if 判斷式的狀況就是判斷是否輸入腳本的參數是否符合預期設計。範例如下:
#!/usr/bin/env bash
# Description:
# Export metadata from AV file into json
# History:
# 2020-07-02 by Mengchun
input_file="$1"
# 指定要被處理的目標(影音檔)。如執行 ./json.sh AV_FILE
# 則 ${input_file} 這個變數代表 AV_FILE 此處檔案的檔名
# verify input # 此段落用來判斷輸入 目標是否為預期的影音檔
if [[ "${input_file}" = "" ]]; then
# 判斷 input_file 這個變數有沒有數值,如果沒有則執行下面指令
echo -n "Please drag-and-drop a file:"
# 顯示提示,提醒使用者可以使拖拉方式將檔案拉到視窗中
read input_file
# 讀取輸入資訊,將資訊傳遞給變數
fi
if [[ "${#}" -gt 1 ]]; then # 判斷是否輸入超過一個目標
echo "Error: too many arguments: ${#}."
# 顯示錯誤訊息,輸入過多目標
exit 1
# 離開這個腳本
elif [[ ! -f "${input_file}" ]]; then
# 若上式不成立,判斷目標是否有這個檔案(-f),若無(!),則執行下面指令
echo "Error: "${input_file}" is not a file."
# 顯示錯誤訊息,輸入目標不是一個檔案
exit 1
elif ffprobe "${input_file}" 2>&1 | grep "Invalid data found" >/dev/null; then
# 若上式不成立,判斷目標是否為影音檔(可用於 ffprobe 這個程式)
# 在此使用 ffprobe 讀取目標, 2>&1 表將 ffprobe 的錯誤訊息輸出(2)
# 傳遞至螢幕輸出(1),使得後續的 grep 可以尋找錯誤訊息中的特定字串
# 尋找結果不需要再次顯示,因此將結果傳遞至丟棄區 >/dev/null
# 當 ffprobe 出現這樣錯誤訊息,表目標不是影音檔,則執行下面指令
echo "Error: '$(basename ${input_file})' is not an AV file."
# 顯示錯誤訊息,輸入執不是一個影音檔
# '$(basename ${input_file})' 用來去除路徑,只留下檔名
exit 1
fi # 結束此迴圈
# generate Metadata in json
# 經過上面的判斷式,確認輸入目標是預期可被 ffprobe 接受的影音檔
# 因此可以開始將目標丟給 ffprobe 這個程式去運作,並產生預期結果
filename="${input_file%.*}_${input_file##*.}.json"
# 將輸入檔的檔名由"."拆分兩段,重新使用下底線"_"組合輸出檔的檔名
ffprobe \
"${input_file}" -show_format -show_streams \ -print_format json \
> "${filename}"
實際執行這個腳本試試:
- 沒有輸入目標:bash-4.4$ ./json.shPlease drag-and-drop a file:
- 輸入超過一個目標:bash-4.4$ ./json.sh video.mp4 video.mp4 video.mp4Error: too many arguments: 3.
- 輸入目標不是檔案:bash-4.4$ ./json.sh there_is_no_fileError: there_is_no_file is not a file.
- 輸入目標不是可接受的影音檔:bash-4.4$ ./json.sh video_mp4.jsonError: 'video_mp4.json' is not an AV file.
- 輸入一個可接受的影音檔:bash-4.4$ ./json.sh video.mp4在一串程式輸出訊息後,可檢查是否有新增預期的目標檔案。bash-4.4$ ls *json*json.sh video_mp4.json
這樣便是最基礎的使用 if 判斷的腳本。在進入下一個判斷式前,下一篇將介紹使用函式(function)來建構腳本,這技巧對於由多個判斷式或多個迴圈的複雜腳本很重要。此文所使用的腳本與影片可由下列連結取得:
- 未加註解原始腳本檔:hello.sh, version.sh, json.sh
- 測試用影片檔:video.mp4
- 利用 ffmpeg 產生測試影片:ffmpeg \-f lavfi -i testsrc2=duration=5:size=320x240:rate=24 \
video.mp4
留言
張貼留言