# 甲、介紹
## 一、前言
這篇是我自己工作上需要撰寫 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