iT邦幫忙

2021 iThome 鐵人賽

DAY 9
0
自我挑戰組

[機派X] 無人機與樹莓派的相遇 Linux不只是過客系列 第 9

[機派X] Day 9 - 玩轉 Bash:原來 Bash 還有這些妙用

引言

今天是機派X系列文章的第九天。
昨天介紹了 Linux 的檔案系統與 Bash 的實際操作,大家都熟悉了嗎?
今天要應用昨天介紹的 bash 指令寫一些小程式,希望透過這些小程式來熟悉指令的用法。

本篇大綱:

  • 引言
  • Example1 - 跟大家說早安
  • Example2 - 建立專案結構
  • Example3 - 備份檔案並加入時間戳記
  • Example4 - 依日期分類檔案
  • Example5 - 依據環境自動連接 Wi-Fi
  • 結語
  • 關於本文章系列

也許剛入手指令,所以你會覺得圖形化使用者界面比較方便,但是其實指令熟悉後會發現好處多多。舉例來說,有些簡單又具備重複性質的工作,可以透過幾行指令輕鬆完成,如此一來便能節省許多時間。反觀,如果為了一些瑣碎的小事便要大動干戈,撰寫圖形化使用者界面的程式其實是很耗時且不切實際的。

雖然市面上已有許多別人開發好的免費工具可以使用,但是別人開發的工具也許只滿足別人的需求,卻無法滿足你我的需求。尤其當我們越向更深的領域探索,所需要的工具客製化程度也會越高,別人的工具可能將會無法滿足我們的需求。

也許 python 等主打輕鬆入門的程式語言可以讓你輕鬆撰寫程式,並完成高度客製化的需求,然而這些程式語言本身卻也存在一些缺點。以 python 來說,python 的多用途奠基在豐富的函式庫(Library)上,往往許多功能都需要另外安裝函式庫後才能使用,函式庫的安裝與維護又是另外一門學問。

透過 bash 便能克服以上的痛點,由於 bash 本身內裝於系統中,相關的指令也都是作業系統的一部份,因此使用上無須額外安裝或設定,與系統間的整合也會更緊密與穩定。當然,這並不表示你應該捨棄其他程式語言,並專情於 bash ,因為每個程式語言或工具都有其誕生的目的,面對特定需求採用最適合的工具才是我們所追求的。希望在以下幾個範例中,能夠讓你熟悉常用的指令,並且發掘這些指令能夠為你提供哪些實質上的幫助。

Example1 - 跟大家說早安

我們可以將多條 Bash 指令包在一個或多個檔案中執行,而含有 Bash 指令的檔案稱為程式腳本(Script)。

舉例來說,我每天要跟 Kevin、Tina 還有 Mary 分別說早安,我可以這樣下指令:

echo '早安,Kevin'
echo '今天很漂亮喔,Tina !!'
echo '早上好,Mary'

或者:

echo -e '早安,Kevin\n今天很漂亮喔,Tina !!\n早上好,Mary'

每次都要輸入那麼多字是不是很麻煩呢?
將以上內容存成檔案 greet ,之後就可以直接呼叫 greet 來「打招呼」了!

透過以下指令,可以將第二種寫法寫入至檔案中。

echo 'echo -e '\''早安,Kevin\n今天很漂亮喔,Tina !!\n早上好,Mary'\' > greet

接著,你可以透過 cat 來確認 greet 當中的內容:

cat greet

要執行(Execute)某個檔案前,需要先賦予它執行權限。
(如果這條指令讓你覺得迷惑,請參考 Day 5 的文章。)

chmod u+x greet

之後只要直接輸入檔案的路徑與檔名就可以直接執行該檔案了!

/home/ubuntu/greet

也可以使用相對路徑來操作:

cd ~/happy
../greet

不過你可能會發現一件詭異的事:

cd ~
greet

會報錯:

bash: greet: command not found

會出現這樣的狀況在於 Bash 預設沒有將當前的目錄加入執行檔的搜尋路徑中。Bash 本身會紀錄「哪些路徑中有執行檔」,並儲存於 PATH 這個變數中。當我們下了一道指令,例如:ls ,Bash 就會在 PATH 裡面的每條路徑中找尋,是否有個檔案具備執行權限,且檔案名稱是 ls ,若有找到就會直接執行,如果找遍所有 PATH 中的路徑都沒有符合的項目,Bash 就會報錯說:該指令(command , CMD)不存在。

在預設狀態下,當前目錄(也就是 .)並未在 PATH 中,因此在 greet 所在目錄直接輸入 greet 會使 Bash 找不到當前目錄下的 greet 檔案來執行,從而導致錯誤發生。

要解決這個問題,有兩種方法:

  1. 將當前目錄 . 加入至 PATH 中。
  2. 直接給予明確的檔案位置 ./greet 。

如果要採用第一種方法,就要變更 PATH 變數中的內容,將當前路徑(也就是 .)加到 PATH 中,由於 PATH 中,不同路徑間是以 : 隔開的,因此可以這樣下指令:

export PATH="$PATH:."
cd ~
greet

如果要採用第二種方法,可以直接這樣下指令。

cd ~
./greet

至此,你已經成功寫出一個最簡單的腳本了!

如果覺得每次寫腳本都要辛苦的用 echo 跟 > 來做很麻煩的話,可以改用 vi 、vim 或 nano 等文字編輯器喔!
也可以搭配之前介紹過的註解,讓你的程式內容更加完整。

#####################################################
#  greet                                            #
#    用來跟同事打招呼用的腳本,會在螢幕上印出打招呼的訊息。  #
#    雖然看起來很廢,但是我深信這是學習腳本的一小步。       #
#                                                   #
#                                      by haward79  #
#                                       2021 09/10  #
#####################################################

# 跟同事打招呼
echo '早安,Kevin'
echo '今天很漂亮喔,Tina !!'
echo '早上好,Mary'

Example2 - 建立專案結構

每次手動建立專案是否覺得很麻煩呢?
這個範例讓我們寫一個小腳本,讓它在當前工作目錄自動建立一個專案,並初始化 git。

echo "Create project in '$(pwd)' "
read -p 'Press ENTER to continue ......' trash

echo ''
read -p 'Please input name of the project: ' name

if [[ -n $name ]]
then
    if [[ ! -d "$name" ]]
    then
        mkdir "$name"
        cd "$name"

        mkdir src bin
        
        echo -e '#include <iostream>\n\nusing std::cout;\nusing std::cin;\n\nint main()\n{\n	cout << "Hello World.\n";\n\n	return 0;\n}\n\n' > 'src/main.cpp'

        echo '.gitignore' > .gitignore

        git init
        git add .
        git commit -m 'First commit: project structure created'

        echo 'Done.'
    else
        echo "Project '$name' already exists."
    fi
else
    echo 'Please input a valid project name.'
fi

Example3 - 備份檔案並加入時間戳記

修改設定檔時,有時候會需要先備份檔案,以防檔案改壞時發生悲劇、無法還原。
備份的檔案有時也會加上時間戳記,以認明這是何時修改的。

以下腳本能夠備份檔案,並在檔案的結尾自動補上當前的時間。
為了讓這個腳本能夠被其他腳本呼叫、再利用,因此我的設計是它也能透過引數讀取要備份檔案的檔名,而未必要與使用者互動輸入檔名。

if [[ -n $1 ]]
then
    filename="$1"
else
    read -p 'Please input filename: ' filename
fi

if [[ -f $filename ]]
then
    cp "$filename" "$filename.bak"
    NOW=$(date +'%Y%m%d %H%M%S')
    echo -e "\nThis file is backed up by a SMART SCRIPT.\nBackup created at $NOW" >> "$filename.bak"
else
    echo "File: '$filename' NOT found."
fi

Example4 - 依日期分類檔案

某天手機的容量爆滿通知,終於迫使你整理手機中的照片與影片,然而將檔案從手機轉移到電腦後,望著大量檔案卻不知從何下手整理,因為檔案命名都是以日期和時間組合而成,照片與影片的命名字首也不相同,此時就讓我們寫個簡單的腳本來處理這個問題吧!
以我自己的手機為例,照片命名格式為:IMG_年月日_時分秒.jpg ,影片命名格式為:VID_年月日_時分秒.mp4 。我們將寫一個腳本,用於將一個混雜照片與影片的資料夾整理成數個以日期為索引的資料夾,每個資料夾中的檔案都只留時間作為檔名。

例如,工作目錄中含有原始檔案:

  • Camera/
    • IMG_20200901_080005.jpg
    • IMG_20200901_080343.jpg
    • VID_20200902_070213.mp4
    • IMG_20200903_132712.jpg
    • IMG_20201018_123000.jpg
    • IMG_20201018_135214.jpg
    • IMG_20201018_200403.jpg
    • VID_20201018_000215.mp4
    • VID_20201018_001826.mp4

我們希望整理成以下結構:

  • Sorted/
    • 20200901
      • 080005.jpg
      • 080343.jpg
    • 20200902
      • 070213.mp4
    • 20200903
      • 132712.jpg
    • 20201018
      • 123000.jpg
      • 135214.jpg
      • 200403.jpg
      • 000215.mp4
      • 001826.mp4
if [[ -d 'Sorted' ]]
then
    rm -r 'Sorted'
fi

mkdir 'Sorted'

for file in Camera/*
do
    export file_ext=$(echo "$file" | rev | cut -d . -f 1 | rev)
    export file_revWithoutExt=$(echo "$file" | rev | cut -d . -f 2)
    export file_date=$(echo "$file_revWithoutExt" | cut -d _ -f 2 | rev)
    export file_time=$(echo "$file_revWithoutExt" | cut -d _ -f 1 | rev)

    echo "Origin : $file"
    echo "Date   : $file_date"
    echo "Time   : $file_time"
    echo "Ext    : $file_ext"
    echo '----------------------------------------'

    if [[ ! -d "Sorted/$file_date" ]]
    then
        mkdir "Sorted/$file_date"
    fi

    mv "$file" "Sorted/$file_date/$file_time.$file_ext"
done

Example5 - 依據環境自動連接 Wi-Fi

我自己使用樹莓派時,通常都是藉由電腦遠端連線,這樣可以免去轉換工作環境時還要重新差拔樹莓派的週邊設備。
也是因此,每次使用樹莓派,我都是直接接電源線而已!!

不過這會面臨一個問題:我希望樹莓派能隨著環境的變化自動切換要連接的無線網路。
例如:我在家裡使用時,樹莓派要連接到家裡的 Wi-Fi ;當我在學校使用時,則連接到手機的 Wi-Fi。
這樣才是真正的免螢幕、免鍵盤啊!只要樹莓派跟電源線帶著走,到哪裡都可以立刻當碼奴!

這就是以下這個腳本誕生的由來!
執行這個腳本之前要先設定 home-wifi 與 mobile-wifi 連線,包含 Wi-Fi 的 SSID、密碼等資訊。
之後,只需要將這個腳本 利用 rc-local 服務設定成開機自動執行 即可。
每次開機後,以下腳本就會自動執行,並依照能抓到的 Wi-Fi SSID 自動完成連線。

for(( ; 1; ));
do
	if [[ $(nmcli device show wlan0 | grep 'home-wifi') = '' && $(nmcli d wifi list | grep 'home-wifi') != '' ]]
    then
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Wi-Fi: home-wifi."
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Clear other connection ..."
		nmcli dev disconnect wlan0
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Connecting to home-wifi ..."
		nmcli con up home-wifi
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	elif [[ $(nmcli device show wlan0 | grep 'home-wifi') = '' ]] && [[ $(nmcli d wifi list | grep 'mobile-wifi') != '' ]] && [[ $(nmcli device show wlan0 | grep 'mobile-wifi') = '' ]]
    then
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Wi-Fi: mobile-wifi."
    	echo "$(date +'%Y.%m.%d %H:%M:%S') | Clear other connection ..."
		nmcli dev disconnect wlan0
		echo "$(date +'%Y.%m.%d %H:%M:%S') | Connecting to mobile-wifi ..."
		nmcli con up mobile-wifi
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	else
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
		echo "$(date +'%Y.%m.%d %H:%M:%S') | No operation to do." >> 'keep_wifi_connection.log';
		echo "$(date +'%Y.%m.%d %H:%M:%S') | =================================================================" >> 'keep_wifi_connection.log'
	fi
	
	sleep 5
done

結語

以上就是五個簡單的 Bash 指令範例,範例中為求簡單易懂,因此仍有許多的狀況未列入腳本設計時的考量。
如果以上腳本成功引起你對 Bash 的興趣,也歡迎你把這些腳本改良或客製化,成為你最實用的小工具。

關於本文章系列

如果對於文章內容有建議、糾錯或圖源標示不正確的問題,歡迎參考 [機派X] Day 1 嘗試與文章作者聯絡。
想看更多本系列的文章,請連結至 [機派X] Day 1 查看大綱。


上一篇
[機派X] Day 8 - 我是 Bash 我調皮,令人匪夷所思的 Bash 語法
下一篇
[機派X] Day 10 - 寒酸的無人機介紹
系列文
[機派X] 無人機與樹莓派的相遇 Linux不只是過客15

尚未有邦友留言

立即登入留言