<h1>Git筆記--Linux Shell入門(二)- Shell 變數、字串與註釋</h1>
<code>Linux Shell</code> 指令是使用 <code>Linux</code> 系統時與作業系統互動的基本方式,而在 <code>GitLab CI</code> 中,每個 <code>Job</code> 都會在 <code>runner</code> 的環境中執行,執行的語言就是 <code>Shell</code>(<code>Linux</code> 預設是 <code>bash</code> 或 <code>sh</code>)。所以,<code>.gitlab-ci.yml</code> 裡的 <code>script:</code> 欄位,其實就是寫給 <code>Shell</code> 執行的指令。
在 <code>Linux Shell</code>(以 <code>Bash</code> 為例)中,**變數(variable)** 是一種用來儲存資料的「容器」,可以用來保存字串、數字、陣列,甚至命令輸出結果,字串(Strings)則用來保存文字的內容,是 <code>Shell</code> 裡最常見的資料型態,例如檔案名、訊息、路徑等。
>[!Note]<code>Shell</code> 是甚麼?</code>
><code>Shell</code> 是一個「命令解釋器」,它讀取你寫的文字指令,然後告訴電腦該做什麼。在 <code>GitLab CI</code> 中,每個 <code>script:</code> 區塊裡的內容就是 <code>Shell</code> 指令。
---
變數的命名規則
---
1. 只能包含字母、數字和底線 <code>_</code>
```bash=
user_name="PU" # ✅ 正確
user-name="PU" # ❌ 錯誤,不能有 "-"
```
2. 必須以字母或底線開頭,不能以數字開頭
```bash=
var1="hello" # ✅ 正確
name="PU" # ✅ 正確
_name="test" # ✅ 正確
1var="hello" # ❌ 錯誤
```
3. 大小寫敏感
```bash=
VAR="UPPER"
var="lower"
echo $VAR # 輸出 UPPER
echo $var # 輸出 lower
```
4. 不能使用空格
```bash=
full_name="PU Lin" # ✅ 正確,整個字串用引號包起來
name = "PU" # ❌ 錯誤,等號左右不能有空格
full name="PU Lin" # ❌ 錯誤,變數名稱不能有空格
```
5. 避免使用特殊符號
```bash=
my_var="value" # ✅ 正確
my$var="value" # ❌ 錯誤,$ 是特殊符號
```
6. 避免使用 <code>shell</code> 關鍵字
```bash=
count="10" # ✅ 正確
if="yes" # ❌ 錯誤,if 是保留關鍵字
```
---
字串的宣告
---
在 Linux Shell 中,我們會使用 雙引號<code>" "</code> 和 單引號<code>' '</code> 來宣告字串,兩種方式分別代表不同的意義。
- [<code>" "</code>(雙引號)](https://www.runoob.com/linux/linux-shell-variable.html#:~:text=%E9%80%80%E5%87%BA%E7%8A%B6%E6%80%81%E7%AD%89%E3%80%82-,Shell%20%E5%AD%97%E7%AC%A6%E4%B8%B2,-%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF): 雙引號的內部字元「大部分」不會進行解析,但是諸如: <code>$變數</code>、<code>$(command)</code>、<code>\</code> 仍會被解析為 <code>shell</code> 指令參數。
```bash=
echo "$HOME"
# 輸出: /home/yourname
# $HOME 被作為指令參數解析了,所以會輸出 Home 的路徑。
```
- [<code>' '</code>(單引號)](https://www.runoob.com/linux/linux-shell-variable.html#:~:text=%E9%80%80%E5%87%BA%E7%8A%B6%E6%80%81%E7%AD%89%E3%80%82-,Shell%20%E5%AD%97%E7%AC%A6%E4%B8%B2,-%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%98%AF): 單引號的內部字元完全不會進行解析,會被直接當作純文字。
```bash=
echo '$HOME'
# 輸出: $HOME
```
---
註釋的宣告
---
我們在使用 Linux Shell 時,可以將程式碼標註為註釋,被標註為註釋的段落不會執行,主要有兩種作法。
1. 使用 <code>#</code> 來將段落標記為註釋,這是最簡單基本的做法
```bash=
# 這是標準的單行註釋
echo "Hello" # 也可以放在指令後面
```
2. 使用「空指令」(no-op),搭配 Here Document 語法或字串,讓 shell 讀取但不執行內容,達到註釋效果。嚴格來說,這是一種利用語法特性的「技巧」,不是正式語法。
```bash=
# 方法 A: 使用 <<EOF
:<<EOF
這是多行註釋
可以寫很多行
EOF
# 方法 B: 使用 <<COMMENT
:<<COMMENT
這也是多行註釋
COMMENT 只是標記名稱,可以自訂
COMMENT
# 方法 C: 使用單引號
: '
這種寫法也可以
但不是最佳實踐
'
```
---
變數的基礎使用
---
<details><summary>1. 透過 <code>$</code> 來傳遞變數的值</summary>
<div>
---
在 <code>Linux Shell</code> 中,我們可以透過 <code>$</code> 來使用變數的值
```bash=
echo $name # 印出 PU
echo $age # 印出 25
```
在實務上建議在使用變數時,加上 <code>{}</code> 來將值框住來避免歧意,使其具有更高的安全性和可讀性。
```bash=
file="abc"
echo $file123 # shell 會以為你的變數叫 file123,而不是 file
echo ${file}123 # 明確:變數是 file,後面是字面文字 123
```
</div>
</details>
<details><summary>2. 使用 <code>$(command)</code> 來由其他指令的結果來建立變數</summary>
<div>
---
我們可以透過利用 <code>$(command)</code> 取得指令輸出,類似於數學中的括號,會先執行 <code>()</code> 內的指令,然後才將結果存入變數。
```bash=
# 範例一: 直接取得日期
today=$(date) # 把 date 指令放進變數
echo ${today} # 輸出: Sun Dec 7 14:32:10 UTC 2025
# 範例二: 將 date 格式化輸出
current_date=$(date +%Y-%m-%d)
echo "今天是:$current_date" # 2025-12-07
```
<code>$(command)</code> 的運作本質上是使用 <code>()</code> 開啟一個 [SubShell](#how_to_avoid_job_fail),並將標準輸出回傳給主 <code>Shell</code>,類似於 <code>kotlin</code> 中的 <code>return</code>。
>[!Note] <code>+</code> 是?
><code>+</code> 是 <code>date</code> 格式化輸出的語法,想要格式化輸出日期,需要在 <code>date</code> 中加上 <code>+</code> 才能啟動格式化輸出,這是 <code>date</code> 這個指令自己規定
</div>
</details>
<details><summary>3. 使用 <code>readonly</code> 建立唯讀變數</summary>
<div>
---
可以透過 <code>readonly</code> 關鍵字來創造唯讀變數,就像 <code>kotlin</code> 中的 <code>val</code> 一樣,當我們嘗試在該 <code>job</code> 中更改宣告為 <code>readonly</code> 的變數時,該 <code>Job</code> 會直接宣告失敗,進而造成整個 <code>pipeline</code> 中斷。
早期的 <code>readonly</code> 需要把 <code>readonly</code> 宣告和賦值分開做。
```bash=
url="https://google.com"
readonly url
```
但其實在現代使用單行也是可以的
```bash=
readonly url="https://google.com"
```
>[!Tip]<span id="how_to_avoid_job_fail">如何避免因變更唯讀變數而導致 job 失敗造成 Pipeline 中斷</span>?
><details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary>1. 使用 <code>SubShell</code> 語法</summary>
><div>
>
>---
>在[上面](#由其他指令的結果來建立變數)的時候,我們學到了使用 <code>$(command)</code> 來由其他指令的結果來建立變數,其本質上是開啟一個「SubShell」來運行內部指令並傳出標準輸出;我們可以用 <code>()</code> 來建立一個 <code>SubShell</code>,<code>SubShell</code> 有以下特點:
>- <code>SubShell</code> 可以理解成「一個小小的、獨立的 <code>Shell</code> 環境」
>- 變數、目錄切換、錯誤等不影響主 <code>Shell</code>
>- 錯誤也不會直接中斷主流程
>```bash=
>test_readonly_fail_Subshell_1:
> stage: test
> script:
> - echo "測試 2-2-1 開始"
> - echo "嘗試修改 readonly 變數(加上SubShell)"
> - echo "宣告唯讀變數 myVal"
> - readonly myVal="My Val"
> - echo "嘗試修改 readonly"
> - (myVal="You guessed wrong, JOJO — actually, I’m DIO!!") || echo "Nah, actually, I’m JOJO."
> - echo "測試 2-2-1 完成"
> allow_failure: true
>```
>
></div>
></details>
><details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary>2. 使用 <code>allow_failure: true</code></summary>
><div>
>
>---
>在 <code>GitLab</code> <code>CI</code>/<code>CD</code> 的 <code>.gitlab-ci.yml</code> 中,<code>allow_failure: true</code> 是一個屬性,用來控制 某個 <code>Job</code> 失敗時不會讓整個 <code>Pipeline</code> 中斷。它並不是 <code>Linux Shell</code> 本身的指令,而是 <code>CI</code>/<code>CD</code> 的設定,但它會影響你 <code>Shell</code> 指令在 <code>Pipeline</code> 中的容錯行為。
>- 雖然不會中斷 <code>Pipeline</code>,但是該 <code>Job</code> 仍會被標記為 「Failed」 且不會執行該 <code>Job</code> 中的後續命令,該 <code>Pipeline</code> 會被標記為 「Warning」
>```bash=
>stages:
> - test
>
>test_readonly_fail:
> stage: test
> script:
> - echo "測試開始"
> - echo "目標:嘗試修改 readonly 變數"
> - echo "宣告唯讀變數 myVal"
> - readonly myVal="My Val"
> - echo "嘗試修改 readonly"
> - myVal="You guessed wrong, JOJO — actually, I’m DIO!!"
> - echo "測試完成"
> allow_failure: true
># 結果: 雖然 pipeline 沒有中斷,但是該 Job 在執行到修改 myVal 時就失敗且停止了。
>```
>
></div>
></details>
>一般來說,要避免因 <code>Job</code> 失敗而崩潰還可以使用 <code>||</code> 或 <code>|| true</code> 兩種方法,但如果是 <code>readonly</code> 的話,在嘗試修改唯讀變數的當下就會直接報錯並失敗,不會執行 <code>||</code> 和 <code>|| true</code> 中的後續指令,因此這兩種方法無效。
>
>- <code>||</code>: 如果左邊的指令 A 執行失敗,那麼就執行右邊的指令 B
>- [|| true](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/git_LinuxShell_9#%E5%B1%80%E9%83%A8%E7%95%B0%E5%B8%B8%E8%99%95%E7%90%86): 如果左邊的指令執行失敗,那就傳遞一個 true,使指令強制視為成功。
</div>
</details>
<details><summary>4. 使用 <code>$(())</code> 來進行整數計算並賦值</summary>
<div>
---
<code>$(())</code> 又稱為 「算術擴展(arithmetic expansion)」, 能執行內部數學運算,並把運算結果替換成字串輸出
- 完全在當前 <code>Shell</code> 內部計算,開銷極低
- 雖然看起來很像是 <code>$(command)</code> 語法加上 <code>()</code> <code>SubShell</code> 語法,但並不是,三者代表的涵義不同。
- 只支援整數運算,不支援小數
- <code>$(())</code> 內的變數不需要加 <code>$</code> 即可直接從外部引用。(要加 <code>$</code> 也可以,但不必要)
- 支援標準的程式語言運算,如加減乘除、取餘數 (<code>%</code>)、甚至位元運算 (<code><<</code>, <code>>></code>, <code>&</code>, <code>|</code>) 和三元運算符
- 會回傳一個結果值,可以用來賦值或顯示
```bash=
# 加法
echo $((5 + 3)) # 8
# 減法
echo $((10 - 4)) # 6
# 賦值
x=$(( 7 * 6 ))
echo $x # 42
# 從外部引用變數,不需要加上"$"就可以直接引用
x=10
echo $(( x + 5 )) # 輸出: 15
```
</div>
</details>
<details><summary>5. 使用 <code>(())</code> 來進行整數計算,並將結果用於條件判斷或改變變數的數值</summary>
<div>
---
<code>(())</code> 又稱為 「算術評估(arithmetic evaluation)」,使用規定基本上和 <code>$(())</code> 相同,使用相同的規定,但兩者在「回傳值」上有若干差異。
- <code>(())</code> 不回傳結果值,但會回傳退出狀態碼(exit status),類似於布林值,用來代表成功或失敗。
- 若計算結果為 <code>true</code> 或非零的值,會回傳 <code>0</code>,<code>0</code> 在 <code>shell</code> 中帶表成功。
- 若計算結果為 <code>false</code> 或等於零,會回傳 <code>1</code>,<code>1</code> 在 <code>shell</code> 中代表失敗。
- <code>(())</code> 適合用做條件判斷或是單純的改變變數的數值
- 雖然看起來和 <code>()</code> <code>SubShell</code> 很像,但和 <code>SubShell</code> 不同,修改變數時,變數會真的在原 <code>Shell</code> 中被更新
```bash=
# 只執行運算,不回傳值
((a = 5 + 3))
echo $a # 輸出:8
# 用於改變變數
a=10
((a++)) # 遞增 a
echo $a # 輸出:11
# 將 (()) 用在做條件判斷上來回傳 true 或 false
read -p "請輸入分數:" score
if ((score >= 90)); then
echo "優秀"
elif ((score >= 60)); then
echo "及格"
else
echo "不及格"
fi
```
</div>
</details>
<details><summary>6. 使用 <code>unset</code> 刪除變數</summary>
<div>
---
可以使用 <code>unset</code> 來「刪除變數」或「清除環境變數」,來刪除舊有的變數來確保之後不會誤用他。
- 會「移除變數」,不會移除數值,變數名稱會消失,但不是把值歸零,而是整個不再存在。
- 不能用來刪除 <code>readonly</code> 變數,試圖刪除會直接使 <code>Job</code> 被認定為失敗
- 也能刪除陣列元素
```bash=
# 基本語法
unset 變數名稱
# 範例
name="PU"
unset name # 移除變數 name
echo $name # 空,因為已被移除
```
</div>
</details>
<details><summary>7. 使用 <code>declare</code> 或 <code>typeset</code> 關鍵字來宣告變數的屬性</summary>
<div>
---
在 <code>Linux Shell</code> 中 <code>declare</code> / <code>typeset</code> 用於宣告和設定變數的屬性,它們就像是 <code>Kotlin</code> 中的 <code>val</code> 或 <code>var</code> 關鍵字,但功能更強大,可以設定變數的「類型」和「行為」。
- <code>declare</code> / <code>typeset</code> 是完全等價的,<code>declare</code> 是新式用法,<code>typeset</code> 是舊式用法。
- 能為變數加上特定的屬性,確保它在腳本中以特定的方式運作。
- 透過「旗標(Flags)」來設定屬性
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary>常用的 Flag 列表</summary>
<div>
---
| **旗標 (Flag)** | 原意 | **作用說明 (目的)** | **類似 Kotlin 概念** | **範例指令** |
| --------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------------------------- |
| **`-r`** | Read-only(唯讀) | 宣告變數為唯讀變數 | 類似 `val` | `declare -r PI=3.14` |
| **`-x`** | export(匯出) | 宣告變數為環境變數 | 類似全域 `Environment Variable` | `declare -x DB_URL="localhost"` |
| **`-i`** | Integer(整數) | 強制變數只能儲存整數值。當您試圖對其賦予非數字字串時,它會被評估為 0。 | 類似 `Int` | `declare -i count=5` |
| **`-a`** | Array(陣列) | 宣告變數為索引陣列,用數字當作索引(0, 1, 2...) | 類似 <code>Array<T></code> | `declare -a list=("a" "b" "c")` |
| **`-A`** | Associative(關聯) | 宣告變數為關聯陣列,用字串當索引的字典結構 | 類似 <code>Map<String, T></code> 或 `HashMap` | `declare -A map map[name]="小明"` |
| **`-l`** | Lowercase(小寫) | 存入的內容自動轉為小寫 | 類似 `String.lowercase()` / `setter` 內的轉換邏輯 | `declare -l NAME="Jojo"` |
| **`-u`** | Uppercase(大寫) | 存入的內容自動轉為大寫 | 類似 `String.uppercase()` / `setter` 內的轉換邏輯 | `declare -u NAME="dio"` |
| **`-f`** | Function(函數) | 這個旗標是用於查詢的。它會列出所有已定義的 Shell 函數及其內容。 | 類似 `Reflection` 查詢已定義函數 | `declare -f` |
| **`-p`** | Print(印出) | 這個旗標是用於查詢的。顯示變數的所有屬性和當前值,它會以 `declare` 指令的形式,印出指定變數(或所有變數)的所有屬性。 | 類似 `Reflection` 或物件的 `toString()` 查詢變數狀態 | `declare -p count` |
</div>
</details>
>[!Note] <code>declare -r</code> 和 <code>readonly</code> 都是讓變數唯讀,兩者差在哪裡?
>- <code>declare</code> 就像是打開標籤選單,然後選一個貼上去,有很多種標籤可供選用, <code>readonly</code> 則是直接拿唯讀的標籤貼上去
>- <code>readonly</code> 無法用在宣告陣列屬性,也不能同時宣告其他屬性
>- <code>declare -r</code> 可以跟其他 <code>declare</code> 屬性一起用(例如 <code>-i</code>、<code>-a</code>)
</div>
</details>
<details><summary>8. 特殊變數</summary>
<div>
---
<code>Linux Shell</code> 在執行腳本時,會自動替你準備一些有著特定資訊的特殊變數,像是:腳本自己的名字、、呼叫時傳入的參數、目前有多少參數、上一個指令的成功或失敗...等等,就像是 <code>kotlin</code> 中的預設屬性,無需宣告即可直接使用。
<b><u>※ 位置參數 (Positional Parameters)</u></b>: 於存取使用者在執行腳本時傳入的參數
1. <code>$0</code>: 表示正在執行腳本本身的名稱或路徑。
```bash=
echo "腳本名稱是:$0" # 顯示腳本的檔名
```
2. <code>$1, $2...</code>: 依序表示使用者傳遞給腳本的第一個、第二個等個別參數。
```bash=
# ./test.sh apple banana
# 在 test.sh 裡
echo $1 # apple
echo $2 # banana
```
3. <code>$@</code>: 將所有參數以「參數原本的分組方式」逐一列出,能保留原始的參數分隔,例如: <code>"a" "b" "c"</code>
```bash=
# 假設執行:./test.sh "hello world" "foo bar"
echo "使用 \$@(分開):"
for arg in "$@"; do
echo " 參數:[$arg]"
done
# 輸出:
# 參數:[hello world]
# 參數:[foo bar]
```
4. <code>$*</code>: 將所有參數以「合併成一行」的方式列出,例如: <code>"a b c"</code>
```bash=
# 假設執行:./test.sh "hello world" "foo bar"
echo "使用 \$*(合併):"
for arg in "$*"; do
echo " 參數:[$arg]"
done
# 輸出:
# 參數:[hello world foo bar]
```
<b><u>※ 環境狀態</u></b>: 這組變數用於存取 <code>Shell</code> 運行環境、上一個指令的狀態或腳本的執行資訊
1. <code>$#</code>: 表示使用者傳遞給腳本的參數總數(不包含 <code>$0</code>)。
```bash=
echo "參數總數:$#"
```
2. <code>$?</code>: 判斷上個指令成功還是失敗,<code>0</code> 代表成功,非 <code>0</code> 則為失敗。[參見](https://hackmd.io/r7gGbtTCRCS1Dv9l9Wbp-A#%E5%9F%BA%E7%A4%8E%E9%98%B2%E7%A6%A6%E8%88%87%E7%8B%80%E6%85%8B%E6%84%9F%E7%9F%A5)。
```bash=
echo $? # 顯示失敗的狀態碼,例如 2
```
3. <code>$$</code>: 表示當前 <code>Shell</code> 或腳本的唯一行程 ID(PID),類似於 <code>Shell</code> 的身分證。常用於建立臨時檔名以避免衝突
```bash=
echo "當前 PID:$$"
```
4. <code>$!</code>: 表示最後一個在背景執行的指令的行程 ID(PID)。
```bash=
sleep 10 &
echo $! # 顯示剛剛那個 <code>sleep</code> 的 <code>PID</code>
```
5. <code>$-</code>: 顯示當前 <code>Shell</code> 啟動時所使用的選項旗標(Flag)
```bash=
echo $-
```
6. <code>$_</code>: 表示上一個指令的最後一個參數/值
```bash=
echo hello world
echo $_ # world
```
</div>
</details>
---
字串的基本操作
---
<details><summary>1. 使用 <code>" "</code> 雙引號及 <code>$</code> 進行字串拼接</summary>
<div>
---
<code>Linux shell</code> 的字串拼接不需要 <code>+</code> 號,只需使用 <code>$</code> 呼叫變數並將字串並排、相鄰即可,但建議加上 <code>{}</code> 來對字串進行區隔。
- 需要在 <code>" "</code> 雙引號內執行,如果使用 <code>' '</code> 單引號,會被判讀為純文字。([參見](#字串的宣告))
- 可以使用 <code>+=</code> 運算符來進行連續拼接
```bash=
a="Linux"
b="Shell"
c="指令"
all="${a}${b}${c}"
echo "$all" # 輸出: LinuxShell指令
```
```bash=
# 連續拼接
message="Hello" # Hellow
message+=" " # 一個空白
message+="World" # World
echo $message # 輸出: Hello World
```
</div>
</details>
<details><summary>2. 使用 <code>${#}</code> 獲得字串長度</summary>
<div>
---
可以使用 <code>${#}</code> 來獲得字串長度
- 空格算一個字元
- 多行字串會包含換行符號 <code>\n</code>,也會被計算在長度內
- 計算的是字元數,不是 <code>byte</code>,若用多字節編碼(例如 <code>UTF-8 中文</code>),每個中文仍算作 1 個字元
```bash=
text="Hello World"
length=${#text}
echo $length # 輸出: 11
# 多行字串
str="Line1
Line2"
echo ${#str} # 11 (包含換行)
# 如何獲得一個空字串的長度
empty=""
length=${#empty}
echo $length # 輸出: 0
```
</div>
</details>
<details><summary>3. 使用 <code>${變數:起始位置:長度}</code> 對字串進行切片</summary>
<div>
---
1. 基本語法: <code>${變數:起始位置:長度}</code>
- 字串切片語法,起始值為 0。
```bash=
text="Hello World"
# 從位置 0 開始,取 5 個字元
echo ${text:0:5} # 輸出: Hello
```
2. 從指定位置開始切到結尾
```bash=
text="Hello World"
# 從位置 6 開始(W),一直取到最後
echo ${text:6} # 輸出: World
```
3. 從右邊開始計算(負數索引)
```bash
text="Hello World"
# 注意: 負數前面要有空格,避免與其他語法混淆
# -5 表示從右邊數來第 5 個字元開始
echo ${text: -5} # 輸出: World
echo ${text: -5:3} # 輸出: Wor
```
- 當使用負數時,冒號前面必須有一個空格
4. 結合 <code>${#}</code> 語法,自動計算長度,方便截取尾部或中段
```bash=
str="Hello, PU"
len=${#str}
# 取倒數 2 個字元
echo ${str:len-2} # PU
```
</div>
</details>
<details><summary>4. 使用 <code>expr index</code> 查詢指定的字元在一個字串中最先出現的位置</summary>
<div>
---
- 一次只會查一個字元
- 一次可以有多個要查找的目標字元
- 多個目標查找時,他會回目標中先出現的那一個
```bash=
text="Hello World"
position=$(expr index "$text" "ro") # 找 r 或 o 中任意一個
echo $position # 'r'在第 9 個字元位置才出現,'o' 在位置 5 就先出現,,所以返回 5
# 輸出: 5
```
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary><code>expr index</code>的查找流程</summary>
<div>
---
```bash=
text="Hello World"
expr index "$text" "Wr"
掃描過程:
位置: 1 2 3 4 5 6 7 8 9 10 11
字元: H e l l o W o r l d
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
找 W? × × × × × × ✓ ← 找到了!
找 r? × × × × × × × × ✓ ← 也找到了,但晚了
結果: 返回 7 (W 先出現)
```
</div>
</details>
</div>
</details>
<details><summary>5. 使用 grep 尋找符合特定模式的那一行</summary>
<div>
---
grep 的原意是 「Global Regular Expression Print」,意思是「在大量文字中,依規則找出符合的內容並印出來」,grep 指令除了能使用字串作為查找目標之外還能使用[正則表達式](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_Regular_Expression)作為搜尋的目標樣本。
```bash=
# 基本語法: grep 關鍵字 檔案
# 在 log.exe 中搜尋 "ERROR" 字串
grep "ERROR" log.txt
```
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary>1. grep 預設會輸出整行內容,也就是包含你搜尋字串的那一整行。</summary>
<div>
---
```bash=
# 假設 log.txt 內容如下:
# 2024-01-10 INFO: System started
# 2024-01-10 ERROR: Connection failed
# 2024-01-10 INFO: Retry successful
grep "ERROR" log.txt
# 輸出: 2024-01-10 ERROR: Connection failed
# 也可以存成變數
errors=$(grep "ERROR" log.txt)
```
</div>
</details>
<details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary>2. grep 支援多種搜尋選項(Flag),例如反向搜尋、顯示行號等等,各選項可混用且不分順序</summary>
<div>
---
| 選項 | 原意 | 功用 |
| ----------------------------------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| <code>-n</code> | line-number | 標示行號,會在輸出中標示目標在第幾行 |
| <code>-i</code> | ignore-case | 不區分大小寫 |
| <code>-v</code> | invert-match | 反向匹配,只顯示不包含指定字串的行 |
| <code>-c</code> | count | 回傳一個整數值,顯示至少包含一次指定字串的行數總和,同一行內出現多次也只會算成一次 |
| <code>-l</code> | file-with-matches | 顯示有哪些檔案的檔案內容中具有指定字串 |
| <code>-r</code> | recursive | 使 grep 能夠搜尋資料夾下的子文件(遞迴搜尋) |
| <code>-A</code>/<code>-B</code>/<code>-C</code> | | 顯示匹配的行以及該行 之後(After)/之前(Before)/上下文(Context) 相連的指定數量的行 |
| <code>-E</code> | Extended Regular Expression | 啟用[擴充型正則表達式(ERE)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_Regular_Expression) |
| <code>-P</code> | Perl Compatible Regular Expressions | 啟用 [Perl 相容型正則表達式(PCRE)](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/kotlin_Regular_Expression)(部分系統可能不支援) |
| <code>-w</code> | whole word | 只匹配「完整單字」,避免匹配到其他單字的一部分,例如只想找 `cat`,不要 `concatenate` |
| <code>-m</code> | max-count | 匹配次數限制,找到指定次數就停止搜尋 |
| <code>-F</code> | fixed string | 把文字模式當作「純文字」搜尋,不解析正則 |
| <code>-x</code> | exact match | 整行必須完全等於文字模式才匹配 |
| <code>-s</code> | silent / suppress errors | 不輸出錯誤訊息,例如檔案不存在也不報錯,通常用在批次或自動化,避免錯誤干擾 |
| <code>-q</code> | quiet / suppress output | 不輸出匹配內容,只回傳退出狀態(0 = 有匹配,1 = 無匹配) |
---
1. <code>-n</code>: 顯示行號
```bash=
grep -n "ERROR" log.txt
# 輸出: 2:2024-01-10 ERROR: Connection failed
```
2. <code>-l</code>: 顯示有哪些檔案的檔案內容中具有指定字串
```bash=
# 假設有三個檔案
# log1.txt 內容:
2024-01-10 INFO: System started
2024-01-10 ERROR: Connection failed
# log2.txt 內容:
2024-01-10 INFO: All systems normal
# error_log.txt 內容:
2024-01-10 ERROR: Database timeout
grep -l "ERROR" *.txt
# 輸出:
# log1.txt
# error_log.txt
```
3. <code>-A</code>/<code>-B</code>/<code>-C</code>: 顯示匹配的行以及該行 之後(After) / 之前(Before) / 之前+之後(Context) 指定數量的行
```bash=
# 假設 log.txt 內容:
1: 2024-01-10 09:00:00 用戶登入
2: 2024-01-10 09:00:05 開始連接資料庫
3: 2024-01-10 09:00:10 ERROR: 連接逾時
4: 2024-01-10 09:00:15 重試連接
5: 2024-01-10 09:00:20 連接成功
grep -A 2 "ERROR" log.txt
# 輸出:
# 2024-01-10 09:00:10 ERROR: 連接逾時 ← 符合的行
# 2024-01-10 09:00:15 重試連接 ← 之後第1行
# 2024-01-10 09:00:20 連接成功 ← 之後第2行
grep -B 2 "ERROR" log.txt
# 輸出:
# 2024-01-10 09:00:00 用戶登入 ← 之前第2行
# 2024-01-10 09:00:05 開始連接資料庫 ← 之前第1行
# 2024-01-10 09:00:10 ERROR: 連接逾時 ← 符合的行
grep -C 2 "ERROR" log.txt
# 輸出:
# 2024-01-10 09:00:00 用戶登入 ← 之前第2行
# 2024-01-10 09:00:05 開始連接資料庫 ← 之前第1行
# 2024-01-10 09:00:10 ERROR: 連接逾時 ← 符合的行
# 2024-01-10 09:00:15 重試連接 ← 之後第1行
# 2024-01-10 09:00:20 連接成功 ← 之後第2行
```
4. <code>-m</code>: 匹配次數限制,找到指定次數就停止搜尋
```bash=
# 找到前 2 次匹配就停止搜尋
grep -m 2 "error" app.log
```
5. <code>-F</code>: 把文字模式當作「純文字」搜尋,不解析正則
```bash=
# 直接找字串 a+b*c
grep -F "a+b*c" file.txt
```
6. <code>-x</code>: 整行必須完全等於文字模式才匹配
```bash=
# 只有完全等於 SUCCESS 的行會被匹配
grep -x "SUCCESS" result.txt
```
</div>
</details>
</div>
</details>
---
參考資料
---
- [Linux grep 命令](https://www.runoob.com/linux/linux-comm-grep.html)
- [grep簡介](https://ithelp.ithome.com.tw/m/articles/10265120)
- [Linux 匹配文字 grep 指令用法教學與範例](https://blog.gtwang.org/linux/linux-grep-command-tutorial-examples/)