- 什麼是 Shell
- Shell 類型比較
- 基本語法
- 變數與參數
- 條件判斷
- 迴圈結構
- 函式
- 實用技巧
- 腳本編寫最佳實踐
- 常見問題
- 總結
Shell 是使用者與作業系統核心(Kernel)之間的介面,負責接收使用者指令並傳遞給系統執行。
使用者輸入指令
↓
Shell(解譯器)
↓
Kernel(核心)
↓
硬體執行
| 模式 |
說明 |
範例 |
| 互動模式 |
在終端機輸入指令,立即執行 |
在 Terminal 輸入 ls |
| 腳本模式 |
執行預先寫好的腳本檔案 |
./script.sh |
echo $SHELL
cat /etc/shells
echo $0
ps -p $$
| 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 (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 |
語法直觀,自動補全佳 |
#!/bin/sh # POSIX 相容,最高可攜性
ls -la
cd /tmp; ls; pwd
mkdir test && cd test && touch file.txt
cat file.txt || echo "檔案不存在"
long_running_command &
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 |
見下方範例 |
cat << EOF
這是第一行
這是第二行
變數展開:$HOME
EOF
cat << 'EOF'
這裡的 $HOME 不會被展開
EOF
| 引號 |
名稱 |
變數展開 |
特殊字元 |
範例 |
"..." |
雙引號 |
✅ 展開 |
部分保留 |
"$HOME is home" |
'...' |
單引號 |
❌ 不展開 |
全部保留 |
'$HOME is literal' |
`...` |
反引號 |
執行指令 |
- |
`date` |
$(...) |
指令替換 |
執行指令 |
- |
$(date) 推薦用法 |
name="World"
echo "Hello $name"
echo 'Hello $name'
echo "Today is $(date)"
name="John"
age=30
echo $name
echo ${name}
echo "${name}!"
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}
echo ${str:0:5}
echo ${str:6}
echo ${str/World/Bash}
echo ${str//o/0}
echo ${str#Hello }
echo ${str%World}
echo ${undefined:-default}
echo ${undefined:=default}
fruits=("apple" "banana" "cherry")
echo ${fruits[0]}
echo ${fruits[1]}
echo ${fruits[@]}
echo ${#fruits[@]}
fruits+=("date")
for fruit in "${fruits[@]}"; do
echo $fruit
done
declare -A person
person[name]="John"
person[age]=30
person[city]="Taipei"
echo ${person[name]}
echo ${!person[@]}
echo ${person[@]}
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
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 $1 in
start)
echo "啟動服務"
;;
stop)
echo "停止服務"
;;
restart)
echo "重啟服務"
;;
*)
echo "用法: $0 {start|stop|restart}"
exit 1
;;
esac
for i in 1 2 3 4 5; do
echo $i
done
for i in {1..5}; do
echo $i
done
for i in {0..10..2}; do
echo $i
done
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
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
count=1
until [ $count -gt 5 ]; do
echo $count
((count++))
done
for i in {1..10}; do
if [ $i -eq 5 ]; then
break
fi
echo $i
done
for i in {1..5}; do
if [ $i -eq 3 ]; then
continue
fi
echo $i
done
function greet() {
echo "Hello, $1!"
}
greet() {
echo "Hello, $1!"
}
greet "World"
greet "Bash"
add() {
local a=$1
local b=$2
echo $((a + b))
}
result=$(add 3 5)
echo "3 + 5 = $result"
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0
else
return 1
fi
}
if is_even 4; then
echo "4 是偶數"
fi
global_var="I am global"
my_func() {
local local_var="I am local"
global_var="Modified"
echo "函式內: $local_var"
}
my_func
echo "函式外: $global_var"
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))
echo $((a * b))
echo $((a / b))
echo $((a % b))
echo $((a ** 2))
let "c = a + b"
echo $c
((a++))
echo $a
find . -name "*.log" -exec rm {} \;
find . -type f | wc -l
for f in *.txt; do
mv "$f" "${f%.txt}.md"
done
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
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 處理清理 |
確保資源釋放 |
name = "John"
name="John"
for file in $(ls *.txt); do
for file in *.txt; do
echo "$file"
done
rm -rf $dir/*
set -u
rm -rf "${dir:?變數未定義}/"*
if ! command -v docker &>/dev/null; then
echo "請先安裝 Docker"
exit 1
fi
chmod +x script.sh
bash script.sh
file script.sh
sed -i 's/\r$//' script.sh
dos2unix script.sh
| 情境 |
推薦 |
| 寫可攜式腳本 |
#!/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 -e |
指令失敗即停止 |
set -u |
未定義變數報錯 |
set -o pipefail |
管線中任一指令失敗即失敗 |
set -x |
顯示執行的指令(除錯) |
建立日期:2025-12-04
最後更新:2025-12-04