---
# System prepended metadata

title: Bash Shell Script 筆記
tags: [筆記, Linux]

---

# 甲、介紹

## 一、前言

這篇是我自己工作上需要撰寫 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