实现一个 Shell
实现一个 Shell
每次你在终端输入 ls -la | grep ".go" | wc -l,发生的事情远比表面看起来复杂。你的 shell 必须:解析这行文本,理解 | 的含义,为 ls、grep、wc 分别创建进程,把 ls 的标准输出连接到 grep 的标准输入,把 grep 的标准输出连接到 wc 的标准输入,等待它们全部结束,然后把最终结果打印到你的终端。
这整个过程涉及操作系统最底层的几个概念:进程创建(fork/exec)、文件描述符继承、管道(pipe)、信号(signal)。Shell 是一个进程管理器,是人类与内核之间的语言翻译器。
构建一个 shell 是学习操作系统原理最直接的方式——不是读书上的描述,而是直接调用系统调用,亲手感受内核的行为。这一章我们用 Go 构建一个具备管道、重定向、内建命令、历史记录的 bash-lite。
Level 1 · Shell 的本质
命令解释器与进程管理器
Shell 的英文原意是"贝壳",是操作系统内核(核心)外面的那一层壳。它有两个核心职责:
命令解释器:把人类输入的文本翻译成操作系统能执行的操作。ls -la → 找到 /bin/ls 程序 → 用 -l、-a 参数执行它。
进程管理器:创建和管理子进程。你运行的每一个程序,都是 shell 创建的一个子进程。Shell 决定子进程的标准输入/输出连接到哪里(终端?文件?管道的另一端?),决定什么时候等待子进程结束,决定如何把信号(Ctrl+C、Ctrl+Z)发送给子进程。
Unix 的核心设计哲学之一是:一切皆文件。管道是文件,终端是文件,网络连接也是文件。Shell 的 I/O 重定向和管道,本质上都是在操作文件描述符(File Descriptor,FD)。
为什么构建 Shell 能让你理解 OS
当你实现 |(管道)时,你必须真正理解 pipe() 系统调用:它创建两个文件描述符,一个只读一个只写,写入一端的数据可以从另一端读出。你必须理解 fork 后文件描述符的继承,理解为什么子进程要关闭不用的那一端(否则读端永远不会收到 EOF),理解为什么要在 exec 后关闭父进程的 fd(防止资源泄漏)。
这些知识,读教科书是一回事,自己写代码调试是另一回事。当你第一次成功让 cat file | wc -l 输出正确结果时,你对管道的理解会从概念变成直觉。
POSIX Shell 标准
我们构建的是一个简化版 shell,不完全符合 POSIX sh 标准。真正的 POSIX shell 要处理:变量、算术展开、命令替换、通配符展开、here-doc、函数定义、条件语句……每一个特性都有大量细节。
如果你想要一个 Go 实现的完整 POSIX shell,可以参考 mvdan/sh——一个完整的 POSIX shell 解析器和解释器,连 Bash 扩展([[, (( )) 等)都部分支持。本章的目标不是完整实现,而是通过实现核心子集来深刻理解每个机制。
Level 2 · 核心机制原理
Shell 语法的层次结构
一个 shell 命令行可以分解为以下层次:
命令行(Command Line)
└── 管道(Pipeline):cmd1 | cmd2 | cmd3
└── 简单命令(Simple Command):cmd [args...] [redirections...]
└── 词元(Token):命令名 / 参数 / 重定向符 / 特殊字符
词法分析(Tokenization)把原始字符串切成词元,语法分析(Parsing)把词元组织成命令树,执行(Execution)按照命令树创建进程。
特殊词元:
| 词元 | 含义 |
|---|---|
| |
管道:前一个命令的 stdout 接到后一个的 stdin |
> |
输出重定向:stdout 写到文件(覆盖) |
>> |
输出重定向追加 |
< |
输入重定向:从文件读 stdin |
2> |
错误重定向:stderr 写到文件 |
& |
后台执行:不等待命令结束 |
; |
命令分隔符:顺序执行 |
&& |
短路与:前一个成功才执行下一个 |
|| |
短路或:前一个失败才执行下一个 |
(cmd) |
子 shell:在子进程环境中执行 |
os/exec.Cmd vs syscall.ForkExec
Go 提供两层进程创建 API,它们面向不同的抽象层次:
os/exec.Cmd:高层 API,封装了 fork+exec 的细节。exec.Command("ls", "-la") 创建一个 Cmd 对象,cmd.Start() 启动,cmd.Wait() 等待结束。I/O 通过 cmd.Stdin、cmd.Stdout、cmd.Stderr 字段设置,可以是 os.File、io.Reader/io.Writer,也可以是 os.Pipe() 返回的管道。
syscall.ForkExec / syscall.Exec:直接调用系统调用。syscall.ForkExec 执行 fork(2) + exec(2),可以精细控制子进程的属性(进程组、会话、额外的文件描述符、umask 等)。需要更深层的控制(如实现作业控制、设置 setuid)时才用这层。
对于构建 bash-lite,os/exec.Cmd 提供了足够的控制能力,同时避免了直接操作文件描述符的低级细节。当需要实现作业控制(fg/bg、Ctrl+Z)时,才需要用到 syscall.SysProcAttr。
进程组与作业控制
作业控制(Job Control)允许用户在前台和后台之间移动进程,挂起和恢复进程。这依赖于操作系统的进程组(Process Group)和会话(Session)概念。
一个进程组是一组相关进程的集合,有一个进程组 ID(PGID)。一个管道 cmd1 | cmd2 | cmd3 的三个进程通常属于同一个进程组。
信号可以发送给整个进程组:kill(-pgid, SIGTERM) 会向进程组中的所有进程发送 SIGTERM。这是 Ctrl+C 能终止整个管道(而不只是第一个进程)的原因。
// 让子进程成为新进程组的 leader
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true, // 子进程的 PGID = 子进程的 PID(成为新进程组的 leader)
}
文件描述符继承
fork() 后,子进程继承父进程的所有文件描述符。这既是管道工作的基础,也是资源泄漏的来源。
对于管道 cmd1 | cmd2,正确的 fd 管理流程:
父进程创建管道: pipe(read_fd, write_fd)
Fork cmd1:
- cmd1 的 stdout = write_fd(把 cmd1 的输出写到管道)
- cmd1 关闭 read_fd(cmd1 不读管道,无需保留读端)
- 父进程关闭 write_fd(父进程不写,无需保留写端)
Fork cmd2:
- cmd2 的 stdin = read_fd(从管道读)
- cmd2 关闭 write_fd(cmd2 不写管道,无需保留写端)
- 父进程关闭 read_fd(父进程不读,无需保留读端)
如果 cmd1 的进程不关闭 read_fd,cmd1 持有管道读端的引用,当 cmd1 进程退出时这个引用才会消失。但如果 cmd2 在等 cmd1 的数据,而 cmd1 还活着(哪怕不写任何数据),管道读端就永远不会收到 EOF,cmd2 就会永远阻塞。这就是"僵死管道"(pipe deadlock)。
Level 3 · 从零构建 bash-lite
项目结构
bashlit/
├── main.go ← REPL 主循环
├── lexer/
│ └── lexer.go ← 词法分析:字符串 → Token 列表
├── parser/
│ └── parser.go ← 语法分析:Token 列表 → 命令树
├── executor/
│ └── executor.go ← 执行:命令树 → 创建进程
└── builtin/
└── builtin.go ← 内建命令:cd, exit, export, history
第一步:词法分析器
package lexer
import (
"strings"
"unicode"
)
type TokenType int
const (
WORD TokenType = iota // 普通词(命令名、参数)
PIPE // |
REDIR_OUT // >
REDIR_APP // >>
REDIR_IN // <
REDIR_ERR // 2>
BACKGROUND // &
SEMICOLON // ;
AND // &&
OR // ||
LPAREN // (
RPAREN // )
EOF
)
type Token struct {
Type TokenType
Val string
}
type Lexer struct {
input []rune
pos int
}
func New(input string) *Lexer {
return &Lexer{input: []rune(input)}
}
func (l *Lexer) Tokenize() []Token {
var tokens []Token
for l.pos < len(l.input) {
ch := l.input[l.pos]
// 跳过空白
if unicode.IsSpace(ch) {
l.pos++
continue
}
// 引号字符串
if ch == '"' || ch == '\'' {
tokens = append(tokens, Token{WORD, l.readQuoted(ch)})
continue
}
// 特殊字符处理
switch {
case ch == '|' && l.peek() == '|':
tokens = append(tokens, Token{OR, "||"})
l.pos += 2
case ch == '&' && l.peek() == '&':
tokens = append(tokens, Token{AND, "&&"})
l.pos += 2
case ch == '>':
if l.peek() == '>' {
tokens = append(tokens, Token{REDIR_APP, ">>"})
l.pos += 2
} else {
tokens = append(tokens, Token{REDIR_OUT, ">"})
l.pos++
}
case ch == '<':
tokens = append(tokens, Token{REDIR_IN, "<"})
l.pos++
case ch == '|':
tokens = append(tokens, Token{PIPE, "|"})
l.pos++
case ch == '&':
tokens = append(tokens, Token{BACKGROUND, "&"})
l.pos++
case ch == ';':
tokens = append(tokens, Token{SEMICOLON, ";"})
l.pos++
case ch == '(':
tokens = append(tokens, Token{LPAREN, "("})
l.pos++
case ch == ')':
tokens = append(tokens, Token{RPAREN, ")"})
l.pos++
case ch == '2' && l.peek() == '>':
tokens = append(tokens, Token{REDIR_ERR, "2>"})
l.pos += 2
default:
tokens = append(tokens, Token{WORD, l.readWord()})
}
}
tokens = append(tokens, Token{EOF, ""})
return tokens
}
func (l *Lexer) readWord() string {
start := l.pos
for l.pos < len(l.input) {
ch := l.input[l.pos]
if unicode.IsSpace(ch) || strings.ContainsRune("|&;<>()", ch) {
break
}
if ch == '"' || ch == '\'' {
break
}
l.pos++
}
return string(l.input[start:l.pos])
}
func (l *Lexer) readQuoted(quote rune) string {
l.pos++ // 跳过开头引号
start := l.pos
for l.pos < len(l.input) && l.input[l.pos] != quote {
if l.input[l.pos] == '\\' {
l.pos++ // 跳过转义字符
}
l.pos++
}
val := string(l.input[start:l.pos])
if l.pos < len(l.input) {
l.pos++ // 跳过结尾引号
}
return val
}
func (l *Lexer) peek() rune {
if l.pos+1 < len(l.input) {
return l.input[l.pos+1]
}
return 0
}
第二步:语法分析器(命令树)
package parser
import "github.com/yourname/bashlit/lexer"
// Command 代表一个可执行的命令节点
type Command struct {
Args []string // 命令名和参数(展开后)
RedirIn string // < filename
RedirOut string // > filename
RedirApp string // >> filename
RedirErr string // 2> filename
Background bool // & 后台执行
Next *Command // ; 分隔的下一个命令
Pipe *Command // | 连接的下一个命令
And *Command // && 连接的下一个命令
Or *Command // || 连接的下一个命令
Subshell *Command // (cmd) 子 shell
}
type Parser struct {
tokens []lexer.Token
pos int
}
func New(tokens []lexer.Token) *Parser {
return &Parser{tokens: tokens}
}
func (p *Parser) Parse() *Command {
return p.parseList()
}
// parseList 解析分号分隔的命令列表
func (p *Parser) parseList() *Command {
cmd := p.parsePipeline()
if p.current().Type == lexer.SEMICOLON {
p.advance()
cmd.Next = p.parseList()
}
return cmd
}
// parsePipeline 解析管道
func (p *Parser) parsePipeline() *Command {
cmd := p.parseAndOr()
if p.current().Type == lexer.PIPE {
p.advance()
cmd.Pipe = p.parsePipeline()
}
return cmd
}
// parseAndOr 解析 && 和 || 逻辑连接
func (p *Parser) parseAndOr() *Command {
cmd := p.parseSimple()
switch p.current().Type {
case lexer.AND:
p.advance()
cmd.And = p.parseAndOr()
case lexer.OR:
p.advance()
cmd.Or = p.parseAndOr()
}
return cmd
}
// parseSimple 解析一个简单命令(含重定向)
func (p *Parser) parseSimple() *Command {
cmd := &Command{}
// 子 shell:(cmd)
if p.current().Type == lexer.LPAREN {
p.advance()
sub := p.parseList()
if p.current().Type == lexer.RPAREN {
p.advance()
}
cmd.Subshell = sub
return cmd
}
for {
tok := p.current()
switch tok.Type {
case lexer.WORD:
cmd.Args = append(cmd.Args, tok.Val)
p.advance()
case lexer.REDIR_IN:
p.advance()
cmd.RedirIn = p.current().Val
p.advance()
case lexer.REDIR_OUT:
p.advance()
cmd.RedirOut = p.current().Val
p.advance()
case lexer.REDIR_APP:
p.advance()
cmd.RedirApp = p.current().Val
p.advance()
case lexer.REDIR_ERR:
p.advance()
cmd.RedirErr = p.current().Val
p.advance()
case lexer.BACKGROUND:
cmd.Background = true
p.advance()
return cmd
default:
return cmd
}
}
}
func (p *Parser) current() lexer.Token {
if p.pos < len(p.tokens) {
return p.tokens[p.pos]
}
return lexer.Token{Type: lexer.EOF}
}
func (p *Parser) advance() {
p.pos++
}
第三步:执行器(管道和重定向)
package executor
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/yourname/bashlit/builtin"
"github.com/yourname/bashlit/parser"
)
type Executor struct {
env map[string]string
history []string
}
func New() *Executor {
return &Executor{env: make(map[string]string)}
}
// Execute 执行一个命令树
func (e *Executor) Execute(cmd *parser.Command) int {
if cmd == nil {
return 0
}
// 分号分隔:顺序执行
if cmd.Next != nil {
e.Execute(cmd)
return e.Execute(cmd.Next)
}
// 管道
if cmd.Pipe != nil {
return e.executePipeline(cmd)
}
// && 短路
if cmd.And != nil {
code := e.executeSingle(cmd)
if code == 0 {
return e.Execute(cmd.And)
}
return code
}
// || 短路
if cmd.Or != nil {
code := e.executeSingle(cmd)
if code != 0 {
return e.Execute(cmd.Or)
}
return code
}
return e.executeSingle(cmd)
}
// executePipeline 执行 cmd1 | cmd2 | ... | cmdN
func (e *Executor) executePipeline(first *parser.Command) int {
// 收集管道中所有命令
var cmds []*parser.Command
cur := first
for cur != nil {
cmds = append(cmds, cur)
cur = cur.Pipe
}
n := len(cmds)
procs := make([]*exec.Cmd, n)
pipes := make([]*os.File, n-1) // n-1 个管道的读端
// 创建所有管道
var writers []*os.File
for i := 0; i < n-1; i++ {
r, w, err := os.Pipe()
if err != nil {
fmt.Fprintf(os.Stderr, "pipe error: %v\n", err)
return 1
}
pipes[i] = r // 读端给下一个命令
writers = append(writers, w) // 写端给当前命令
}
// 设置每个命令的 stdin/stdout
for i, cmd := range cmds {
args := e.expandArgs(cmd.Args)
if len(args) == 0 {
continue
}
procs[i] = exec.Command(args[0], args[1:]...)
procs[i].Env = e.buildEnv()
// stdin
if i == 0 {
// 第一个命令:stdin 可能来自重定向
if cmd.RedirIn != "" {
f, err := os.Open(cmd.RedirIn)
if err != nil {
fmt.Fprintf(os.Stderr, "open %s: %v\n", cmd.RedirIn, err)
return 1
}
procs[i].Stdin = f
} else {
procs[i].Stdin = os.Stdin
}
} else {
procs[i].Stdin = pipes[i-1]
}
// stdout
if i == n-1 {
// 最后一个命令:stdout 可能重定向到文件
if out, err := e.openOutput(cmd); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
} else if out != nil {
procs[i].Stdout = out
} else {
procs[i].Stdout = os.Stdout
}
} else {
procs[i].Stdout = writers[i]
}
procs[i].Stderr = os.Stderr
}
// 启动所有进程
for i, proc := range procs {
if proc == nil {
continue
}
if err := proc.Start(); err != nil {
fmt.Fprintf(os.Stderr, "start %s: %v\n", cmds[i].Args[0], err)
return 1
}
}
// 关闭父进程持有的管道端(极其重要:否则子进程永远收不到 EOF)
for _, w := range writers {
w.Close()
}
for _, r := range pipes {
r.Close()
}
// 等待所有进程结束,返回最后一个命令的退出码
var lastCode int
for _, proc := range procs {
if proc == nil {
continue
}
if err := proc.Wait(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
lastCode = exitErr.ExitCode()
} else {
lastCode = 1
}
} else {
lastCode = 0
}
}
return lastCode
}
// executeSingle 执行单个命令(含 I/O 重定向)
func (e *Executor) executeSingle(cmd *parser.Command) int {
if cmd.Subshell != nil {
// 子 shell:在新进程中执行命令树
return e.Execute(cmd.Subshell)
}
if len(cmd.Args) == 0 {
return 0
}
args := e.expandArgs(cmd.Args)
// 检查内建命令
if code, handled := builtin.Run(args, e.env, &e.history); handled {
return code
}
// 外部命令
proc := exec.Command(args[0], args[1:]...)
proc.Env = e.buildEnv()
// 设置 stdin
if cmd.RedirIn != "" {
f, err := os.Open(cmd.RedirIn)
if err != nil {
fmt.Fprintf(os.Stderr, "open %s: %v\n", cmd.RedirIn, err)
return 1
}
defer f.Close()
proc.Stdin = f
} else {
proc.Stdin = os.Stdin
}
// 设置 stdout
if out, err := e.openOutput(cmd); err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
} else if out != nil {
defer out.Close()
proc.Stdout = out
} else {
proc.Stdout = os.Stdout
}
// 设置 stderr
if cmd.RedirErr != "" {
f, err := os.Create(cmd.RedirErr)
if err != nil {
fmt.Fprintf(os.Stderr, "create %s: %v\n", cmd.RedirErr, err)
return 1
}
defer f.Close()
proc.Stderr = f
} else {
proc.Stderr = os.Stderr
}
if cmd.Background {
if err := proc.Start(); err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", args[0], err)
return 1
}
fmt.Printf("[%d]\n", proc.Process.Pid)
return 0
}
if err := proc.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
return exitErr.ExitCode()
}
fmt.Fprintf(os.Stderr, "%s: command not found\n", args[0])
return 127
}
return 0
}
func (e *Executor) openOutput(cmd *parser.Command) (*os.File, error) {
if cmd.RedirOut != "" {
return os.Create(cmd.RedirOut)
}
if cmd.RedirApp != "" {
return os.OpenFile(cmd.RedirApp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
return nil, nil
}
// expandArgs 展开参数(环境变量、Glob)
func (e *Executor) expandArgs(args []string) []string {
var result []string
for _, arg := range args {
// 展开环境变量 $VAR 或 ${VAR}
expanded := os.Expand(arg, func(key string) string {
if v, ok := e.env[key]; ok {
return v
}
return os.Getenv(key)
})
// Glob 展开
if strings.ContainsAny(expanded, "*?[") {
matches, err := filepath.Glob(expanded)
if err == nil && len(matches) > 0 {
result = append(result, matches...)
continue
}
}
result = append(result, expanded)
}
return result
}
func (e *Executor) buildEnv() []string {
var env []string
for k, v := range e.env {
env = append(env, k+"="+v)
}
// 继承父进程的环境变量
env = append(env, os.Environ()...)
return env
}
第四步:内建命令
package builtin
import (
"fmt"
"os"
"strconv"
"strings"
)
// Run 如果 args[0] 是内建命令则执行并返回 (code, true),否则返回 (0, false)
func Run(args []string, env map[string]string, history *[]string) (int, bool) {
if len(args) == 0 {
return 0, false
}
switch args[0] {
case "cd":
return cd(args), true
case "exit":
return exit(args), true
case "export":
return export(args, env), true
case "unset":
return unset(args, env), true
case "echo":
return echo(args, env), true
case "history":
return printHistory(*history), true
case "pwd":
return pwd(), true
case "type":
return typeCmd(args), true
}
return 0, false
}
func cd(args []string) int {
dir := ""
if len(args) < 2 {
dir = os.Getenv("HOME")
if dir == "" {
fmt.Fprintln(os.Stderr, "cd: HOME not set")
return 1
}
} else {
dir = args[1]
}
if err := os.Chdir(dir); err != nil {
fmt.Fprintf(os.Stderr, "cd: %v\n", err)
return 1
}
return 0
}
func exit(args []string) int {
code := 0
if len(args) >= 2 {
if n, err := strconv.Atoi(args[1]); err == nil {
code = n
}
}
os.Exit(code)
return code
}
func export(args []string, env map[string]string) int {
// export VAR=value 或 export VAR(从 os.Environ() 获取当前值)
for _, arg := range args[1:] {
if idx := strings.Index(arg, "="); idx != -1 {
env[arg[:idx]] = arg[idx+1:]
} else {
if v := os.Getenv(arg); v != "" {
env[arg] = v
}
}
}
return 0
}
func unset(args []string, env map[string]string) int {
for _, key := range args[1:] {
delete(env, key)
os.Unsetenv(key)
}
return 0
}
func echo(args []string, env map[string]string) int {
parts := make([]string, 0, len(args)-1)
newline := true
for i, arg := range args[1:] {
if i == 0 && arg == "-n" {
newline = false
continue
}
// 展开 $VAR
expanded := os.Expand(arg, func(key string) string {
if v, ok := env[key]; ok {
return v
}
return os.Getenv(key)
})
parts = append(parts, expanded)
}
if newline {
fmt.Println(strings.Join(parts, " "))
} else {
fmt.Print(strings.Join(parts, " "))
}
return 0
}
func printHistory(history []string) int {
for i, cmd := range history {
fmt.Printf("%4d %s\n", i+1, cmd)
}
return 0
}
func pwd() int {
dir, err := os.Getwd()
if err != nil {
fmt.Fprintln(os.Stderr, err)
return 1
}
fmt.Println(dir)
return 0
}
func typeCmd(args []string) int {
builtins := map[string]bool{
"cd": true, "exit": true, "export": true, "echo": true,
"history": true, "pwd": true, "type": true, "unset": true,
}
for _, name := range args[1:] {
if builtins[name] {
fmt.Printf("%s is a shell builtin\n", name)
} else {
fmt.Printf("%s is an external command\n", name)
}
}
return 0
}
第五步:REPL 主循环(带历史和 Tab 补全)
package main
import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/chzyer/readline"
"github.com/yourname/bashlit/executor"
"github.com/yourname/bashlit/lexer"
"github.com/yourname/bashlit/parser"
)
func main() {
ex := executor.New()
// 使用 chzyer/readline 支持历史和 Tab 补全
rl, err := readline.NewEx(&readline.Config{
Prompt: "\033[32m$ \033[0m", // 绿色 $ 提示符
HistoryFile: os.Getenv("HOME") + "/.bashlit_history",
AutoComplete: readline.NewPrefixCompleter(buildCompleter()...),
InterruptPrompt: "^C",
EOFPrompt: "exit",
})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
defer rl.Close()
// 处理 Ctrl+C:不退出 shell,只中断当前输入
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT)
go func() {
for range sigCh {
// readline 内部处理了 Ctrl+C,这里只防止 shell 退出
}
}()
// REPL 主循环
var history []string
for {
// 动态更新提示符(显示当前目录)
dir, _ := os.Getwd()
home := os.Getenv("HOME")
if strings.HasPrefix(dir, home) {
dir = "~" + dir[len(home):]
}
rl.SetPrompt(fmt.Sprintf("\033[36m%s\033[0m \033[32m$\033[0m ", dir))
line, err := rl.Readline()
if err != nil { // io.EOF 或 Ctrl+D
fmt.Println("exit")
break
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
history = append(history, line)
// 词法分析
tokens := lexer.New(line).Tokenize()
// 语法分析
p := parser.New(tokens)
cmdTree := p.Parse()
// 执行
ex.Execute(cmdTree)
}
}
// buildCompleter 返回内建命令的 Tab 补全列表
func buildCompleter() []readline.PrefixCompleterInterface {
builtins := []string{"cd", "exit", "export", "echo", "history", "pwd", "type", "unset"}
var items []readline.PrefixCompleterInterface
for _, b := range builtins {
items = append(items, readline.PcItem(b))
}
return items
}
Level 4 · 进阶与边界
作业控制:fg/bg 和 Ctrl+Z
真正的 shell 支持作业控制:Ctrl+Z 挂起前台进程,bg 把它放到后台继续运行,fg 把后台进程拉回前台。
实现作业控制需要:
- 用
SysProcAttr.Setpgid = true让每个管道命令组成为独立进程组。 - 用
tcsetpgrp()把终端控制权从 shell 进程组切换到前台进程组。 - 用
SIGTTOU/SIGTTIN信号协调终端 I/O。 - 维护一个作业表(job table),记录每个后台作业的进程组 ID 和状态。
// 设置子进程的进程组
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
if err := cmd.Start(); err != nil {
return 1
}
pgid := cmd.Process.Pid
// 把终端控制权给子进程组(前台运行)
// tcsetpgrp 需要 cgo 或 syscall 直接调用
// syscall.Syscall(syscall.SYS_IOCTL, uintptr(ttyFd), syscall.TIOCSPGRP, uintptr(unsafe.Pointer(&pgid)))
// 等待子进程(同时处理 SIGCHLD)
var wstatus syscall.WaitStatus
syscall.Wait4(cmd.Process.Pid, &wstatus, 0, nil)
// 恢复终端控制权给 shell 进程组
shellPgid := syscall.Getpgrp()
// 再次 tcsetpgrp(shellPgid)
Heredoc 支持
Heredoc(Here Document)允许在命令行内嵌多行文本:
cat <<EOF
line one
line two
EOF
实现要点:识别 <<DELIMITER 语法,然后持续读取输入行直到遇到独占一行的 DELIMITER,把收集到的行通过 strings.NewReader 传入命令的 stdin。
func readHeredoc(delimiter string) string {
var lines []string
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
if line == delimiter {
break
}
lines = append(lines, line)
}
return strings.Join(lines, "\n")
}
环境变量展开的完整实现
完整的变量展开比 os.Expand 要复杂很多:
echo ${VAR:-default} # VAR 未设置时用 default
echo ${VAR:+set} # VAR 已设置时用 set
echo ${#VAR} # VAR 的长度
echo ${VAR%suffix} # 去掉最短匹配的后缀
echo ${VAR##prefix} # 去掉最长匹配的前缀
echo $? # 上一个命令的退出码
echo $$ # 当前 shell 的 PID
echo $! # 最近后台进程的 PID
这些特殊变量和展开语法需要自己实现,os.Expand 只处理简单的 $VAR 形式。
与 mvdan/sh 的对比
mvdan/sh 是 Go 实现的完整 POSIX shell 解析器和解释器,代码质量极高,可以作为学习参考。它的架构清晰地分为:
syntax包:词法+语法分析,支持 bash 扩展语法interp包:解释器,处理变量、管道、重定向、函数expand包:变量展开、命令替换、算术展开
和我们的 bash-lite 相比,mvdan/sh 有几十倍的代码量,处理了所有 POSIX 规范的细节。但核心思路是一致的:词法 → 语法 → AST → 遍历执行。
一旦你构建了 bash-lite 并理解了每一个机制,阅读 mvdan/sh 的源码会变得非常自然。你会发现自己的简化实现和工业级实现的每一处差异,都对应着真实的 shell 行为细节——而不是神秘的工程黑箱。
这就是"构建它以理解它"的价值:不只是会用,而是真正懂得它为何如此工作。