前言
許多開發者認為語言設計、DSL(領域專用語言)、語言工具的建構過程過於抽象。然而,資訊界早已存在一個歷史悠久的 DSL 環境 —— Unix 命令列。
在深入學習 lexing、parsing、IR、codegen 等抽象的語言工程之前,不妨先花些時間探索 Unix 命令列,親身體驗「DSL」如何在日常操作中運作。
語法
在 Unix 世界裡,語法就是指令:
- Shell 內建指令與 POSIX 指令:可視為這個 DSL 的核心語法,通常不必刻意區分。
- AWK:功能更強的「進階語法」,適合處理文字與資料流。
- Make 等工具:帶有小型 DSL 的指令,形成特定情境下的子系統。
- Perl、Python、Ruby:屬於大型子系統,提供更通用的程式語言能力。
學習程式設計並不是要先讀完整本 language reference 才能開始寫程式。
Unix 亦然 —— 邊學邊用,才能真正進步。
REPL
在 Unix 世界裡,命令列本身就是 REPL:
- 這個 REPL 相較於多數程式語言的 REPL 危險許多,因為它會直接改變系統的一部分。
- Shell 語法可以直接輸入在命令列上,但通常不太實用。
- 在尚未熟悉 Shell 前,不要隨意修改設定檔,更不適合用設定檔來練習。
- 若想安全地探索這個 REPL,可以先下載一些外部資料,例如 Yahoo 個股或 ETF 的股價表,避免影響系統本身。
資料型態
在 Unix 這個 DSL 中,資料型態並不是核心重點 —— 幾乎所有東西都以字串形式存在:
指令、檔案名稱、目錄名稱,全都是字串。
由於 Shell 缺乏複合型態與資料結構,它並不適合用來撰寫演算法。
但這並非缺陷,而是設計上的本意:Shell 的角色在於操作系統,而非成為通用程式語言。
運算子
Unix 也有自己的一套「運算子」:
- Pipe (
|):用來串接多個指令,形成資料流的管線。 - 重導 (redirection):決定資料流的方向,例如輸入、輸出或錯誤訊息。
- 判斷式 (predicate):如
-z、-f、-d等,用來檢查字串是否為空、檔案是否存在、路徑是否為目錄。 - 整數運算:雖然效能不佳,但在需要簡單計數時仍可派上用場。
- 浮點數運算:Shell 並未提供,因為這並非它的設計目的。
這些運算子讓 Shell 能夠靈活地編排系統操作,而不是專注於數值計算。
敘述
在 Unix 世界裡,指令本身就是敘述:
- 敘述可以只由一個指令構成。
- 也可以透過 pipe 串接多個指令,形成一個更長、更複雜的敘述。
- 每個敘述都會真實地改變系統的一部分,因為它直接操作底層環境。
- 雖然 Shell 並不是 functional language,但若小心設計,仍能區分哪些敘述具有 side effect,哪些則是純粹操作。
語法糖
在 Unix 中,語法糖就是 alias。
它適用於那些短到不值得包進腳本裡的單行敘述,讓常用指令更方便。
以下是一個常見的範例:
alias mv='mv -i'
alias cp='cp -i'
alias rm='rm -i'
alias ls='ls --color=auto -F'
alias grep='grep --color=auto'
alias ll='ls -lh'
alias la='ls -A'
透過 alias,我們能快速建立習慣化的指令組合,既省時又降低操作失誤的風險。
函式
Shell 也支援 函式,用來封裝並重複使用一段較長的敘述。以下是一個範例:
ABS_PATH ()
(
cd -- "$(dirname -- "$1")" && printf '%s/%s\n' "$(pwd)" "$(basename -- "$1")"
)
這個函式會回傳指定檔案的絕對路徑,讓你在腳本或命令列中更方便地處理路徑問題。
模組
既然指令是 Unix 的語法,那麼 系統路徑 (PATH) 就可以視為「模組路徑」。
透過修改 PATH,我們能「引入」社群提供的指令,或是自己撰寫的 DSL,讓它們成為環境的一部分:
PATH="$PATH:$HOME/bin"
PATH="$PATH:$HOME/.plenv/bin"
PATH="$PATH:$HOME/opt/racket-9.2/bin"
PATH="$PATH:$HOME/src/ccrun"
PATH="$PATH:$HOME/src/ccwarn"
PATH="$PATH:$HOME/src/ocaml-clean-compile"
PATH="$PATH:$HOME/src/clean-artifact"
PATH="$PATH:$HOME/src/hanparse/bin"
PATH="$PATH:$HOME/src/chunk-spec/bin"
PATH="$PATH:$HOME/src/inplace"
PATH="$PATH:$HOME/src/serpctl/bin"
export PATH
eval "$(plenv init -)"
這樣一來,新的指令就能像模組一樣被「載入」到命令列環境中,與內建語法並肩運作。
DSL
在 Unix 中,命令列腳本本身就是一種 DSL。
腳本能創造新的指令,融入既有環境,與內建指令一同運作。
限於篇幅,這裡不深入介紹腳本語法的細節。
撰寫腳本是探索 Unix 命令列的核心技能。
掌握腳本後,就不再受限於內建「語法」,而能依照自己的習慣設計流程,讓環境更貼近需求。
舉例來說,ocaml-clean-compile 是一系列用來改善 OCaml 編譯流程的腳本。
原本 OCaml 在編譯時會產生大量暫存檔,必須手動刪除,非常麻煩。
這些腳本則會自動在暫存目錄中編譯程式碼,編譯完成後將執行檔傳回,並順手清理暫存目錄,讓流程更乾淨高效。
框架
Oh My Zsh 就是一個典型的 Shell 框架,但它僅能在 Zsh 環境下使用。
這類框架的好處在於:除了原有的「語法」,還能額外提供許多框架專屬的語法,確實能提升生產力。
然而,從 DSL 的角度來看,並不建議長期依賴「框架」。原因有二:
- 可攜性不足:依賴框架的腳本一旦脫離該框架就無法運作。
- 學習受限:過度依賴框架,會錯失將日常摩擦轉化為腳本的機會,難以累積屬於自己的 DSL。
因此,框架適合用來輔助,但真正的價值仍在於透過腳本打造屬於自己的命令列生態。
結語
Unix 命令列的本質是 Environment as a Language。
只要花一些時間學習如何撰寫腳本,就能大幅提升生產力,突破內建工具的限制,打造屬於自己的工作流程。
嘗試探索 Unix 命令列,體驗這個 internal DSL 所帶來的效率與彈性。
當你日後設計或使用其他 DSL 時,會更能理解並感受到 DSL 所帶來的生產力提升。
Unix vs DSL 對照表
| Unix 元素 | DSL 概念 | 說明 |
|---|---|---|
| 指令 (Shell / POSIX) | 語法 | 基本語法單元,直接操作系統。 |
| Pipe、重導 | 運算子 | 控制資料流與指令組合,形成複合語法。 |
判斷式 (-z, -f) |
Predicate | 條件判斷,決定敘述的執行路徑。 |
| 敘述 (單指令 / pipeline) | Statement | 可執行的語法結構,直接改變系統狀態。 |
| 函式 (Shell function) | Function | 可重用的語法封裝,提升抽象層次。 |
alias |
語法糖 | 簡化常用語法,讓操作更直觀。 |
| PATH | 模組路徑 | 引入外部工具或自製 DSL,擴充語法空間。 |
| 腳本 (Shell script) | DSL | 定義新語法與流程,累積成專屬的 DSL。 |
| 框架 (Oh My Zsh 等) | Framework | 提供額外語法與工具,但可攜性有限。 |