# 甲、介紹 ## 一、前言 這篇是我自己工作上需要撰寫 bash shell 而從網路找來的資源整理成的筆記,盡量由簡入深,沒有摸過 bash 的人應該可以當成教學來看,有些較難較少用的我也沒有寫,以建立基本的自動化腳本程式為目標的話,應該是足夠的了。如果有錯誤或建議新增的內容,歡迎告訴我。 ## 二、參考資源 * [鳥哥私房菜 - 第十章、認識與學習 BASH](https://linux.vbird.org/linux_basic/centos7/0320bash.php) * [鳥哥私房菜 - 第十二章、學習 Shell Scripts](https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php) * [Jason Note - shell十三問](https://jasonblog.github.io/note/shell/2311.html) * [Bash命令语法和Bash Cheat Sheet中文速查表 | HelloDog](https://wsgzao.github.io/post/bash/) * [Bash Script 語法解析](https://medium.com/vswe/bash-shell-script-cheat-sheet-15ce3cb1b2c7) * [GNU Bash manual ](https://www.gnu.org/software/bash/manual/) * [Bash 程式設計教學與範例:function 自訂函數](https://officeguide.cc/bash-tutorial-define-use-functions/) ## 三、編寫 使用 `vi` 等文字編輯軟體來編寫。 ```bash= vi fileName.sh ``` 副檔名習慣上用 `.sh`,但不是必要。 ==注意==:若在 Windows 上編寫再丟到 Linux 上執行,要小心 **CR** 和 **CRLF** 的差別,可能會有超乎預期結果。建議都在 Linux 或 WSL 下編寫。 ## 四、結構 檔案第一列通常寫以下宣告,不寫也是有辦法執行,但通常會寫並加上此檔案用處或作者等資訊: ```bash= #! /bin/bash ``` ==注意==:為節省篇幅,以下程式碼都不加這一列。 ## 五、執行 有執行權限的話,執行的指令會比較輕鬆。以下指令給檔案加上執行權限。 ```bash= # 寫完 sh 檔案後執行以下,給檔案增加可執行權限給所有人 chmod +x file.sh # 寫完 sh 檔案後執行以下,給檔案增加可執行權限給自己帳號 chmod u+x file.sh ``` 執行 ```bash= # 有執行權限的檔案,可如下執行 ./file.sh # 沒有的話這樣也可以執行 sh file.sh ``` # 乙、基礎 ## 一、註解 ### 1. 單行註解 從 `#` 符號後面到結束(換行字元)是註解。 ```bash= # 這裡是註解 # 這也是註解 echo Hi # 這一列會印出 Hi,井字號開始到行尾是註解 ``` ### 2. 多行註解 使用 `: '...'` 實現多行註解。 ```bash= : ' 這是多行註解第一行 這是多行註解第二行 這是多行註解第三行 ' ``` ## 二、顯示文字 ### 1. 一行文字 `echo` 指令可顯示文字到終端機畫面上,搭配其他指令也可做複雜運用。`echo` 會自帶斷行字元在最後,如果不需要自動換行就加 `-n`。 即使是沒有要讓人看的執行檔,例如排程的作業,`echo` 也可以產生文字來製作 log,非常常用。 ```bash= echo Hello echo World echo -n Demo # 這裡加參數不產生換行字元來結束,所以下一個 echo 會接在這後面 echo bash script ``` 結果: ``` Hello World Demobash script ``` ==注意==:如果利用其他指令搭配 `echo`,可能會因為換行字元而出現非預期結果,所以常使用 `-n`。 ### 2. 空白行、換行 如果只有 `echo`,就是換行,可作為排版用: ```bash= echo echo ``` 有的情況是上個動作游標並不會回到第一個字元,加 `echo` 可強迫換行回到一開始,改善畫面上的顯示結果。 ### 3. 跳脫字元 `echo` 指令中如果要讓 `\n` 等可以發揮作用,要使用 `-e` 參數,比較以下: ```bash= # 這裡不會讓換行字元生效 echo "Hello\nWorld" # 這裡會生效 echo -e "Hello\nWorld" ``` ``` Hello\nWorld Hello World ``` 上面第二次的 HelloWorld 就有在 `\n` 處換行了。 ### 6. 顯示顏色 與上面相同,若使用 `\e` 或 `\033`,並使用 `echo -e` 就可以輸出顏色。 ```bash= # 完整印出來,不會有顏色 echo "Bash\e[1;33mScript\e[m" # 因為色碼而印出亮黃色字 echo -e "Bash\e[1;33mScript\e[m" ``` 結果: ``` Bash\e[1;33mScript BashScript ``` 第二列的 Script 會是亮黃色 ==注意==: * 若是用在 `PS1` 的自訂提示字元,顏色碼的前後要用 `\[` 和 `\]` 包起來,才不會因為計算字數時出錯造成游標異常。例如 `PS1="\e[1;33m\u@\h\$ \e[m"` 會有游標問題,用 `PS1="\[\e[1;33m\]\u@\h\$ \[\e[m\]"` 就不會。 * `\033[m` 可能在某些系統不能使用,似乎 `\e[m` 比較泛用。 ### 5. 文字檔 `cat` 指令可倒出文字檔的內容。 ```bash= cat demo.txt ``` 若不是全文都要使用,常會搭配 `grep`、`head` 等 pipeline 運用。 ## 三、分行與合併 可以將多行用分號 `;` 合併。 ```bash= echo 第一行訊息 echo 第二行訊息 # 以上兩列的寫法,可改用以下一列的寫法 echo 第一行訊息; echo 第二行訊息 ``` ```bash= if [ $var -eq 0 ] then echo var等於0 fi # 以上可用以下寫法 if [ $var -eq 0 ]; then echo var等於0 fi ``` 原則上 Shell 以 `LF` 和 `;` 拆分不同指令。 ## 四、設定變數 直接使用等號 `=` 來設定變數。取用時變數名稱前加上 `$` 符號,且用 `{}` 包起來更好。 ==注意==:等號前後不能有空白,例如給與變數 count 值 1,要寫 `count=1`,不能寫 `count = 1`。 ### 1. 簡易型 ```bash= title=範例 echo $title echo ${title} echo "標題為:${title}" ``` 結果: ``` 範例 範例 標題為:範例 ``` ### 2. 有特殊字元 但是變數值如果有空白等特殊符號,必須加引號。習慣上都會加雙引號,編輯器的提示也會比較清楚。 ```bash= title="範例 示範基本的變數存取" echo "標題為:${title}" ``` 結果: ``` 標題為:範例 示範基本的變數存取 ``` ### 3. 刪除變數 使用 `=` 會賦值,如果不要了可以使用 `unset` 指令來清空。 ```bash= message="Hello" echo ${message} unset message echo ${message} message="Bye" echo ${message} ``` 結果: ``` Hello Bye ``` 雖然清空內容也可以使用 `message=""`,但變數仍然佔用記憶體,且可能污染影響後面作業,若要確實刪除變數,就使用 `unset` 指令。另外,清除可以一次處理多個變數。 ```bash= # 清除三個變數 unset var1 var2 var3 ``` ## 五、引號 ### 1. 雙引號 " 字串若有特殊符號像空白字元或驚嘆號,要用雙引號包起來。 ```bash= title="Hello world! demo" echo ${title} ``` 結果: ``` Hello world! demo ``` 雙引號之間可以取值出來,以下範例將 `echo` 的字串加上雙引號,一樣可以帶出 `title` 的值。 ```bash= title="Hello world! demo" echo ${title} echo "${title}" ``` 結果: ``` Hello world! demo Hello world! demo ``` ### 2. 單引號 ' 單引號中是不會處理的,寫什麼文字就是什麼文字。 ```bash= title="Hello world! demo" echo '${title}' ``` 結果: ``` ${title} ``` 如果文字中的特殊符號被視為指令而無法送出,例如驚嘆號會被視為歷史記錄指令,且內容不需要帶出而是完整呈現,就適合用單引號,例如 SFTP 登入帳密是 `myAcc` 和 `pwd!#%` ```bash= curl -k "sftp://192.168.2.5/home/myAcc/file.txt" --user 'myAcc:pwd!#%' ``` ### 3. 撇引號 ` 有需要執行 shell 取得東西,就要寫在撇引號裡面。 ```bash= dt_now=`date` echo "完整現在日期時間:${dt_now}" echo "發生時間:`date +%F_%T`" ``` 結果: ``` 現在日期時間:Mon Apr 22 10:55:30 CST 2024 發生時間:2024-04-22_10:55:30 ``` 和下面的單小括號 `$()` 一樣效果。`$()` 是比較新的寫法,如果機器支援,普遍都用 `$()` 而不是較不好用的 ``。 ### 4. 中括號 [] 裡面放表達式做判斷用,要注意中括號要間隔空白字元,例如要寫 `[ ${var1} -eq 0 ]`,不能寫 `[${var1} -eq 0]`。 ```bash= read -p "請輸入指令(Q離開):" choice # 若輸入 Q 則結束 if [ "${choice}" == "Q" ] then exit fi ``` 很常見到使用雙中括號 `[[ ]]` 的做法,在某些情況這樣才不會出錯,例如判斷正則表示式的時候,後面會說明。另外要看更詳細的表達式判斷,可查閱 `[` 的文件。 ==注意==:雙中括號 `[[]]` 在較舊版本的 Bash Shell 可能無法使用,要小心。 ```bash= # 看更多說明 man [ ``` ### 5. 雙中括號 [[]] `[[` 與 `]]` 是Bash shell 特有的擴展語法,可支援 `*` 或 `?` ,有更強的字串比對能力。 ### 6. 單小括號 $() 同撇引號``,取執行的結果。 ```bash= login_name=$(whoami) echo "現在登入的是 ${login_name}" ``` 結果: ``` 現在登入的是 cyber ``` 比起撇引號,`$()` 在多層的時候比較容易使用且好讀,而撇引號需要使用 `\` 跳脫。機器支援的話普遍使用 `$()`。 ### 7. 雙小括號 $(()) 有要做計算時將表達式用 `$(())` 包起來。裡面取得變數值時可加 `$` 或 `${}`,也可不加。 ```bash= num1=9 num2=7 echo "num1 = ${num1}, num2 = ${num2}" num3=$((num1 + num2)) echo "num1 加 num2 等於 ${num3}" num3=$((${num3} * 5)) echo "再 5 倍得 ${num3}" ``` 結果: ``` num1 = 9, num2 = 7 num1 加 num2 等於 16 再 5 倍得 80 ``` ## 六、陣列 ### 1. 宣告 有兩種宣告、並一開始就賦值的作法 ```bash= # 用小括號放入值,以空白隔開。 names=('Adam' 'Billy' 'Carl') ``` ```bash= # 同上用小括號,以空白隔開,加入指定編號 names=([0]='Adam' [1]='Billy' [2]='Carl') ``` ```bash= # 宣告空的陣列 names=() ``` ### 2. 賦值 `array_name[index]=new_value` 做賦值。 ```bash= names=('Adam' 'Billy' 'Carl') names[3]='Denise' names[4]='Elizabeth' ``` ### 3. 附加 對陣列變數使用 `+=()` 可持續增加,也可一次附加多個。 ```bash= names=() names+=('Adam') names+=('Billy') names+=('Carl') names+=('Denise' 'Elizabeth') ``` ### 4. 取值 使用 `$` 加 `array_name[index]` 取值。 ==注意==:要用 `${array_name[index]}`,不能用 `$array_name[index]` ```bash= names=('Adam' 'Billy' 'Carl') names[3]='Denise' names[4]='Elizabeth' echo "${names[0]}" echo "${names[1]}" echo "${names[2]}" echo "${names[3]}" echo "${names[4]}" ``` 結果: ``` Adam Billy Carl Denise Elizabeth ``` 以上取值也可以使用 `for in` 和 `array_name[@]`,結果是一樣的。 ```bash= names=('Adam' 'Billy' 'Carl') names[3]='Denise' names[4]='Elizabeth' for name in ${names[@]} do echo "${name}" done ``` ### 5. 陣列長度 使用 `${#array_name[@]}` 可取得元素個數。 ```bash= names=('Adam' 'Billy' 'Carl') names[3]='Denise' names[4]='Elizabeth' # 取得元素個數 echo "共 ${#names[@]} 個名字" ``` 結果: ``` 共 5 個名字 ``` ### 6. 陣列編號 使用 `${!array_name[@]}` 可取得從 0 開始的連續編號,可結合 `for` 做多個陣列的取值。 ```bash= names=('Adam' 'Billy' 'Carl' 'Denise' 'Elizabeth') # 印出從 0 開始的編號 echo ${!names[@]} # 用編號遍尋陣列,印出每一個項目 for i in ${!names[@]} do echo ${names[${i}]} done ``` ``` 0 1 2 3 4 Adam Billy Carl Denise Elizabeth ``` ### 7. for in 空白 如果陣列資料有空白時,要小心空白可能會拆開成不同的值,比較下面: ```bash= names=('Andrew Adam' 'Billy Brown' 'Cindy Clark') for name in ${names[@]} do echo "${name}" done ``` 會得到 ``` Andrew Adam Billy Brown Cindy Clark ``` 若 `for in` 時有加上雙引號,就不會因為空白而拆分: ```bash= names=('Andrew Adam' 'Billy Brown' 'Cindy Clark') for name in "${names[@]}" do echo "${name}" done ``` 會得到 ``` Andrew Adam Billy Brown Cindy Clark ``` # 丙、輸出輸入 ## 一、從終端機取得輸入 ### 1. 一般輸入 使用 `read` 取得使用者輸入字串,輸入結果會賦值到後面的變數,下面示範是 acc。(可直接 `read` 就有變數並賦值了,不需要有宣告的動作) ```bash= echo -n "請輸入帳號:" read acc echo "歡迎 ${acc} 使用。" ``` 結果: ``` 請輸入帳號:cyber 歡迎 cyber 使用。 ``` ### 2. 提示輸入 使用 `read` 與 `-p` 可同時提示字串並取得使用者輸入字串。 ```bash= # 提示並取得輸入結果 read -p "請輸入帳號:" acc echo "歡迎 ${acc} 使用。" ``` 結果: ``` 請輸入帳號:cyber 歡迎 cyber 使用。 ``` ### 3. 密碼輸入 `read` 的 `-s` 參數可以不顯示輸入的內容,適合做密碼的輸入,或是不需要看到輸入內容的特殊處理。 ==注意==: * 若要隱藏輸入內容的 `-s` 又要有 `-p` 的提示文字,`s` 要在 `p` 的前面。 * `-s` 參數結束時不會換行,若想要好看,就要自己加 `echo` 額外換行。 ```bash= # 提示並取得輸入結果 read -sp "請輸入密碼:" pwd # 刻意增加一個換行動作 echo echo "你輸入的是 ${pwd}。" ``` 結果: ``` 請輸入密碼: 你輸入的是 123。 ``` ### 4. 限制長度 如果有限制長度,例如只接受一個字元,可以使用 `-n 長度`。字數達到的時候就會取得,不過要注意不會換行,有需要就要自己加。 ```bash= read -p "請輸入字元(y/n):" -n 1 choice echo echo "你輸入的是 ${choice}" ``` ``` 請輸入字元(y/n):y 你輸入的是 y ``` ### 5. 多變數輸入 `read` 後可接多個變數,使用空白字元隔開 ```bash= read -p "請輸入名稱、年齡、成績,以空白字元隔開:" name age score echo "姓名[${name}],年齡[${age}]歲,成績[${score}]分" ``` 結果: ``` 請輸入名稱 年齡 成績,以空白字元隔開:John 17 95 姓名[John],年齡[17]歲,成績[95]分 ``` ### 6. 避免轉義 使用 `read` 指令加上 `-r` 參數時,會忠實將字串讀入,不會因為 `\` 而轉義,在後面應用 `read` 於文字檔案處理時很重要。 參考以下範例比較差異: ```bash= # 輸入反斜線會被轉義 read -p "請輸入帶有反斜線的文字:" input echo "${input}" echo -e "${input}" echo # 避免轉義,會完整讀取進來 read -r -p "請輸入帶有反斜線的文字:" input echo "${input}" echo -e "${input}" ``` ``` 請輸入帶有反斜線的文字:HELLO\nWORLD HELLOnWORLD HELLOnWORLD 請輸入帶有反斜線的文字:HELLO\nWORLD HELLO\nWORLD HELLO WORLD ``` 以上第一次輸入 `HELLO\nWORLD` 時中間的反斜線被轉義了,使得 `input` 變數的反斜線消失,內容只剩下 `HELLOnWORLD` ,而第二次有加上 `-r` 參數,會完整讀入內容為 `HELLO\nWORLD`,最後的 `echo -e` 再將 `\n` 轉成換行字元。 ## 二、計算賦值 等號 `=` 可以將右邊的值給左邊的變數,若右邊是會計算的式子,可用雙小括號 `$(())` 包起來。 ```bash= count=0 count=$((count + 1)) ``` ## 三、文字檔 ### 1. 輸出到文字檔 `>` 符號可將結果輸出到檔案。`>>` 符號可將結果附加到檔案。若檔案不存在會自動建立。 ```bash= # 目標檔案內容會被清空,完全變成要放入的文字 echo "Hello world" > "result.txt" # 目標檔案內容不會清空,會保留原來的,往後面附件新的文字 echo "New line" >> "result.txt" ``` 雖然檔案會自動建立,但是路徑若有資料夾但是資料夾不存在,還是會發生錯誤,在路徑有資料夾的情況下可以加 test 檢查資料夾是否存在,提高程式碼的品質。 ### 2. 從文字檔輸入 通常文字檔都很多列,所以會用迴圈 `for` 或 `while` 來處理,後面筆記更多描述。以下範例是使用 `<` 將文字檔導入給 `while` 與 `read` 讀出文字檔的內容。 ```bash= while read line do echo "${line}" done < "result.txt" ``` ``` Hello world New line ``` ## 四、取自執行結果 ### 1. 一列 完整取出 若執行結果只有一列,可以使用 `$()` 執行後賦予給變數。 下面示範執行 `whoami` 取得登入的帳號,賦值給 `account` 變數。與執行 `date` 得到機器的日期時間,賦值給 `nowtime` 變數。 ```bash= account="$(whoami)" nowtime="$(date)" echo "目前登入的帳號是 ${account},日期時間為 ${nowtime}" ``` 結果: ``` 目前登入的帳號是 cyber,日期時間為 Fri Nov 22 17:03:35 CST 2024 ``` ### 2. 一列 要拆分 以查詢檔案或資料夾大小的指令 `du` 為例,會列出大小和檔案本身 ```bash= # -b 參數會顯示多少 byte du -b example.txt ``` 得到一列的結果,用空白分成兩塊 ``` 2610 example.txt ``` 若要取得空白分隔的第一塊的 2610,可將結果用 `<<<` 和 `read -a` 傳給陣列變數再依位置取出來。 ```bash= read -a size_info <<< "$(du -b example.txt)" echo "example.txt 大小為 ${size_info[0]} bytes" ``` ``` example.txt 大小為 2610 bytes ``` ### 3. 多列 < <() 執行結果不只一列時,可以使用 `< <(指令)` 來取得執行結果,導向給 `while` 與 `read` 一列一列給 `line` 變數。 下面示範有一個指令 `oc get pods` 會印出如下像是表格的文字: ``` $ oc get pods NAME READY STATUS RESTARTS AGE my-app-bnvdb 1/1 Running 8 (28m ago) 31d web-server-ff6d4 3/3 Running 2 (28m ago) 31d my-logger-b967f 1/1 Running 2 (51m ago) 47d $ ``` ```bash= # read 指令加上 -r 參數不轉義,會完整讀入內容 while read -r line do echo "${line}" done < <(oc get pods) ``` ==注意==: * `< <` 中間要空白 * `<(` 中間要相連 第一個 `<` 是重導向符號,後面的 `<()` 是一組的(稱作 Process Substitution)。 以上表格形式的資料還可以運用陣列,使用 `read -a` 將 `line` 拆解成各個項目給 `items` 陣列。下面示範取出每一列第一個項目 NAME 並印出來。 ```bash= while read -r line do read -a items <<< "${line}" echo "${items[0]}" done < <(oc get pods) ``` 結果: ``` NAME my-app-bnvdb web-server-ff6d4 my-logger-b967f ``` ### 4. 多列 | 也可以使用 pipeline `|` 先執行指令,將結果導向給 `while` 與 `read`。 ```bash= oc get pods | while read -r line do read -a items <<< "${line}" echo "${items[0]}" done ``` 結果同上面。 ## 五、傳入引數 執行 bash shell 指令時可傳入引數,增加控制的彈性。 ### 1. 各別引數 $1 $2 使用 `$1`、`$2` …… 等取引數。例如以下 `demo.sh` 內容為: ```bash= #! /bin/bash var1=${1} echo "輸入的第一個引數為${1},第二個為${2}" echo "取得變數為${var1}" ``` 執行時有沒有用雙引號包住傳入的字串,可看出差異: ```bash= ./demo.sh hello 123 world 輸入的第一個引數為hello,第二個為123 取得變數為hello ./demo.sh "hello 123" world 輸入的第一個引數為hello 123,第二個為world 取得變數為hello 123 ``` `$n` 是base on 1 從 1 開始,`$0` 是指檔案本身,下面會介紹。 ### 2. 所有引數 $@ 使用 `$@` 取得所有引數,可使用 `for in` 各別取出來,如下是 process.sh 內容 ```bash= #! /bin/bash echo '傳入檔案為:'${@} for arg in ${@} do echo "處理${arg}..." done ``` 執行結果: ```bash= ./process.sh "web.config" "server.conf" "appsettings.json" 傳入檔案為:web.config server.conf appsettings.json 處理web.config... 處理server.conf... 處理appsettings.json... ``` ### 3. 引數個數 $# `$#` 會得到傳入引數的個數,可用 `if` 來判斷傳入內容是否符合繼續往下做的條件,例如以下是 `info.sh` 內容。 ```bash= if [ ${#} -ne 3 ] then echo "錯誤,需要三個值" exit fi echo "學生${1},年齡${2},主修${3}" ``` ``` ./info.sh Mario 17 Math 學生Mario,年齡17,主修Math ./info.sh Luigi 16 錯誤,需要三個值 ``` ### 4. 指令本身 $0 `$0` 會得到指令本身,例如用 `./process.sh web.config` 時,`${0}` 會得到 `./process.sh`,可以應用在提示時,避免日後檔案名稱異動時還要修改內容。 ```bash= # 若沒有傳入引數 if [ -z "${1}" ] then # 用 ${0} 來顯示執行的指令本身 echo "請傳入引數,例如 ${0} web.config" exit fi ``` ``` ./process.sh 請傳入引數,例如 ./process.sh web.config ``` ### 5. 引數位移 shift `shift` 指令會讓傳入的引數少去一個,全部往前挪動,也有 `shift N` 一次挪動 N 個的用法。看以下範例 `demo.sh` ```bash= echo "傳入引數為:\$1=${1},\$2=${2},\$3=${3}" # 挪一個引數 shift echo "(執行 shift)" echo "傳入引數為:\$1=${1},\$2=${2},\$3=${3}" # 挪兩個引數 shift 2 echo "(執行 shift 2)" echo "傳入引數為:\$1=${1},\$2=${2},\$3=${3}" ``` 結果: ``` ./demo.sh a b c d e f g 傳入引數為:$1=a,$2=b,$3=c (執行 shift) 傳入引數為:$1=b,$2=c,$3=d (執行 shift 2) 傳入引數為:$1=d,$2=e,$3=f ``` 第一次執行 `shift` ,本來的 `$1` 消失,`$2` 的值給 `$1`,`$3` 的值給 `$2`,以此類推。 第二次時執行的是 `shift 2`,一次挪兩個引數,所以 `$1` 和 `$2` 消失,而 `$3` 和 `$4` 往前挪。 `shift` 主要應用應該是管理,在下一個項目會說明,這裡在提供一個範例,檔名 `calc.sh`(不過用到了判斷流程的 `if` 和迴圈的 `while`,不熟的話可看後面項目): ```bash= if [ "${1}" == "+" ] then # 傳入的第一個引數是加號,就將剩餘所有引數加起來 result=0 while [ ${#} -gt 1 ] do result=$((result + ${2})) shift done echo "連加結果為 ${result}" elif [ "${1}" == "x" ] then # 傳入的第一個引數是x,就將剩餘所有引數乘起來 result=1 while [ ${#} -gt 1 ] do result=$((result * ${2})) shift done echo "連乘結果為 ${result}" fi ``` 結果: ``` ./calc.sh + 1 2 3 4 5 6 連加結果為 21 ./calc.sh x 1 2 3 4 5 6 連乘結果為 720 ``` ### 6. 引數管理 操作 Linux 時應該都體驗過像是 `-f xxx`、`--output xxx`、`--verbose` 等傳入引數的做法,這個就可以用 `shift` 來達成,假設檔案 `process.sh` 希望 * `--file` 或 `-f` 指定檔案 * `--type` 或 `-t` 指定類型為 A 或 B * `--verbose` 或 `-v` 讓作業顯示細節 ```bash= show_detail=0 # 有引數就走進去迴圈判斷是哪一種 while [ ${#} -gt 0 ] do if [ "${1}" == "--file" -o "${1}" == "-f" ] then # 是 --file 或 -f,就將傳入的下一個引數給 process_file 變數,位移兩個引數 if [ -f "${2}" ] then process_file="${2}" shift 2 else echo "檔案${2}不存在,結束作業" exit fi elif [ "${1}" == "--type" -o "${1}" == "-t" ] then # 是 --type 或 -t,就將傳入的下一個引數給 process_type 變數,位移兩個引數 if [ "${2}" == "A" -o "${2}" == "B" ] then process_type="${2}" shift 2 else echo "${2} 為預期外的類型,結束作業" exit fi elif [ "${1}" == "--verbose" -o "${1}" == "-v" ] then # 是 --verbose 或 -v,,位移一個引數 show_detail=1 shift else # 都不是就結束 echo "預期外的引數:${1},結束作業" exit fi done echo "將處理檔案 ${process_file},處理類型為 ${process_type}" if [ ${show_detail} -eq 1 ] then echo "將顯示作業細節" else echo "將隱藏作業細節" fi ``` 結果: ``` ./process.sh --type A -f data.txt --verbose 將處理檔案 data.txt,處理類型為 A 將顯示作業細節 ./process.sh --file "data.txt" -v --type B 將處理檔案 data.txt,處理類型為 B 將顯示作業細節 ./process.sh -t A --file "data.txt" 將處理檔案 data.txt,處理類型為 A 將隱藏作業細節 ``` 以上可以看出,傳入引數時是先給 `--file` 還是先 `--type` 並不影響,是不錯的管理作法。 ## 六、傳入步驟 像是 SFTP 等指令,會有一步步輸入的需求,可以使用 Here Document 的做法: ### 1. HereDocument 基礎 假設 `sftp -i ~/.ssh/id_rsa acc@192.168.1.2` 使用私鑰登入後,依序要輸入 ``` cd /file/public get myfile.txt bye ``` 可以使用 `<<標識符號` 來傳入下一列開始直到標識符號之間的指令: ```bash= sftp -i ~/.ssh/id_rsa acc@192.168.1.2 <<EndOfCommand cd /file/public get myfile.txt bye EndOfCommand echo "完成下載檔案" ``` ==注意==: * 標識符號習慣上使用 `EOF`,他只是一個標籤,不要重覆出現即可。 * 要注意 `<<` 和 `標識符號` 中間不可以有空白。 ### 2. 排版 如果要在指令加上縮排(TAB字元)增加可讀性,則傳入標識符時要加上減號,使用 `<<-`,就會排除每一列最前方的縮排字元。若寫在腳本檔中使用 `TAB` 按鈕產生縮排字元即可,如果是在 bash 中手動執行要先按 `Ctrl` + `V` 再按 `TAB` 鍵來產生縮排字元。 ```bash= # 以下縮排必須是 TAB 產生的縮排字元,而不是四個空白 sftp -i ~/.ssh/id_rsa acc@192.168.1.2 <<-EOF cd /file/public get myfile.txt bye EOF echo "完成下載檔案" ``` ### 3. 外部指令檔 可將指令編寫在另一個檔案,例如 `command.txt`: ``` cd /file/public get myfile.txt bye ``` 然後使用 `<` 重導向,來傳入指令檔案。 ```bash= sftp -i ~/.ssh/id_rsa acc@192.168.1.2 < command.txt ``` # 丁、流程 ## 一、判斷式 ### 1. if 使用 `if [ condition ]` 、 `then` 、 `fi` 做單一判斷。 ==注意==: * condition 的前後中括號要留空白,像是 `[ $num -eq 10 ]` 可以,但 `[$sum -eq 10]` 不行 * `then` 和 `fi` 中間可縮排也可不縮排 * `[ ]` 常會用兩個 `[[ ]]`,例如表達式有出現 `[ ]` 像是正則表示式的時候,比較不會出現問題。 #### 範例(1) 數字的比較 ```bash= echo -n "請輸入數字:" read num if [ ${num} -gt 10 ] then echo "${num}大於10" fi echo "結束" ``` 輸入小於等於 10 的結果: ``` 請輸入數字:7 結束 ``` 輸入大於 10 的結果: ``` 請輸入數字:18 18大於10 結束 ``` #### 範例(2) 字串的比較 ==注意==: * 字串變數用雙引號包起來比較好,不包的話值一個字的時候是沒問題,若字串帶有空白等字元會出錯 * 字串的比較,**等於** 時要使用 `=`、`==` ,**不等於** 時要使用 `!=` ```bash= echo -n "請輸入密碼:" read code if [ "${code}" = "mario" ] then echo "輸入成功!" fi echo "結束" ``` 結果: ``` 請輸入密碼:luigi 結束 ``` 結果: ``` 請輸入密碼:mario 輸入成功! 結束 ``` ### 2. if else 使用 `if [ condition ]`、`then`、`else`、`fi` 做滿足與不滿足時的判斷。 #### 範例(1) 數字的比較 ```bash= echo -n "請輸入第一個數字:" read num1 echo -n "請輸入第二個數字:" read num2 if [ ${num1} -eq ${num2} ] then echo "${num1}等於${num2}" else echo "${num1}與${num2}不相等" fi ``` 輸入不同的數字時的結果: ``` 請輸入第一個數字:37 請輸入第二個數字:48 37與48不相等 ``` 輸入相同的數字時的結果: ``` 請輸入第一個數字:32 請輸入第二個數字:32 32等於32 ``` ### 3. if elif else 使用 `if [ condition1 ]`、`then`、`elif [ condition1 ]`、`then`、`elif [ condition2 ]`、`then`、...、`fi`,也可以加入 `else` 區塊。 ```bash= echo -n "請輸入星期的數字:" read choice message="" if [ ${choice} -eq 1 ] then message="星期一,猴子穿新衣" elif [ ${choice} -eq 2 ] then message="星期二,猴子肚子餓" elif [ ${choice} -eq 3 ] then message="星期三,猴子去爬山" elif [ ${choice} -eq 4 ] then message="星期四,猴子看電視" elif [ ${choice} -eq 5 ] then message="星期五,猴子去跳舞" elif [ ${choice} -eq 6 ] then message="星期六,猴子去斗六" else message="星期日,猴子過生日" fi echo $message ``` 結果: ``` 請輸入星期的數字:5 星期五,猴子去跳舞 ``` 結果: ``` 請輸入星期的數字:7 星期日,猴子過生日 ``` ### 4. if 排版 以下是排版參考,要注意的是縮成一列時 `then`、`else` 後面==不能有分號==。 #### (1) if then fi ```bash= if [ ${var} -eq 0 ] then echo "var 是 0" fi ``` ```bash= if [ ${var} -eq 0 ]; then echo "var 是 0" fi ``` ```bash= if [ ${var} -eq 0 ]; then echo "var 是 0"; fi ``` #### (2) if then else fi ```bash= if [ ${var} -eq 0 ] then echo "var 是 0" else echo "var 非 0" fi ``` ```bash= if [ ${var} -eq 0 ]; then echo "var 是 0" else echo "var 非 0" fi ``` ```bash= if [ ${var} -eq 0 ] then echo "var 是 0" else echo "var 非 0" fi ``` ```bash= if [ ${var} -eq 0 ]; then echo "var 是 0"; else echo "var 非 0"; fi ``` #### (3) if then elif then else fi ```bash= if [ ${var} -gt 2 ] then echo '大於 2' elif [ ${var} -lt 2 ] then echo '小於 2' else echo '等於 2' fi ``` ```bash= if [ ${var} -gt 2 ]; then echo '大於 2' elif [ ${var} -lt 2 ]; then echo '小於 2' else echo '等於 2' fi ``` ```bash= if [ ${var} -gt 2 ]; then echo '大於 2' elif [ ${var} -lt 2 ]; then echo '小於 2' else echo '等於 2' fi ``` ```bash= if [ ${var} -gt 2 ]; then echo '大於 2'; elif [ ${var} -lt 2 ]; then echo '小於 2'; else echo '等於 2'; fi ``` ### 5. case in 很多高階語言有 switch case 或是 SQL 裡有 case when 的語法,可用來取代大量的 if else,在 Bash Shell 中也有,也就是 `case`、`in`、`)`、`;;`、`esac`。將上面猴子範例改寫: ```bash= echo -n "請輸入星期的數字:" read choice message="" case ${choice} in 1) message="星期一,猴子穿新衣" ;; 2) message="星期二,猴子肚子餓" ;; 3) message="星期三,猴子去爬山" ;; 4) message="星期四,猴子看電視" ;; 5) message="星期五,猴子去跳舞" ;; 6) message="星期六,猴子去斗六" ;; *) message="星期日,猴子過生日" esac echo $message ``` 最後的 `*)` 是比對剩下的任何情形,可看做 Java C# 裡的 `default:`。 ## 二、表達式邏輯 ==注意==:以下中括號符號要留空白,例如 `[ ${num} -le 0 ]`,不能寫 `[${num} -le 0]` 以下列出常用的,更詳細的可輸入 `man test` 查看文件。 ### 1. 數字大於小於等於 `[ expression ]` 要空格 | | 符號 | 範例 | | -------- | -------- | -------- | | 大於 | `-gt` | `[ ${num1} -gt 10 ]` | | 小於 | `-lt` | `[ ${a} -lt ${b} ]` | | 等於 | `-eq` | `[ ${num1} -eq ${num2} ]` | | 不等於 | `-ne` | `[ ${num1} -ne 0 ]` | | 大於或等於 | `-ge` | `[ ${input} -ge 1 ]` | | 小於或等於 | `-le` | `[ ${num} -le 0 ]` | `((expression))` 可不用空格,取得變數可加也可不加 `$` 符號。 | | 符號 | 範例 | | -------- | -------- | -------- | | 大於 | `>` | `(( ${num1} > 10 ))` | | 小於 | `<` | `((a < b))` | | 等於 | `==` | `(( ${num1} == ${num2} ))` | | 不等於 | `!=` | `((num1 != 0))` | | 大於或等於 | `>=` | `(( ${input} >= 1 ))` | | 小於或等於 | `<=` | `((${num} <= 0))` | ### 2. 字串相等不相等 | | 符號 | 範例 | | -------- | -------- | -------- | | 等於 | `=` | `[ "${var1}" = "${var2}" ]` | | 等於 | `==` | `[ "${var1}" == "${var2}" ]` | | 不等於 | `!=` | `[ "${var1}" != "${var2}" ]` | | 長度非零 | `-n` | `[ -n "${var}" ]` | | 長度為零 | `-z` | `[ -z "${input}" ]` | ### 3. 且,或,否定 | | 符號 | 範例 | | -------- | -------- | -------- | | 且 AND | `-a` | `[ ${age} -ge 18 -a ${age} -le 65 ]` | | 或 OR | `-o` | `[ "${user}" = 'admin' -o "${user}" = 'root' ]` | | 否定NOT | `!`| `[ ! -x "start.sh" ] && echo "無法執行"` | ### 4. 檔案系統 | | 符號 | 範例 | | -------- | -------- | -------- | | 存在且是檔案 | `-f` | `[ -f "${1}" ]` | | 存在且是資料夾 | `-d` | `[ -d "./log" ]` | | 存在 | `-e` | `[ -e "${file}" ]` | | 存在且可讀取 | `-r` | `[ -r "./log" ]` | | 存在且可寫入 | `-w` | `[ -w "result.txt" ]`| | 存在且可執行 | `-x` | `[ -x "update.sh" ]` | ### 5. 正則表示式 | | 符號 | 範例 | | -------- | -------- | -------- | | 比對 | `=~` | `[[ ${input} =~ [yYnN] ]]` | 在 `[ ]` 中使用 `=~` 為測試是否符合正則表示式,並會產生符合的結果到 `BASH_REMATCH` 陣列變數裡,index 0 就是符合的文字。 ==注意==:RegEx 常會用中括號 `[ ]` 造成和外面的衝突,所以 test 常用兩個中括號 `[[ ]]`。 有一個文字檔資料 data.csv 如下 ``` Name,Age,Email,City Adam,15,adam1234@gmail.com,New York Billy,16,superman2008@mail.yahoo.com,Los Angeles Carl,9,,New York Denise,16,denis.smith@gmail.com,Detroit Elizabeth,15,Elizabeth_Boston@yahoo.com,Boston ``` 要用 RegEx 找出 Email,使用 `[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}` ```bash= while read line do if [[ "${line}" =~ [a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4} ]] then echo ${BASH_REMATCH[0]} fi done < "data.csv" ``` 結果: ``` adam1234@gmail.com superman2008@mail.yahoo.com denis.smith@gmail.com Elizabeth_Boston@yahoo.com ``` ### 6. 預設值 ### 7. test `test` 指令可以用 `[ ]` 來簡寫,例如下面兩個是一樣的: ```bash= if [ -f "log.txt" ] then echo "ok" fi ``` ```bash= if test -f "log.txt" then echo "ok" fi ``` 在某些情況要用 `test`,例如檔案在缺少權限的資料夾中又想檢視是否存在,需要 `sudo`: ```bash= # 會遇到 Permission Denied if [ -f "/root/folder/log.txt" ] then echo "ok" fi # 提示輸入 root 密碼後可見 if sudo test -f "/root/folder/log.txt" then echo "ok" fi ``` ## 三、迴圈 for ### 1. 基礎 使用 `for in`、`do`、`done` 做計次迴圈。 #### 範本(1) 印 1 到 10 連續整數 ==注意==:似乎 `{a..b}` 是比較新的 Bash 才有的寫法。 ```bash= for i in {1..10} do echo "第${i}列" done ``` 結果: ``` 第1列 第2列 第3列 第4列 第5列 第6列 第7列 第8列 第9列 第10列 ``` #### 範本(2) 印 1 到 10 間隔 也可以加入間隔: ```bash= for i in {1..10..4} do echo $i done ``` 結果: ``` 1 5 9 ``` #### 範本(3) 九九乘法 巢狀迴圈 ```bash= for i in {2..9} do for j in {1..9} do echo "$i * $j = $((i * j))" done done ``` 結果: ``` 2 * 1 = 2 2 * 2 = 4 2 * 3 = 6 2 * 4 = 8 2 * 5 = 10 2 * 6 = 12 2 * 7 = 14 2 * 8 = 16 2 * 9 = 18 3 * 1 = 3 3 * 2 = 6 ... 9 * 8 = 72 9 * 9 = 81 ``` ### 2. 讀取文字檔 執行 `cat` 文字檔案的結果可以 `for in` 一列一列讀取出來 ```bash= c=0 for line in $(cat "demo.txt") do c=$((c + 1)) echo "第${c}列:${line}" done ``` 或 ```bash= c=0 for line in `cat "demo.txt"` do c=$((c + 1)) echo "第${c}列:${line}" done ``` ### 3. for ; ; 形式 C 或 Java C# 等高階語言的 for 形式,用雙小括號包住 `((初始值;判斷式;步數間隔))` ```bash= for ((i=0; i<=100; i=i+7)) do echo -n "${i}, " done ``` 結果: ``` 0, 7, 14, 21, 28, 35, 42, 49, 56, 63, 70, 77, 84, 91, 98, ``` ### 4. for seq 如果 `for` 的開始結束是變數,要搭配 `seq` ```bash= num1=7 num2=15 for i in $(seq ${num1} ${num2}) do echo $i done ``` 結果: ``` 7 8 9 10 11 12 13 14 15 ``` ==注意==:雖然 `seq n m` 也好用,但確切知道範圍時,使用 `{n..m}` 的效率會比較好,尤其是大數字的時候。 ### 5. 遍歷陣列 陣列可以使用 `for`、`in` 和 `${!array_name[@]}` 來取得編號,可用來遍歷項目,確定項目一樣多的時候可用來在多個陣列中取值。 ```bash= ids=('s0024' 's0179' 's1076') names=('Adam' 'Billy' 'Carl') for i in ${!ids[@]} do echo ${ids[i]} ${names[i]} done ``` 結果: ``` s0024 Adam s0179 Billy s1076 Carl ``` ## 四、迴圈 while ### 1. 基礎 使用 `while [ condition ]`、`do`、`done` 做條件迴圈 ```bash= # 費氏數列 f1=0 f2=1 f3=1 echo -n ${f1}, ${f2} while [ ${f3} -lt 1500 ] do f3=$((f1 + f2)) f1=${f2} f2=${f3} echo -n ", ${f3}" done echo echo "費氏數列結束" ``` 結果: ``` 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597 費氏數列結束 ``` ### 2. 跳過與中斷 `while` 中的 `continue` 可以跳過,而 `break` 可以中斷並跳出。 ```bash= ``` ### 3. 無窮迴圈 `while :` 則進入無窮迴圈,`break` 會讓此層迴圈結束。例如前面費氏數列的例子可改寫成以下: ```bash= # 費氏數列無窮迴圈寫法 f1=0 f2=1 f3=1 echo -n ${f1}, ${f2} while : do f3=$((f1 + f2)) f1=${f2} f2=${f3} echo -n ", ${f3}" if [ ${f3} -ge 1500 ] then break fi done echo echo "費氏數列結束" ``` ### 4. 讀取文字檔 用重導向 `<` 將檔案餵給 `while`,使用 `read` 讀出每一列文字給變數(以下為 `line`): ```bash= while read line do echo ${line} done < "demo.txt" ``` 或是 `cat` 搭配 `|` 導向 ```bash= cat "demo.txt" | while read line do echo ${line} done ``` ==注意==:執行 `cat` 導給 `while` 的作法效率較差,會將文字存入記憶體做使用,而 `<` 逐行讀取較好。 ## 五、結束 Shell ### 1. 終止並給定 Exit code 指令 `exit` 可停掉這個 Shell,也可接一個 `0` 到 `255` 的數字作為 Exit code 給原先的 Shell (parent shell)做為判斷用。 ```bash= # 直接離開 if [ ${count} -eq 0 ] then exit fi # 丟出 3 的 Exit code 給 parent shell if [ "${msg}" -ne "" ] then exit 3 fi ``` ### 2. 取得 Exit code 使用 `$?` 或 `${?}` 可取得子 shell 的 Exit code ```bash= # 前面先執行其他 shell if [ ${?} ne 0 ] then echo "發生錯誤" fi ``` 另外賦值比較不會跑掉: ```bash= # 執行其他 shell exit_code=${?} if [ ${exit_code} ne 0 ] then echo "發生錯誤,返回碼為${exit_code}" fi ``` ## 六、串接作業 ### 1. && 成功才往後做 若有三個執行檔 task1、task2 和 task3 task1: ```bash= #! /bin/bash if [ -f "log.txt" ] # 當存在此檔案時為 true then exit 0; else exit 1; fi ``` task2: ```bash= #! /bin/bash cat "log.txt" ``` task3: ```bash= #! /bin/bash ./task1 && ./task2 ``` 執行 task3 時,當存在 log.txt 時會印出內容,因為 task1 回傳 Exit Code 0 表示 task1 成功,而 `&&` 在左邊 task1 成功時會往後做 task2,就印出 log.txt 的內容;若不存在 log.txt 檔案,執行 task3 會因為 task1 回傳 Exist Code 1 表示失敗,`&&` 就不會往下執行 task2,不像直接執行 task2 時會看到缺少 log.txt 檔案的訊息。 以上應用可以寫在一起: ```bash= [ -f "log.txt" ] && cat "log.txt" ``` ### 2. || 失敗才往後做 若是 `||` 就是失敗了才往後,當前面成功時則結束。 ```bash= echo "123" || echo "456" ``` 結果: ``` 123 ``` 前者印出 `123` 已經成功了,就不會往後面做印出 `456`。 ### 3. 組合多重邏輯 `&&` 可和 `||` 組合,例如: ```bash= [ -f "log.txt" ] && cat "log.txt" || echo "缺少檔案" ``` 當存在 log.txt 且為檔案時,印出內容,若失敗則印出「缺少檔案」,這種「執行 cmd1,成功時執行 cmd2,失敗時執行 cmd3」可用: `cmd1 && cmd2 || cmd3`,處理多重邏輯時常見。 ### 4. & 背景處理 :::success //TODO ::: ### 5. | 串接往後餵 使用 `|` 可將左邊的結果給右邊繼續處理,非常常用。 #### (1) 示範 假設先有一個 demo 檔案,內容讀取輸入再印出來,如下: ```bash= #! /bin/bash # demo.sh 內容 # 讀取使用者輸入 read text # 印出使用者輸入文字到畫面上 echo echo "輸入的內容是:${text}" echo ``` 如果只是直接執行並輸入文字「測試」,結果當然是這樣: ``` $ ./demo.sh 測試 輸入的內容是:測試 $ ``` 但若做 `echo` 「測試測試」再 `|` 給 demo,就會變成: ``` $ echo "測試測試" | ./demo.sh 輸入的內容是:測試測試 $ ``` #### (2) 簡單應用 ```bash= # 列出 State 是 TIME_WAIT 的網路連線 netstat | grep 'TIME_WAIT' # 將文字做 base64 編碼 echo -n 'Hello world' | base64 # 列出此資料夾下所有檔名有 .sh 的檔案 find . | grep '.sh' ``` ## 七、延遲 `sleep` 指令可讓作業等待指定的時間,單位是秒,可傳入浮點數。 ```bash= echo "第一列" # 等待 2 秒 sleep 2 echo "第二列" # 等待 500 毫秒 sleep 0.5 echo "第三列" ``` # 戊、文字處理 ## 一、截斷、子字串 ### 1. 基本 使用 `${parameter:offset:length}`(意義為 `${變數名:起始位置:長度}`)來取部份字串,起始位置是 base 0,若 `${變數名:起始位置}` 沒有指定長度,就取到結束。 ==注意==:取到結束的寫法要小心取到斷行字元出現預期外的結果。 ```bash= msg="20240422.log" echo "記錄日期為 ${msg:0:4} 年 ${msg:4:2} 年 ${msg:6:2} 日" echo "副檔名為 ${msg:9}" ``` 結果: ``` 記錄日期為 2024 年 04 年 22 日 副檔名為 log ``` ### 2. 起始位置為負數 起始位置 offset 若為負數,就從尾端往左數,如果沒有指定長度 length 可以直接理解成「**負多少就取多少字**」,例如 `-3` 就取 3 個字。 ==注意==:`:` 和 `-` 中間要空格,否則會被視為 `:-` 的預設值處理。 ```bash= file="stdout_20240422.log" echo "副檔名為 ${file: -3}" echo "日期為 ${file: -12:4} 年 ${file: -8:2} 月 ${file: -6:2} 日" ``` 結果: ``` 副檔名為 log 日期為 2024 年 04 月 22 日 ``` 起始位置使用負數的作法,常用於取尾端資料的時候。 ### 3. 長度為負數 長度 length 若為負數,就取到倒數的位置,例如 `-3` 就取到倒數 3 個字 ==前==,結果是取到倒數第 4 個字。可理解成「**負多少就取到完但是留多少字不取**」。 ```bash= file="stdout_20240422.log" echo "類型為 ${file:0:-13},日期為 ${file: -12:8}" file="localhost_access_log_20240422.log" echo "類型為 ${file:0:-13},日期為 ${file: -12:-4}" ``` 結果: ``` 類型為 stdout,日期為 20240422 類型為 localhost_access_log,日期為 20240422 ``` 可以起始位置 offset 和長度 length 同時使用負數,像上面例子 offset -12 則取後面 12 字,又 length -4 則留 4 字不取。 長度使用負數的作法,常用於要避開尾端剩餘字元的時候。 ## 二、文字檔特定列數 以下範例文字檔 demo.txt: ``` 第一列 Hello world hi 第二列 你好 第三列 這是 Linux bash 筆記示範文字 第四列 5555 第六列 Line 7 ``` ### 1. 印出文字與列數 `cat` 指令可倒出文字檔內容,加 `-n` 參數可一起印出列數。 ```bash= cat "demo.txt" echo cat -n "demo.txt" ``` 結果: ``` 第一列 Hello world hi 第二列 你好 第三列 這是 Linux bash 筆記示範文字 第四列 第六列 Line 7 1 第一列 Hello world hi 2 第二列 你好 3 第三列 這是 Linux bash 筆記示範文字 4 第四列 5 5555 6 第六列 7 Line 7 ``` ### 2. 後幾列 如果有大量的文字,例如 log 記錄檔,但只要截取最新**最後的幾列**,可用 `tail` 指令直接閱讀檔案: ```bash= # 使用 -n 加數字 tail -n 5 "demo.txt" # 直接使用 -數字 tail -5 "demo.txt" ``` 或是用 `|` 來導入資料: ```bash= # 使用 -n 加數字 cat "demo.txt" | tail -n 5 # 直接使用 -數字 cat "demo.txt" | tail -5 ``` 結果: ``` 第三列 這是 Linux bash 筆記示範文字 第四列 5555 第六列 Line 7 ``` ### 3. 前幾列 同樣的若要取**開頭幾列**,使用 `head` 指令閱讀檔案: ```bash= # 使用 -n 數字 head -n 3 "demo.txt" # 直接使用 -數字 head -3 "demo.txt" ``` 或是使用 `|` 導入資料: ```bash= # 使用 -n 數字 cat "demo.txt" | head -n 3 # 直接使用 -數字 cat "demo.txt" | head -3 ``` 結果: ``` 第一列 Hello world hi 第二列 你好 第三列 這是 Linux bash 筆記示範文字 ``` ### 4. 開頭幾列之後 若是要排除掉前面的部份,或是**從第幾列開始印**,使用 `tail -n +數字`: ```bash= # 注意有加號 tail -n +3 "demo.txt" ``` 或是使用 `|`: ```bash= # 注意有加號 cat "demo.txt" | tail -n +3 ``` 結果 ``` 第三列 這是 Linux bash 筆記示範文字 第四列 5555 第六列 Line 7 ``` ### 5. 最後幾列之前 若是要排除後面的部份﹐或是**印到倒數第幾列之前**,使用 `head -n -數字`: ```bash= # 注意數字前還有減號 head -n -5 "demo.txt" ``` 或是使用 `|`: ```bash= cat "demo.txt" | head -n -5 ``` 結果 ``` 第一列 Hello world hi 第二列 你好 ``` ### 6. 中間幾列 若要取中間一定範圍,可先 `head` 取出前幾列,用 pipeline `|` 餵給 `tail` 取後幾列,例如: #### (1) 取第四列 ```bash= # 取第四列 head -n 4 "demo.txt" | tail -n 1 ``` 結果: ``` 第四列 ``` #### (2) 取第三到第六列 ```bash= # 取第三到第六列 head -n 6 "demo.txt" | tail -n +3 ``` 結果: ``` 第三列 這是 Linux bash 筆記示範文字 第四列 5555 第六列 ``` #### (3) 去掉前一列和後三列 ```bash= # 去掉前一列和後三列(也就是從第 2 列開始印,最後 3 列不印,注意加號和減號) cat "demo.txt" | tail -n +2 | head -n -3 ``` 結果: ``` 第二列 你好 第三列 這是 Linux bash 筆記示範文字 第四列 ``` 僅顯示前幾列後幾列的做法效率比較好,可避免全檔案載入記憶體中。 ## 三、尋找文字 ### 1. 文字檔找文字 ```bash= # 在 log.txt 找到有 ERROR 字樣的列 grep ERROR log.txt # 在 log.txt 找到有 ERROR 字樣的列 cat log.txt | grep ERROR # 略過大小寫檢查,在 log.txt 找到有 cert、Cert、CERT... 字樣的列 grep -i cert log.txt ``` ### 2. 文字檔用正則表示式 ```bash= # 在 log.txt 中找到有 cert 字樣或有 trust 字樣的列 grep -E "cert|trust" log.txt # 在 data.txt 中找到符合身份證字號的列 grep -E "[A-Z][01][0-9]{8}" data.txt ``` ### 3. 變數找變數 利用兩個中括號 `[[`、`]]` 搭配 `*` 字元,可做模糊比對。 ```bash= # 前面已產生的字串內容 student_list="John,Mary,Leo,Billy,Johnny,Dora," read -p "請輸入查詢名稱:" query # 使用 * 字元做模糊比對 if [[ "${student_list}" == *"${query}"* ]] then echo "有 ${query}" else echo "不存在 ${query}" fi ``` 以上也可以用 `=~` 正則表示式來做比對,就不用 `*` 字元: ```bash= # 前面已產生的字串內容 student_list="John,Mary,Leo,Billy,Johnny,Dora," read -p "請輸入查詢名稱:" query # 使用 =~ 做正則表示式的比對 if [[ "${student_list}" =~ "${query}" ]] then echo "有 ${query}" else echo "不存在 ${query}" fi ``` 結果 ``` 請輸入查詢名稱:Billy 有 Billy 請輸入查詢名稱:Adam 不存在 Adam ``` 以上有一個問題是,如果 `query` 內容帶有分隔字元 `,`,可能造成誤判,例如 `query=lly,Jo`,如果有這個可能在,可以前後加上分隔字元,是一個方法 ```bash= if [[ ",${student_list}," == *",${query},"* ]] ... ``` ## 四、取代文字 字串後接兩個 `/` 隔開成三區,可做文字取代,格式是 `${variable/pattern/replacement}`,==不過這只會取代一次==。 另外,pattern 可以使用正則表示式。 ### 1. 基本 ```bash= # 將 Tel2 取代成 Phone Number text="Tel1=062991111,Tel2=0912345678,Address=Tainan" # 將 Tel2 文字取代換成 Phone Number echo "${text/Tel2/Phone Number}" # 將 Tel1、Tel2、... 、Tel9 文字取代成 Phone Number,但只會取代一次 echo "${text/Tel[1-9]/Phone Number}" ``` ``` Tel1=062991111,Phone Number=0912345678,Address=Tainan Phone Number=062991111,Tel2=0912345678,Address=Tainan ``` ### 2. 模版取代 若要依多筆資料組出像是 `name=Alice,action=add` 給後續動作例如匯入來使用。 ```bash= # 要匯入的名稱,資料在陣列中 name_list=("Alice" "Bob" "Carl" "Denise" "Ella") # 模版字串 template="name=[student_name],action=add" for name in "${name_list[@]}" do # 中括號等特殊字元前面使用 \ 跳脫 data="${template/\[student_name\]/${name}}" echo "${data}" done ``` ``` name=Alice,action=add name=Bob,action=add name=Carl,action=add name=Denise,action=add name=Ella,action=add ``` 模版取代常用於固定模式的批次處理。 ### 3. 多次取代 第一個 `/` 改成 `//` 就可以做到多次取代,也就是 `${variable//pattern/replacement}` 比較以下: ```bash= uid="A123456789" # 取代比對到的第一個字 echo "${uid/[0-9]/*}" # 取代比對到的所有字 echo "${uid//[0-9]/*}" ``` ``` A*23456789 A********* ``` ## 五、串接、分割 ### 1. IFS Linux 中有一個變數 `IFS` (Internal Field Separator) 在控制串接與分割的字元,可以將他印出來檢視,預設是空白字元 ```bash= echo "abc${IFS}xyz" ``` ``` abc xyz ``` 可以修改他,記得要做 `unset IFS` 還原,避免影響後續作業。 ```bash= # 將 IFS 暫時設定成逗號「,」 IFS="," # 還原到初始設定 unset IFS ``` ### 2. 分割 如果有一個資料如下 `alice@example.com,bob@school.net,carl@gmail.com,david@gmail.com` 使用 `for in` 可遍尋資料,如果調整 `IFS` 也就是依據不同字元來分割,就會得到不同結果,參考以下範例(注意程式第 6 列和第 16 列 `${data}` 不能寫成 `"${data}"`,被雙引號包起來會被視為一個 expression 無法拆解): ```bash= data="alice@example.com,bob@school.net,carl@gmail.com,david@gmail.com" # 設定「,」為分割的依據 IFS="," echo "以「,」分割的結果:" for item in ${data} do echo "${item}" done echo # 設定「@」為分割的依據 IFS="@" echo "以「@」分割的結果:" for item in ${data} do echo "${item}" done unset IFS ``` ``` 以「,」分割的結果: alice@example.com bob@school.net carl@gmail.com david@gmail.com 以「@」分割的結果: alice example.com,bob school.net,carl gmail.com,david gmail.com ``` 當然也可以分割成陣列 ```bash= data="alice@example.com,bob@school.net,carl@gmail.com,david@gmail.com" # 使用逗點來分割 IFS="," # 賦值給陣列 array=() for item in ${data} do array+=("${item}") done echo "${array[2]}" # 還原分割字元 unset IFS ``` 可以使用 `read` 指令,與 `-a` 參數與 `<<<` 將資料讀入給陣列,會更簡潔: ```bash= data="alice@example.com,bob@school.net,carl@gmail.com,david@gmail.com" # 使用逗點來分割 IFS="," # 使用 read 指令 read -a array <<< "${data}" echo "${array[2]}" # 還原分割字元 unset IFS ``` ### 3. 串接 陣列使用 `[*]` 會串接所有項目,藉由調整 `IFS` 可以得到想要的串接結果。 ```bash= data=('Alice' 'Bob' 'Carl' 'David') # 沒有調整 IFS,使用預設的空白來串接 combined="${data[*]}" echo "以空白串接的結果:${combined}" # 調整 IFS 為逗號字元來串接 IFS="," combined="${data[*]}" echo "以逗點串接的結果:${combined}" unset IFS ``` 結果: ``` 以空白串接的結果:Alice Bob Carl David 以逗點串接的結果:Alice,Bob,Carl,David ``` # 己、檔案處理 ## 一、針對檔案處理 `for` 或 `while` 可針對檔案處理 ### 1. 條列 ```bash= log="./log/20240504.log" config="setting.conf" batch="start.sh" for f in ${log} ${config} ${batch} do echo '---正處理:'${f}'---' cat ${f} done ``` 不過以上在檔名有空白 ` ` 會被拆分成兩個檔案而錯誤,可在 `for in` 加雙引號: ```bash= log="./log/20240504.log" config="setting.conf" batch="start.sh" for f in "${log}" "${config}" "${batch}" do echo '---正處理:'"${f}"'---' cat "${f}" done ``` ### 2. 清單 ```bash= fileList="./log/20240504.log setting.conf start.sh" for f in "${fileList}" do echo '---正處理:'"${f}"'---' cat "${f}" done ``` ### 3. 傳入參數 `$@` 或 `${@}` 可取得傳入的參數 ```bash= for f in "${@}" do echo '---正處理:'"${f}"'---' cat "${f}" done ``` 若執行檔為 `process.sh`,執行傳入參數: ```bash= # 用空白分開,傳入變數 ./process.sh ./log/20240504.log setting.conf start.sh # 加上雙引號會更好 ./process.sh "./log/20240504.log" "setting.conf" "start.sh" ``` ### 4. 外部清單 另外有一個文字檔案 fileList.txt 存放內容: ``` ./log/20240504.log setting.conf start.sh ``` `while` 搭配 `<` 導入清單內容,用 `read` 讀出每一列的檔案 ```bash= while read f do echo '---正處理:'"${f}"'---' cat "${f}" done < "fileList.txt" ``` 以上也可加入空行判斷,與檔案是否存在的判斷: ```bash= while read f do # 非空白行且檔案存在才做運算 if [ -n "${f}" -a -f "${f}" ] then echo '---正處理:'"${f}"'---' cat "${f}" fi done < "fileList.txt" ``` ## 二、遍尋檔案 ### 1. 所有檔案 用 `*` 列出所有檔案,`for` 可遍尋。要注意的是 `*` 會連資料夾也列入,可用 `[ ]` 和 `-f` 檢查是否為檔案。 ```bash= # log 資料夾下所有檔案 process_all_file="./log/*" for f in ${process_all_file} do # 判斷是檔案而非資料夾 if [ -f "${f}" ] then echo '---'"${f}"'---' cat "${f}" else echo "${f} 非檔案" fi done ``` ### 2. 特定檔案 ```bash= # log 資料夾下檔名為 2024 開頭,且副檔名為 log 的所有檔案 process_logs="./log/2024*.log" for f in ${process_logs} do echo "${f}" done ``` ## 三、路徑和檔名 可透過對字串的處理,來取得檔案的路徑、檔名、主檔名與副檔名,若操作對象的字串並不是完整路徑,要先用 `realpath` 工具,關於這點可看後面筆記。 ### 1. 取得路徑 從右邊找第一個 `/` 符號,移除與之後部份,剩下的即檔案所在的路徑。 ```bash= file="/home/user/docs/sample.txt" path="${file%/*}" echo "路徑為: ${path}" ``` ``` 路徑為: /home/user/docs ``` ### 2. 排除路徑 從左邊找所有的 `/` 符號,移除與之前部份,剩下的即檔案完整檔名。 ```bash= file="/home/user/docs/sample.txt" filename="${file##*/}" echo "完整檔名為: ${filename}" ``` ``` 完整檔名為: sample.txt ``` ### 3. 取得主檔名 先找到完整檔名,從右邊第一個 `.` 符號,移除與之後部份,剩下的即檔案主檔名。 ```bash= file="/home/user/docs/sample.txt" filename="${file##*/}" main="${filename%.*}" echo "主檔名為: ${main}" ``` ``` 主檔名為: sample ``` ### 4. 取得副檔名 找右邊第一個 `.` 符號,取剩下部份即檔案副檔名。 ```bash= file="/home/user/docs/sample.txt" ext="${file##*.}" echo "副檔名為: ${ext}" ``` ``` 副檔名為: txt ``` ## 四、產生或清空 雖然 `echo >`或 `echo >>`可以輸出到指定檔案,且不存在時會自動新增,但沒寫好可能會有多餘的字元。 ### 1. 新增空白檔案 如果檔案不存在,想新增一個空的檔案,`touch` 指令可以增加完全空白內容的檔案,會比 `echo` 空白內容還好。 ```bash= # 同資料夾下產生名稱為 demo.txt 的空白檔案 touch "demo.txt" ``` ### 2. 清空檔案 若檔案已存在,想要清空他,是可以送空白內容清掉檔案,但要注意 `echo` 是會多帶換行字元的,加上 `-n` 參數才能真的清空。 ```bash= # 清空 demo.txt 的內容 echo -n '' > "demo.txt" # 清空 demo.txt 的內容,更簡潔 echo -n > "demo.txt" ``` # 庚、函式 反覆的動作可以寫成函式來重覆使用。 ## 一、宣告與呼叫 ### 1. 基本 `function` 可宣告函式,然後直接寫函式名稱來呼叫。 ```bash= # 宣告函式 function info(){ echo "現在時間是:$(date)" echo "登入帳號是:$(whoami)" } # 呼叫函式 info ``` 結果: ``` 現在時間是:Sat Jun 1 15:07:31 CST 2024 登入帳號是:cyber ``` ### 2. 精簡 可以不用寫 `function`,只要有函式名稱和小括號的結構即可。 ```bash= info(){ echo "現在時間是:$(date)" echo "登入帳號是:$(whoami)" } info ``` 或是寫 `function` 但省略小括號的寫法。 ```bash= function info{ echo "現在時間是:$(date)" echo "登入帳號是:$(whoami)" } info ``` 不過精簡的寫法是 Bash 的特殊語法,建議使用基本的寫法提高可讀性。 ## 二、傳入引數 和 shell 傳入引數一樣,使用 `$1`、`$2`、... 可依序取得傳入引數,或 `$@` 取得所有的引數。 ### 1. 指定引數 以下為寫入 log 的範例,第一個引數是類別,第二個是訊息內容。 ```bash= log_file="20240601.log" function log(){ # date +%F 取得現在年月日,而 date +%T 取得現在時分秒,並非這裡的重點 echo '['$(date +%F)' '$(date +%T)']['${1}']' ${2} >> ${log_file} } log " INFO" "開啟作業" log "ERROR" "發生異常" log " INFO" "作業結束" ``` 執行後 20240601.log 的結果為: ``` [2024-06-01 15:37:55][ INFO] 開啟作業 [2024-06-01 15:37:55][ERROR] 發生異常 [2024-06-01 15:37:55][ INFO] 作業結束 ``` ### 2. 所有引數 以下為計算總和與平均的範例,輸入任意多個引數,使用 `$@` 取得引數集合,用 `for in` 逐一處理。 ```bash= function calc(){ sum=0 count=0 for number in ${@} do # 總和 sum=$((sum + number)) # 計數 count=$((count + 1)) done echo "總和:${sum}" echo "平均:$((sum / count))" } calc 1 2 4 7 0 8 6 ``` 結果: ``` 總和:28 平均:4 ``` ## 三、全域、區域變數 原則上 Bash 中的變數都是默認為全域變數在操作,若希望變數只在函式中可見且不想污染全域變數,可使用 `local` 來宣告區域變數。 :::info //TODO ::: 若函式內的變數名稱與全域的並無衝突,就沒有必要使用 `local`;但如果有同名的時候,函式內的變數還是宣告成區域較好。 ## 四、回傳結果 雖然有 `return` 指令,但這是 Exit Code 做為成功失敗的依據。另外也可以當作結束函數的指令。 ### 1. Exit Code 使用 `return` 指令來回傳 Exit Code 為多少,0 為成功,其他為失敗,沒有寫 `return` 則預設為 0。以下示範計算平均時,檢查引數的個數 `${#}`,若為 0 則表示沒有傳入引數。 ```bash= function calc(){ if [ ${#} -eq 0 ] then # 若沒有傳入引數,則結束函式並且返回 1 return 1 fi sum=0 count=0 for number in ${@} do # 總和 sum=$((sum + number)) # 計數 count=$((count + 1)) done echo "總和:${sum}" echo "平均:$((sum / count))" } # 有傳入數字,會完成作業,返回碼為 0 echo "開始計算..." calc 1 2 4 7 0 8 6 echo "返回碼為 ${?}" echo # 無傳入數字,作業會中斷,返回值為 1 echo "開始計算..." calc echo "返回碼為 ${?}" ``` 結果: ``` 開始計算... 總和:28 平均:4 返回碼為 0 開始計算... 返回碼為 1 ``` ### 2. 回傳值 - 補捉 雖然不能像其他程式語言一樣直接讓函式回傳值,但可以使用 `echo` 將結果輸出到 stdout,再從外部取來運算。以下範例是取得傳入的數字之中的最大值,輸出並捕捉到,再繼續使用的範例。 ```bash= function max(){ result=$1 for number in ${@} do if [ ${number} -gt ${result} ] then result=${number} fi done # 將最大值的結果印在 stdout echo -n "${result}" } # 函數會 echo 出結果,再取得並指定給變數 m m=$(max 1 2 4 7 0 8 6) echo "最大值為 ${m}" echo "最大值的五倍為 $((m * 5))" ``` 結果: ``` 最大值為 8 最大值的五倍為 40 ``` ### 3. 回傳值 - 全域 使用全域變數也是一個方法,小心控制即可: ```bash= # 變數宣告在外 result=0 function max(){ result=$1 for number in ${@} do if [ ${number} -gt ${result} ] then result=${number} fi done } # 函數執行後會將結果賦予給全域變數result max 1 2 4 7 0 8 6 echo "最大值為 ${result}" echo "最大值的五倍為 $((result * 5))" ``` 結果與上面的相同。 # 辛、特殊 ## 一、base64 編碼或 OpenSSL 加解密 使用 pipeline 和 `echo` 可進行編碼或解碼。而 OpenSSL 可提供很多演算法的加解密,這裡以 RSA 為例子。 ==注意==: * 使用 `echo` 時加上 `-n` 可防止產生多餘的斷行字元。 * 將要處理的文字放入引號 `"` 或 `'` 之中,可避免出現預期之外的結果。 ### 1. 將文字以 base64 編碼 ```bash= echo -n "Hello world" | base64 ``` 結果: ``` SGVsbG8gd29ybGQ= ``` ### 2. 將 base64 文字解碼 ```bash= echo -n "SGVsbG8gd29ybGQ=" | base64 -d ``` 結果: ``` Hello world ``` ### 3. base64 運用 #### (1) 一列文字 用撇引號或 `$()` 取得解碼結果並賦值給變數,讓之後腳本可以運用。 ```bash= server_ip="MTkyLjE2OC4wLjEK" server_ip=$(echo -n "$server_ip" | base64 -d) echo "伺服器ip為:$server_ip" ``` 結果: ``` 伺服器ip為:192.168.0.1 ``` #### (2) 文字檔 如果要將整個文字檔做 base64 編碼,可以使用以下指令。假設 `input.txt` 文字檔的內容如下: ``` Hello world hi 你好 這是 Linux bash 筆記示範文字 ``` 執行檔內容: ```bash= cat input.txt | base64 > output.txt echo "完成" ``` 執行結果: ``` 完成 ``` `output.txt` 文字檔的內容會是: ``` SGVsbG8gd29ybGQgaGkK5L2g5aW9CumAmeaYryBMaW51eCBiYXNoIOethuiomOekuuevhOaWh+Wt lwo= ``` ### 4. RSA 產生公私鑰 以下 `-out`、`-pubout` 等等可用 `--out`、`--pubout`。下面用 OpenSSL v1.1.0 以後新版為範例。 ```bash= # 產生檔案名稱為 private.pem 的私鑰 opensll genpkey -algorithm RSA -out "private.pem" # 以 private.pem 產生名稱為 public.pem 的公鑰 openssl pkey -pubout -in "private.pem" -out "public.pem" ``` 預設輸出的格式是 PEM,若要調整再使用 `-outform XXX` 參數。 ### 5. RSA 加密 加密的過程中使用 `echo` 加上 `-n` 避免產生多餘的換行字元,且前後加上單引號,避免特殊字元的影響。 ```bash= # 使用私鑰加密 echo -n 'Hello!' | openssl pkeyutl -encrypt -inkey "private.pem" | base64 # 使用公鑰加密 echo -n 'Hello!' | openssl pkeyutl -encrypt -pubin -inkey "public.pem" | base64 ``` 結果: ``` Phj52gq/K1NaMsCJoIxTki1cA+b+rOvVIBy7EAYb+BE6FMbcYrRy68uLNSzcqHh0 U8VwAmcEVxunkb6fWUHBtI4FxXSEY4+ZlLZJ+ipMdQqyj5QdNmQPAwLo9XCb33t0 ZOJkrLuzY1ziQIWC6F6fX8fkVYCxG9e0YzK5FzZDkkAzpVLlJfjqogTQKAHm5nsV 1MD2TvHvDJH9cDFRdJlhMv2D2lUw0rTZ61l0e1saRiBIAvhThU+5bIhYXYw3Jbk7 5MubEA0hocxRvgRvbdZTVthuYY/ZWkRWI0cOAQi49QI4keO1wEK70tzedSzYSAmN sN94mBxIdI05XKMaORD7uA== ``` 將輸出的 base64 編碼字串刪除多餘換行字元,再複製起來使用。或直接貼這些包含換行字元的字串也可以解密。 ### 6. RSA 解密 要解密已加密的內容,就是以上步驟反過來,做 `openssl -decrypt` 前先將 base64 編碼解開: ```bash= c="Phj52gq/K1NaMsCJoIxTki1cA+b+rOvVIBy7EAYb+BE6FMbcYrRy68uLNSzcqHh0 U8VwAmcEVxunkb6fWUHBtI4FxXSEY4+ZlLZJ+ipMdQqyj5QdNmQPAwLo9XCb33t0 ZOJkrLuzY1ziQIWC6F6fX8fkVYCxG9e0YzK5FzZDkkAzpVLlJfjqogTQKAHm5nsV 1MD2TvHvDJH9cDFRdJlhMv2D2lUw0rTZ61l0e1saRiBIAvhThU+5bIhYXYw3Jbk7 5MubEA0hocxRvgRvbdZTVthuYY/ZWkRWI0cOAQi49QI4keO1wEK70tzedSzYSAmN sN94mBxIdI05XKMaORD7uA==" echo -n "${c}" | base64 -d | openssl pkeyutl -decrypt -inkey "private.pem" ``` 結果: ``` Hello! ``` ## 二、日期時間 ### 1. 基本格式 使用 `date` 指令可取得詳細日期時間字串,若要取得個別的年、月、日、時、分、秒,要搭配 `+FORMAT`,下表是常見的格式,若要查看可用格式可執行 `man date`。 | FORMAT | 意義 | 範例 | | -------- | -------- | -------- | | `%Y` | 年 | `2024` | | `%m` | 月(01, 02, ..., 12) | `04` | | `%d` | 日(01, 02, ...) | `22` | | `%H` | 時(00, 01, ..., 23) | `09` | | `%M` | 分(00, 01, ..., 59) | `27` | | `%S` | 秒(00, 01, ..., 60) | `35` | | `%I` | 時(01, 02, ..., 12) | `07` | | `%N` | 微秒(000000000, ... 999999999) | `745111952` | | `%u` | 星期(1, 2, ..., 7) | `3` | | `%F` | 相當於 `%Y-%m-%d` | `2024-04-22` | | `%T` | 相當於 `%H:%M:%S` | `09:25:08` | | `%R` | 相當於 `%H:%M` | `09:25` | | `%Z` | 時區 | `CST` | 範例: ```bash= echo `date` y=`date +%Y` m=`date +%m` d=`date +%d` t=`date +%T` echo "今天是 ${y} 年 ${m} 月 ${d} 日,時間是 ${t}" file=`date +%Y%m%d`.log echo "記錄檔名為 ${file}" ``` 結果: ``` Mon Apr 22 10:41:05 CST 2024 今天是 2024 年 04 月 22 日,時間是 10:41:05 記錄檔名為 20240422.log ``` ### 2. 穿插特殊字元 當格式中出現特殊字元,像是空白或括號,須在字元前面加上反斜線 `\`。 ```bash= date +%F\ %T date +%F\ %T\(%Z\) ``` ``` 2024-11-01 11:55:27 2024-11-01 11:55:27(CST) ``` ### 3. 自訂 prompt 的日期時間 在 `PS1` 變數中使用 `\D{format}` 可以加入日期和時間,格式與 `date +` 雷同,特殊字元不需要加反斜線。 ```bash= PS1="[\D{%F %T (%Z)}][\u@\h \W]\$ " [2024-11-04 09:16:24 (CST)][user@machine ~]$ ``` ## 三、檔案 ### 1. 日期時間戳 使用 `stat` 指令加上 `%y` 參數,可得到檔案最後修改日期時間戳 ```bash= modify_time=`stat -c %y demo.txt` echo "demo.txt最後修改時間為:${modify_time}" ``` 結果: ``` demo.txt最後修改時間為:2024-04-22 15:28:28.621690100 +0800 ``` ### 2. 檔案大小 使用 `du` 指令可以取得資料夾或檔案的大小,常用參數: * `-h`、`--human-readable` 容易閱讀的格式像 `1K` `234M` `2G` * `-b`、`--bytes` 以多少 byte 顯示 * `-m` 以多少 mb 顯示 * `-d N`、`--max-depth N` 資料夾層級,N 為 0 時表示該資料夾,N 為 1 時展開子資料 * `-s`、`--summarize` 資料夾本身(同 `--max-depth 0`) 不過顯示的結果是檔案或資料夾大小加上檔案或資料夾本身,以空白隔開,所以要再用陣列把他分開,下面範例是用 `curl` 存取一個 API 後將回應結果存到一個檔案裡,然後用 `du` 取得檔案大小。 ```bash= file="./api_result.txt" curl http://www.example.com/api > "${file}" # 取得檔案大小資訊全部 file_info="$(du -h ${file})" # 拆成陣列 read -a info_array <<< "${file_info}" echo "檔案資訊:${file_info}" echo "檔案大小:${info_array[0]}" ``` 結果 ``` 檔案資訊:44K ./api_result.txt 檔案大小:44K ``` ### 3. 結尾的 CRLF 用 LF 取代 Windows 系統中的檔案通常以 CRLF `\r\n` 結尾,但 Linux 則使用 LF `\n` ,跨系統時若沒有處理這個差異,可能會導致異常。例如 Windows 的文字檔案拿去 Linux 分析,在 `while read line` 時`${line}` 其實都帶著 CR 在尾端,會有預期之外的錯誤。 以下是使用 `sed` 指令做字元的轉換: ```bash= # 將 input.txt 的文字所有 CRLF 轉為 LF 輸出成 output.txt sed 's/\r$//' input.txt > output.txt ``` ### 4. 取得實際路徑 前面有對檔案取路徑、完整檔名、主檔名和副檔名等操作,其實是對字串的操作,如果是傳入 **相對路徑** 可能就會失敗。這種情況可以使用 `realpath` 先取得 **絕對路徑**,再進行後續操作。 ```bash= file="sample.txt" echo "相對路徑:${file}" file=$(realpath "${file}") echo "完整路徑:${file}" ``` ``` 相對路徑:sample.txt 完整路徑:/home/user/sample.txt ``` ## 四、JSON ### 1. 取字串屬性值 使用 `grep -o` 與正則表示式,可以取得 JSON 的屬性,例如一包 JSON 中有一個屬性是 `access_token`,要取得其內容: ```bash= json='{"success":true,"access_token":"c92dc7d0d2fb4b60b5eb","data":null}' token=`echo -n "${json}" | grep -o '"access_token":"[^"]*' | grep -o '[^"]*$'` echo "${token}" ``` 結果: ``` c92dc7d0d2fb4b60b5eb ``` ==注意==:這個方法只能截取內容不含雙引號 `"` 的屬性值,像是下面範例的 `message` 含有 `"`,而 traceId 沒有: ```json= { "success": false, "message": "parameter \"code\" not found.", "traceId": "20240526002117" } ``` 以上 JSON 可以取 `traceId`,但不能取 `message`。確定字串內容不會含有雙引號再使用此方法。 ## 五、游標控制 一般來說腳本程式都一直往下印出文字,其實可以印出特殊的游標控制字元到終端機,做到一些畫面上的排版。使用方法為 `\033[` 加上各個字元,然後使用 `echo -e` 來輸出,建議也加上 `-n` 避免換行字元造成移動上的誤解。 參考:[Console code - Linux Programmer's Manual](https://www.unix.com/man_page/linux/4/console_codes/) ### 1. 相對目前游標位置的移動 依目前游標的位置再做上下右左的移動。 | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |A| 游標往上移動一列 | `\033[3A` 游標往上移動 3 列 | |B| 游標往下移動一列 | `\033[2B` 游標往下移動 2 列 | |C| 游標往右移動一個字元 | `\033[8C` 游標往右移動 8 個字元 | |D| 游標往左移動一個字元,若已到列首則不動 | `\033[7D` 游標往左移動 7 個字元 | |E| 游標往下移動一列,並到列首 | `\033[3E` 游標往下移動 3 列,移到列首 | |F| 游標往上移動一列,並到列首 | `\033[5F` 游標往上移動 5 列,移到列首 | ### 2. 絕對位置的移動 終端機畫面最上方為第 1 列,往下為第 2 列、第 3 列、……,列首為第 1 字元,往右為第 2 字元、第 3 字元、…… | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |d| 游標移動到指定列數的同字元處 | `\033[4d` 游標移到第 4 列的同行位置 | |G| 游標移動到本列的指定字元位置 | `\033[9G` 游標移到本列第 9 個字元 | |H| 游標移動到指定列數和行數 | `\033[3;5H` 游標移動到第 3 列第 5 個字元 | ### 3. 游標顯示與否 移動游標的操作中如果還看得到游標可能會有點雜亂,可以隱藏再來移,記得腳本結束時要恢復,否則會造成之後操作困擾。 | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |?25l|隱藏游標|`\033[?25l` 輸出後都看不到游標 | |?25h|顯示游標|`\033[?25h` 輸出後可以看到游標| ### 4. 記憶位置 定點顯示很好用的功能,先用 s 儲存起來,每次用 u 移回來。 | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |s| 記憶目前游標位置 | `\033[s` | |u| 將游標位置移到已記憶的位置 | `\033[u` | |6n|回傳游標的座標,形式是 `Esc[y;xR` | `\033[6n` | ### 5. 清除列或清除畫面 以下送出後會清除畫面上的文字,游標位置不會移動。 | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |K| 清除本列游標位置(含)到結尾的字元 | `\033[K` | |1K| 清除本列開頭到游標位置(含)的字元 | `\033[1K` | |2K| 清除本列所有字元 | `\033[2K` | |J| 清除游標位置(含)以後全畫面所有字元 | `\033[J` | |1J| 清除游標位置(含)以前全畫面所有字元 | `\033[1J` | |2J| 清除全畫面所有字元 | `\033[2J` | |X| 清除游標位置(含)右邊指定數量的字元 | `\033[4X` 清除 4 個字元 | ### 6. 刪除 與上面的 K J X 不同的地方在於,K J X 是畫面上顯示文字的清除,並不會被其他的文字補上,而 M P 會。 | 字元 | 說明 | 範例 | |:--------:| -------- | -------- | |M|刪除游標所在列(含)以下列數,更下方會往上補| `\033[2M` 刪除 2 列 | |P|刪除游標所在列(含)右邊字元,更右方會往左補| `\033[4P` 刪除 4 字 | ### 6. 範例 以下範例將陣列中的項目一個個印出來在 「Now processing:」的後方,而「Progress bar:」後方的 `-` 字元一個個被取代成 `V` 字元。創造出文字不會一直往下印出的畫面。 ```bash= # 陣列資料,有 8 筆 list=('Adam' 'Billy' 'Carl' 'Denise' 'Ellizabeth' 'Flora' 'Gina' 'Henry') # 先印出「Progess bar:--------的字樣」 echo -n "Progress bar:" for i in ${!list[@]} do echo -n "-" done # 換行,移到Progress bar的下方 echo echo -n "Now processing:" # 要右移的字元數 offset=13 for i in "${!list[@]}" do # 將游標往上 1 列,且移到第 1 字元 echo -ne "\033[F" # 向右 offset 數量 echo -ne "\033[${offset}C" # 印出一個 V 取代掉 - 字元 echo -n "V" # 將游標往下 1 列,且移到第 1 字元 echo -ne "\033[E" # 向右 15 個字元 echo -ne "\033[15C" # 清空游標到尾端本列的所有字元 echo -ne "\033[K" # 印出資料 echo -n "${list[${i}]}" # 調整向右位移數量,並暫停 0.5 秒 offset=$((offset + 1)) sleep 0.5 done echo ``` 結果: ``` $ ./demo.sh progress bar:VVVVV--- Now prcessing: Ellizabeth $ ``` 以上不在乎可讀性,可以用一列即可: ```bash= echo -ne "\033[F\033[${offset}CV\033[E\033[15C\033[K${list[${i}]}" ``` ## 六、進位 0開頭的八進位問題 https://stackoverflow.com/questions/24777597/value-too-great-for-base-error-token-is-08