Shell 完全指南:sh、Bash、Zsh 比較與使用

完整介紹各種 Shell 的差異、選擇指南、常用語法與腳本編寫技巧


目錄

  1. 什麼是 Shell
  2. Shell 類型比較
  3. 基本語法
  4. 變數與參數
  5. 條件判斷
  6. 迴圈結構
  7. 函式
  8. 實用技巧
  9. 腳本編寫最佳實踐
  10. 常見問題
  11. 總結

什麼是 Shell

Shell 是使用者與作業系統核心(Kernel)之間的介面,負責接收使用者指令並傳遞給系統執行。

使用者輸入指令
    Shell(解譯器)
    Kernel(核心)
    硬體執行

Shell 的兩種模式

模式 說明 範例
互動模式 在終端機輸入指令,立即執行 在 Terminal 輸入 ls
腳本模式 執行預先寫好的腳本檔案 ./script.sh

查看系統 Shell

# 查看當前使用的 Shell
echo $SHELL

# 查看系統可用的 Shell
cat /etc/shells

# 查看當前 Shell 進程
echo $0
ps -p $$

Shell 類型比較

主要 Shell 一覽

Shell 全名 發布年份 特點
sh Bourne Shell 1979 Unix 原始 Shell,最基本
bash Bourne Again Shell 1989 Linux 預設,最廣泛使用
zsh Z Shell 1990 功能強大,macOS 預設
ksh Korn Shell 1983 商業 Unix 常見
csh C Shell 1978 類 C 語法
fish Friendly Interactive Shell 2005 現代化,使用者友善
dash Debian Almquist Shell 1997 輕量快速,符合 POSIX

sh vs bash vs zsh 詳細比較

特性 sh (POSIX) bash zsh
相容性 最高(標準) 高(相容 sh) 高(相容 sh/bash)
陣列 ❌ 不支援 ✅ 一維陣列 ✅ 關聯陣列
字串操作 基本 豐富 更豐富
自動補全 基本 良好 優秀(可擴展)
提示符自訂 基本 良好 優秀
外掛系統 有限 ✅ Oh My Zsh
啟動速度 最快 較慢(外掛多時)
預設系統 腳本標準 Linux macOS (10.15+)

如何選擇?

使用情境 建議 Shell 原因
寫可攜式腳本 sh / bash 相容性最高
Linux 日常使用 bash 預設且功能完整
macOS 日常使用 zsh 系統預設 + 強大功能
追求效率與美觀 zsh + Oh My Zsh 外掛生態豐富
系統啟動腳本 dash / sh 啟動速度快
初學者 fish 語法直觀,自動補全佳

Shebang(腳本首行)

#!/bin/sh        # POSIX 相容,最高可攜性
#!/bin/bash      # 使用 Bash 特性
#!/bin/zsh       # 使用 Zsh 特性
#!/usr/bin/env bash  # 推薦:自動找 bash 路徑

基本語法

指令執行

# 單一指令
ls -la

# 多指令(依序執行)
cd /tmp; ls; pwd

# 多指令(前一個成功才執行下一個)
mkdir test && cd test && touch file.txt

# 多指令(前一個失敗才執行下一個)
cat file.txt || echo "檔案不存在"

# 背景執行
long_running_command &

# 管線(Pipe)
cat file.txt | grep "pattern" | sort | uniq

重導向

符號 說明 範例
> 輸出覆寫 echo "text" > file.txt
>> 輸出附加 echo "text" >> file.txt
< 輸入重導向 sort < file.txt
2> 錯誤輸出 cmd 2> error.log
2>&1 錯誤合併到標準輸出 cmd > all.log 2>&1
&> 全部輸出(bash) cmd &> all.log
<< Here Document 見下方範例
# Here Document(多行輸入)
cat << EOF
這是第一行
這是第二行
變數展開:$HOME
EOF

# Here Document(不展開變數)
cat << 'EOF'
這裡的 $HOME 不會被展開
EOF

引號差異

引號 名稱 變數展開 特殊字元 範例
"..." 雙引號 ✅ 展開 部分保留 "$HOME is home"
'...' 單引號 ❌ 不展開 全部保留 '$HOME is literal'
`...` 反引號 執行指令 - `date`
$(...) 指令替換 執行指令 - $(date) 推薦用法
name="World"

echo "Hello $name"      # Hello World(變數展開)
echo 'Hello $name'      # Hello $name(字面值)
echo "Today is $(date)" # Today is Wed Dec 4...(指令替換)

變數與參數

變數定義與使用

# 定義變數(等號前後不能有空格!)
name="John"
age=30

# 使用變數
echo $name
echo ${name}      # 推薦:明確變數邊界
echo "${name}!"   # John!

# 唯讀變數
readonly PI=3.14159

# 刪除變數
unset name

特殊變數

變數 說明
$0 腳本名稱
$1 ~ $9 位置參數(第 1~9 個參數)
${10} 第 10 個以後的參數
$# 參數個數
$@ 所有參數(個別字串)
$* 所有參數(單一字串)
$? 上一指令的結束狀態(0=成功)
$$ 當前 Shell 的 PID
$! 最近背景程序的 PID
#!/bin/bash
echo "腳本名稱: $0"
echo "參數個數: $#"
echo "第一個參數: $1"
echo "所有參數: $@"
echo "上一指令狀態: $?"

字串操作

str="Hello World"

# 字串長度
echo ${#str}              # 11

# 子字串
echo ${str:0:5}           # Hello(從 0 開始取 5 個字)
echo ${str:6}             # World(從 6 開始到結尾)

# 取代
echo ${str/World/Bash}    # Hello Bash(取代第一個)
echo ${str//o/0}          # Hell0 W0rld(取代全部)

# 刪除
echo ${str#Hello }        # World(刪除開頭匹配)
echo ${str%World}         # Hello (刪除結尾匹配)

# 預設值
echo ${undefined:-default}    # 如果未定義,使用 default
echo ${undefined:=default}    # 如果未定義,設定並使用 default

陣列(Bash/Zsh)

# 定義陣列
fruits=("apple" "banana" "cherry")

# 存取元素
echo ${fruits[0]}         # apple(bash 從 0 開始)
echo ${fruits[1]}         # banana

# 所有元素
echo ${fruits[@]}         # apple banana cherry

# 陣列長度
echo ${#fruits[@]}        # 3

# 新增元素
fruits+=("date")

# 遍歷陣列
for fruit in "${fruits[@]}"; do
    echo $fruit
done

關聯陣列(Bash 4+ / Zsh)

# 宣告關聯陣列
declare -A person

# 賦值
person[name]="John"
person[age]=30
person[city]="Taipei"

# 存取
echo ${person[name]}      # John

# 所有鍵
echo ${!person[@]}        # name age city

# 所有值
echo ${person[@]}         # John 30 Taipei

條件判斷

if 語法

# 基本語法
if [ condition ]; then
    commands
elif [ condition ]; then
    commands
else
    commands
fi

# 範例
if [ -f "/etc/passwd" ]; then
    echo "檔案存在"
else
    echo "檔案不存在"
fi

測試運算子

檔案測試

運算子 說明
-e file 檔案存在
-f file 是普通檔案
-d file 是目錄
-r file 可讀
-w file 可寫
-x file 可執行
-s file 檔案大小 > 0
-L file 是符號連結
if [ -d "/tmp" ]; then
    echo "/tmp 是目錄"
fi

if [ -x "./script.sh" ]; then
    echo "腳本可執行"
fi

字串測試

運算子 說明
-z str 字串長度為 0
-n str 字串長度不為 0
str1 = str2 字串相等
str1 != str2 字串不相等
name=""
if [ -z "$name" ]; then
    echo "name 是空的"
fi

if [ "$USER" = "root" ]; then
    echo "你是 root"
fi

數值比較

運算子 說明
-eq 等於 (equal)
-ne 不等於 (not equal)
-gt 大於 (greater than)
-ge 大於等於 (greater or equal)
-lt 小於 (less than)
-le 小於等於 (less or equal)
age=18
if [ $age -ge 18 ]; then
    echo "已成年"
fi

[[ ]] vs [ ](Bash/Zsh 擴展)

# [[ ]] 是 Bash/Zsh 擴展,更強大
# 支援 && || 邏輯運算
if [[ $age -ge 18 && $age -le 65 ]]; then
    echo "工作年齡"
fi

# 支援正則表達式
if [[ $email =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then
    echo "Email 格式正確"
fi

# 支援模式匹配
if [[ $filename == *.txt ]]; then
    echo "是文字檔"
fi

case 語法

case $1 in
    start)
        echo "啟動服務"
        ;;
    stop)
        echo "停止服務"
        ;;
    restart)
        echo "重啟服務"
        ;;
    *)
        echo "用法: $0 {start|stop|restart}"
        exit 1
        ;;
esac

迴圈結構

for 迴圈

# 列表迴圈
for i in 1 2 3 4 5; do
    echo $i
done

# 範圍迴圈(Bash)
for i in {1..5}; do
    echo $i
done

# 帶步進的範圍
for i in {0..10..2}; do
    echo $i    # 0 2 4 6 8 10
done

# C 風格迴圈(Bash)
for ((i=0; i<5; i++)); do
    echo $i
done

# 遍歷檔案
for file in *.txt; do
    echo "處理: $file"
done

# 遍歷指令輸出
for user in $(cat /etc/passwd | cut -d: -f1); do
    echo "使用者: $user"
done

while 迴圈

# 基本 while
count=1
while [ $count -le 5 ]; do
    echo $count
    ((count++))
done

# 讀取檔案每一行
while IFS= read -r line; do
    echo "行: $line"
done < file.txt

# 無限迴圈
while true; do
    echo "執行中..."
    sleep 1
done

until 迴圈

# 條件為假時執行
count=1
until [ $count -gt 5 ]; do
    echo $count
    ((count++))
done

迴圈控制

# break:跳出迴圈
for i in {1..10}; do
    if [ $i -eq 5 ]; then
        break
    fi
    echo $i
done

# continue:跳過本次迭代
for i in {1..5}; do
    if [ $i -eq 3 ]; then
        continue
    fi
    echo $i    # 1 2 4 5(跳過 3)
done

函式

定義與呼叫

# 定義函式(兩種寫法)
function greet() {
    echo "Hello, $1!"
}

# 或
greet() {
    echo "Hello, $1!"
}

# 呼叫函式
greet "World"    # Hello, World!
greet "Bash"     # Hello, Bash!

函式參數與返回值

# 函式參數
add() {
    local a=$1
    local b=$2
    echo $((a + b))
}

result=$(add 3 5)
echo "3 + 5 = $result"    # 3 + 5 = 8

# 返回狀態碼
is_even() {
    if [ $(($1 % 2)) -eq 0 ]; then
        return 0    # 成功(偶數)
    else
        return 1    # 失敗(奇數)
    fi
}

if is_even 4; then
    echo "4 是偶數"
fi

區域變數

# 使用 local 宣告區域變數
global_var="I am global"

my_func() {
    local local_var="I am local"
    global_var="Modified"
    echo "函式內: $local_var"
}

my_func
echo "函式外: $global_var"    # Modified
echo "函式外: $local_var"     # (空,因為是區域變數)

實用技巧

指令替換

# 取得當前日期
today=$(date +%Y-%m-%d)
echo "Today: $today"

# 取得檔案行數
lines=$(wc -l < file.txt)
echo "行數: $lines"

# 巢狀指令替換
files=$(ls $(dirname $0))

算術運算

# $(( )) 算術展開
a=5
b=3
echo $((a + b))     # 8
echo $((a * b))     # 15
echo $((a / b))     # 1
echo $((a % b))     # 2
echo $((a ** 2))    # 25(次方)

# let 指令
let "c = a + b"
echo $c             # 8

# (( )) 算術求值
((a++))
echo $a             # 6

常用指令組合

# 查找並處理檔案
find . -name "*.log" -exec rm {} \;

# 查找並統計
find . -type f | wc -l

# 批次重新命名
for f in *.txt; do
    mv "$f" "${f%.txt}.md"
done

# 平行處理(xargs)
cat urls.txt | xargs -P 4 -I {} curl -O {}

# 監控日誌
tail -f /var/log/syslog | grep --line-buffered "error"

除錯技巧

# 開啟除錯模式
set -x    # 顯示執行的每一行
set +x    # 關閉除錯

# 遇錯即停
set -e    # 任何指令失敗就停止
set +e    # 關閉

# 未定義變數報錯
set -u    # 使用未定義變數報錯

# 常用組合(腳本開頭)
set -euo pipefail

# 執行時除錯
bash -x script.sh

腳本編寫最佳實踐

腳本模板

#!/usr/bin/env bash
#
# 腳本名稱: script.sh
# 描述: 這個腳本做什麼
# 作者: Your Name
# 日期: 2024-01-01
#

set -euo pipefail

# 常數定義
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"

# 顏色定義
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color

# 日誌函式
log_info() {
    echo -e "${GREEN}[INFO]${NC} $*"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $*" >&2
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $*" >&2
}

# 用法說明
usage() {
    cat << EOF
用法: $SCRIPT_NAME [選項] <參數>

描述:
    這個腳本的功能說明

選項:
    -h, --help      顯示此幫助訊息
    -v, --verbose   詳細輸出
    -f, --file      指定檔案

範例:
    $SCRIPT_NAME -f config.txt
    $SCRIPT_NAME --help
EOF
    exit 1
}

# 清理函式(腳本結束時執行)
cleanup() {
    log_info "清理臨時檔案..."
    # rm -rf "$TMP_DIR"
}
trap cleanup EXIT

# 主函式
main() {
    # 解析參數
    while [[ $# -gt 0 ]]; do
        case $1 in
            -h|--help)
                usage
                ;;
            -v|--verbose)
                VERBOSE=true
                shift
                ;;
            -f|--file)
                FILE="$2"
                shift 2
                ;;
            *)
                log_error "未知選項: $1"
                usage
                ;;
        esac
    done

    # 主要邏輯
    log_info "腳本開始執行"
    # ...
    log_info "腳本執行完成"
}

# 執行主函式
main "$@"

最佳實踐檢查清單

項目 說明
使用 #!/usr/bin/env bash 可攜性更好
加入 set -euo pipefail 更嚴格的錯誤處理
變數用雙引號包起來 防止空格問題:"$var"
使用 ${var} 而非 $var 明確變數邊界
函式內用 local 宣告變數 避免污染全域
檢查指令是否存在 command -v cmd &>/dev/null
提供 --help 選項 使用者友善
使用 trap 處理清理 確保資源釋放

常見問題

問題 1:空格導致錯誤

# ❌ 錯誤:等號前後有空格
name = "John"

# ✅ 正確:等號前後不能有空格
name="John"

# ❌ 錯誤:沒有引號,檔名有空格會出錯
for file in $(ls *.txt); do

# ✅ 正確:使用引號
for file in *.txt; do
    echo "$file"
done

問題 2:變數未定義

# ❌ 危險:如果 $dir 未定義,會刪除根目錄!
rm -rf $dir/*

# ✅ 安全:使用 set -u 或檢查
set -u
rm -rf "${dir:?變數未定義}/"*

問題 3:指令找不到

# 檢查指令是否存在
if ! command -v docker &>/dev/null; then
    echo "請先安裝 Docker"
    exit 1
fi

問題 4:腳本沒有執行權限

# 加上執行權限
chmod +x script.sh

# 或直接用 bash 執行
bash script.sh

問題 5:換行符問題(Windows vs Unix)

# 檢查檔案換行符
file script.sh

# 轉換 Windows (CRLF) 到 Unix (LF)
sed -i 's/\r$//' script.sh
# 或
dos2unix script.sh

總結

Shell 選擇指南

情境 推薦
寫可攜式腳本 #!/bin/sh#!/usr/bin/env bash
Linux 日常使用 Bash
macOS 日常使用 Zsh(系統預設)
追求效率美觀 Zsh + Oh My Zsh
初學者 Fish(語法直觀)

語法快速參考

操作 語法
變數賦值 var="value"
使用變數 ${var}"$var"
指令替換 $(command)
算術運算 $((a + b))
條件判斷 if [ condition ]; then ... fi
for 迴圈 for i in list; do ... done
while 迴圈 while [ condition ]; do ... done
函式定義 func() { ... }
檔案測試 -f(檔案) -d(目錄) -e(存在)
字串比較 = != -z(空) -n(非空)
數值比較 -eq -ne -gt -lt -ge -le

常用 set 選項

選項 說明
set -e 指令失敗即停止
set -u 未定義變數報錯
set -o pipefail 管線中任一指令失敗即失敗
set -x 顯示執行的指令(除錯)

建立日期:2025-12-04 最後更新:2025-12-04

🔗相關文章