16  打造個人 CLI 工具

當沒有現成工具滿足你的需求時,自己寫一個。

16.1 Shell Script 基礎

16.1.1 基本結構

背景(問題發現):在日常工作中,我們經常需要自動化重複性任務,但手動輸入命令既耗時又容易出錯。Shell script 提供了一個將命令序列化、可重複執行的解決方案。

方法:Shell script 的基礎建立在三個核心概念上: - Shebang (#!/bin/bash):告訴系統用哪個直譯器執行腳本 - 變數:儲存和重用資料,使用 $ 符號引用 - 命令替換 $():將命令的輸出儲存為變數值

結果(程式碼)

#!/bin/bash
# 這是註解

# 變數
name="World"
echo "Hello, $name!"

# 命令替換
today=$(date +%Y-%m-%d)
echo "Today is $today"

討論/延伸: - 變數賦值時等號兩邊不能有空格(name="World" 正確,name = "World" 錯誤) - 使用雙引號 "$var" 可以正確處理包含空格的變數值 - 命令替換也可使用反引號 `command`,但 $() 更現代且易讀 - 考慮使用 shellcheck 工具檢查腳本常見錯誤

16.1.2 條件判斷

背景(問題發現):腳本經常需要根據檔案是否存在、是檔案還是目錄等條件執行不同的操作。沒有條件判斷,腳本無法智能地處理各種情境。

方法:使用 if-elif-else 結構配合測試表達式 [ ]: - -f file:檢查是否為一般檔案 - -d file:檢查是否為目錄 - -e file:檢查是否存在(任何類型) - 使用雙引號 "$var" 防止變數為空時出錯

結果(程式碼)

if [ -f "$file" ]; then
    echo "File exists"
elif [ -d "$file" ]; then
    echo "It's a directory"
else
    echo "Not found"
fi

討論/延伸: - [ ][[ ]] 的差異:[[ ]] 是 bash 擴展,支援更多功能(如正則、&&||) - 常用測試運算子:-z (字串為空)、-n (字串非空)、-eq (數字相等) - 字串比較:= (相等)、!= (不等)、< (字典序小於) - 邏輯運算:&& (且)、|| (或)、! (非)

16.1.3 迴圈

背景(問題發現):需要對一批檔案或資料執行相同操作時,手動處理每個項目不切實際。迴圈允許我們批次處理資料,大幅提升效率。

方法:兩種主要迴圈模式: - for 迴圈:遍歷已知的項目清單(如檔案、陣列元素) - while 迴圈:持續執行直到條件不滿足,常用於逐行讀取檔案 - read -r:讀取一行輸入,-r 防止反斜線被解釋為跳脫字元

結果(程式碼)

# for 迴圈
for file in *.md; do
    echo "Processing $file"
done

# while 迴圈
while read -r line; do
    echo "$line"
done < input.txt

討論/延伸: - for 迴圈的其他形式:for i in {1..10} (序列)、for ((i=0; i<10; i++)) (C-style) - 處理包含空格的檔名:使用 while IFS= read -r -d '' file; do ... done < <(find . -print0) - breakcontinue:提前跳出迴圈或跳過當次迭代 - 平行處理:考慮使用 xargs -P 或 GNU parallel 加速批次作業

16.1.4 函數

背景(問題發現):當某段邏輯需要在多處重複使用時,複製貼上程式碼會導致維護困難。函數提供了程式碼重用和模組化的機制。

方法:函數定義和使用的關鍵概念: - function name()name():定義函數 - $1, $2, ...:位置參數,代表傳入的第一個、第二個引數 - local:宣告區域變數,避免污染全域命名空間 - 直接呼叫函數名並傳入參數即可執行

結果(程式碼)

function greet() {
    local name="$1"
    echo "Hello, $name!"
}

greet "World"

討論/延伸: - $@$*:代表所有位置參數,"$@" 正確保留每個參數的引號 - $#:參數數量 - return vs exitreturn 退出函數並返回狀態碼,exit 終止整個腳本 - 函數可以放在獨立檔案中,用 source. 載入以便重用 - 最佳實踐:為函數添加說明註解、驗證參數數量和類型

16.2 實用工具範例

16.2.1 建立並進入目錄

背景(問題發現):建立新目錄後通常需要立即進入該目錄開始工作。傳統做法需要執行兩個命令:mkdir new-project && cd new-project,重複輸入目錄名稱既繁瑣又容易打錯。

方法:組合兩個命令並利用特殊變數: - mkdir -p "$@":建立目錄,-p 確保父目錄存在,"$@" 接收所有參數 - &&:邏輯 AND,只有前一命令成功才執行後一命令 - $_:特殊變數,代表上一個命令的最後一個參數(即目錄名稱)

結果(程式碼)

function mkcd() {
    mkdir -p "$@" && cd "$_"
}

討論/延伸: - 這是最常用的 shell 函數之一,大幅提升工作流效率 - 支援建立巢狀目錄:mkcd path/to/nested/dir - 變體:可加入錯誤處理,如 mkdir -p "$@" && cd "$_" || return 1 - 可擴展功能:建立目錄後自動初始化 git、建立 README 等

16.2.2 建立日期目錄

背景(問題發現):組織工作日誌、會議記錄或每日任務時,經常需要建立以日期命名的目錄。手動輸入日期格式(如 2024-03-15)容易出錯且浪費時間。

方法:自動產生今天的日期並建立對應目錄: - date +%F:輸出 ISO 8601 格式日期(YYYY-MM-DD) - $():命令替換,將 date 輸出存入變數 - local:確保變數只在函數內有效 - 組合 mkdircd 建立並進入目錄

結果(程式碼)

function mkymd() {
    local d="$(date +%F)"
    mkdir -p "./$d"
    cd "./$d"
}

討論/延伸: - 日期格式變體:%Y%m%d (20240315)、%Y/%m/%d (2024/03/15) - 可加入時間戳:date +%F_%H%M%S 產生 2024-03-15_143052 - 進階版本:接受參數指定日期偏移,如 mkymd -1 建立昨天的目錄 - 應用場景:每日工作日誌、會議筆記歸檔、臨時測試環境

16.2.3 快速建立新專案

背景(問題發現):開始新專案時需要重複執行一系列步驟:建立目錄、初始化 git、建立 README、開啟編輯器等。手動執行這些步驟既耗時又容易遺漏某些步驟。

方法:整合專案初始化流程的自動化函數: - read -r:互動式讀取使用者輸入,-r 保留輸入的原始內容 - echo -n:不換行的提示訊息,讓輸入在同一行 - 使用日期作為目錄前綴方便排序和管理 - 自動建立基本的 README 檔案 - 初始化 git 倉庫並開啟編輯器

結果(程式碼)

function newpj() {
    echo -n "Enter project name: "
    read -r project_name

    echo -n "Enter due date (YYYY-MM-DD): "
    read -r due_date

    local folder_path=~/projects/"$due_date-$project_name"
    mkdir -p "$folder_path"
    echo "# $project_name" > "$folder_path/README.md"

    cd "$folder_path"
    git init
    nvim README.md
}

討論/延伸: - 可加入參數驗證:檢查日期格式、專案名稱不為空 - 擴展模板功能:複製預設的 .gitignoreLICENSE 等檔案 - 整合專案管理工具:自動在 GitHub 建立遠端倉庫、設定追蹤問題 - 支援不同專案類型:Node.js (npm init)、Python (uv init)、Rust (cargo new) 等 - 可改用參數而非互動式輸入:newpj myproject 2024-12-31

16.2.4 轉換影片為 GIF

背景(問題發現):在文件、README 或技術部落格中展示操作流程時,GIF 動畫比靜態截圖更直觀。但 ffmpeg 的參數複雜,每次都要查文件才能記得正確的轉換命令。

方法:封裝 ffmpeg 轉換邏輯並設定合理的預設值: - -z "$1":檢查第一個參數是否為空 - ${input%.mp4}:移除 .mp4 副檔名(bash 字串操作) - -r 15:設定 15fps 幀率(平衡檔案大小和流暢度) - -vf "scale=720:-1":縮放寬度到 720px,高度自動等比例調整 - -ss-to:指定擷取的時間範圍(前 10 秒)

結果(程式碼)

function mp4_to_gif() {
    if [[ -z "$1" ]]; then
        echo "Usage: mp4_to_gif <input.mp4>"
        return 1
    fi

    local input="$1"
    local output="${input%.mp4}.gif"

    ffmpeg -y -i "$input" \
        -r 15 \
        -vf "scale=720:-1" \
        -ss 00:00:00 \
        -to 00:00:10 \
        "$output"

    echo "Created: $output"
}

討論/延伸: - 優化檔案大小:使用調色盤 -vf "fps=10,scale=480:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" - 支援自訂時間範圍:改為接受三個參數 mp4_to_gif input.mp4 00:00:05 00:00:15 - 批次處理:結合 for 迴圈轉換目錄下所有 MP4 檔案 - 替代工具:gifsicle 可進一步壓縮 GIF、gifski 提供更高品質的轉換

16.2.5 提醒功能

背景(問題發現):在終端機工作時突然想到需要處理的事項,但切換到提醒 App 會打斷工作流程。需要一個快速的方式直接從命令列新增提醒。

方法:封裝 macOS reminders CLI 工具,簡化常用操作: - 位置參數:$1 提醒文字、$2 到期日期 - ${3:-Inbox}:第三個參數預設值為 “Inbox”(bash 參數擴展語法) - 使用 macOS 內建的 reminders 命令列工具(需先安裝) - --due-date:設定提醒的到期日期

結果(程式碼)

function remind() {
    local text="$1"
    local date="$2"
    local list="${3:-Inbox}"

    reminders add "$list" "$text" --due-date "$date"
}

討論/延伸: - 使用方式:remind "買牛奶" "2024-03-20" "購物" - macOS reminders 工具需透過 Homebrew 安裝:brew install reminders-cli - 可擴展為支援更多選項:優先級、標籤、重複提醒等 - 跨平台替代方案:整合 Todoist API、Google Tasks API 或其他任務管理服務 - 進階版本:解析自然語言日期(如 “明天”、“下週五”)

16.3 腳本組織

把常用函數放在模組化檔案中:

~/.dotfiles/
├── zsh/
│   └── modules/
│       ├── functions.zsh   # 通用函數
│       ├── note_related.zsh # 筆記相關
│       └── snippets.zsh    # 文字片段
└── shellscripts/
    ├── backup.sh
    └── deploy.sh

.zshrc 中載入:

背景(問題發現):當自訂函數和腳本越來越多時,全部塞在 .zshrc 會導致檔案臃腫難以維護。需要一個模組化的組織方式來管理不同類別的功能。

方法:模組化載入策略: - 將相關函數分類到獨立的 .zsh 檔案中 - 使用 source 命令載入模組檔案 - $DOTFILES 變數指向 dotfiles 目錄,便於跨機器同步 - 按功能分類:通用工具、筆記管理、文字片段等

結果(程式碼)

source "$DOTFILES/zsh/modules/functions.zsh"
source "$DOTFILES/zsh/modules/note_related.zsh"

討論/延伸: - 延遲載入優化:只在需要時才載入模組,加快 shell 啟動速度 - 使用迴圈批次載入:for module in $DOTFILES/zsh/modules/*.zsh; do source "$module"; done - 條件載入:根據作業系統或主機名稱載入特定模組 - 版本控制:將模組化的 dotfiles 放入 git,便於在多台電腦間同步 - 除錯技巧:在 source 前加 set -x 追蹤載入過程

16.4 進階:Python CLI

對於複雜工具,考慮用 Python:

背景(問題發現):當 CLI 工具需要複雜的邏輯、資料處理、API 呼叫或跨平台支援時,Shell script 會變得難以維護。Python 提供了更好的結構化程式設計能力和豐富的生態系統。

方法:使用 Click 框架快速建立 Python CLI 工具: - #!/usr/bin/env python3:使腳本可執行,自動找到 Python 3 直譯器 - @click.command():裝飾器,將函數轉換為 CLI 命令 - @click.option():定義命令列選項,支援預設值、說明文字、型別驗證 - click.echo():跨平台的輸出函數(比 print 更可靠)

結果(程式碼)

#!/usr/bin/env python3
import click

@click.command()
@click.option('--name', default='World', help='Name to greet')
def hello(name):
    """Simple program that greets NAME."""
    click.echo(f'Hello, {name}!')

if __name__ == '__main__':
    hello()

討論/延伸: - 安裝 Click:pip install clickuv add click - 使腳本可執行:chmod +x script.py,然後直接執行 ./script.py --name Alice - Click 進階功能:子命令、參數驗證、互動式提示、進度條、顏色輸出 - 替代框架:argparse(標準庫)、typer(基於型別提示)、fire(自動生成 CLI) - 打包發布:使用 setuptoolspoetry 將工具打包成可安裝的套件 - 測試:使用 Click 的 CliRunner 進行單元測試

16.5 實作練習

  1. 寫一個建立日期目錄的函數
  2. 寫一個批次重新命名檔案的腳本
  3. 為常用工作流建立自訂函數
Tip建議

從簡單的 shell function 開始,當複雜度增加時再考慮用 Python 重寫。