用Zig写一个Shell 01:REPL与内建命令

Rycbartbad

Shell是一种命令语言解释器,它通过读取用户的输入或者加载脚本,调用相应的程序并打印输出。本系列将使用 Zig 0.16.0 从零开始实现一个运行在 Windows 环境下的 Shell。

命令提示符

所有的 Shell 都以一个命令提示符作为开头,表示就绪状态。在 Linux 或 Unix 系统上,你会经常看到 $ 或 #;在 Windows 系统上,则会经常看到 >,这些就是我们所说的命令提示符。在开始实现之前我们需要先对 stdin 和 stdout 进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
const std = @import("std");

pub fn main(init: std.process.Init) !void {
const io = init.io;

var stdin_buffer: [256]u8 = @splat(0);
var stdin_writer = std.Io.File.Reader.init(.stdin(), io, &stdin_buffer);
const stdin = &stdin_writer.interface;

var stdout_buffer: [256]u8 = @splat(0);
var stdout_writer = std.Io.File.Writer.init(.stdout(), io, &stdout_buffer);
const stdout = &stdout_writer;
}

现在我们有了标准输入和标准输出,于是可以创建 REPL(“读取-求值-打印”循环),目前先跳过求值与打印部分,只输出命令提示词和接收用户输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub fn main(init: std.process.Init) !void {
// ...

while(true) {
try stdout.print("> ", .{});
try stdout.flush(); // Zig 需要手动刷新

// takeDelimiter()会返回到分隔符前的切片,需要去除 Windows 输入末尾包含的 '\r'
const input = if (try stdin.takeDelimiter('\n')) |origin|
if (origin.len > 0 and origin[origin.len - 1] == '\r')
origin[0 .. origin.len - 1]
else
origin
else
"";
_ = input; // 暂时不处理用户的输入
}
}
1
2
PS > zig build run
> 111

非法命令处理

非法命令通常指系统无法识别或执行的命令,通常由拼写错误,权限不足或命令不存在导致。当前代码中我们没有实现任何命令,所以不管用户输入什么都是非法的。当 Shell 检测到非法命令时,会提示 “xxx: command not found”。我们在之前的代码中已经从 stdin 读取到了用户输入,接下来只需将用户输入打印出来即可。

1
2
3
// _ = input;
try stdout.print("{s}: command not found\n", .{input});
try stdout.flush();

虽然只是 “command not found”,但是至少现在我们的 Shell 有了反馈。 :D

1
2
3
PS > zig build run
> hello
hello: command not found

内建命令

有一些命令需要改变 Shell 自身的状态,如:当前目录、退出状态,这些命令不能由子进程执行。所以我们需要在代码中写好这些内建命令。本章先实现一些简单的内建命令。

1
2
3
4
5
6
const Builtin = enum {
exit,
echo,
type,
};

我们需要读取 input 中的各个参数,使用 tokenizeScalar() 可以快速进行文本分割,它返回一个迭代器,可以使用 next() 返回当前 token 并前进一步。stringToEnum() 能将字符串转换为枚举类型。

1
2
3
4
5
6
7
8
var arg_iter = std.mem.tokenizeScalar(u8, input, ' ');
const command_str = arg_iter.next().?;
const command = std.meta.stringToEnum(Builtin, command_str);
if(command) |builtin_cmd|{
// ...
} else {
try stdout.print("{s}: command not found\n", .{command_str});
}

exit

当 Shell 接收到 exit 命令时,它应该立即结束进程。同时,exit 接收一个参数,用于指定退出状态(0~255)。

1
2
3
4
5
6
7
8
9
if(command) |builtin_cmd|{
switch(builtin_cmd){
.exit => {
const status = std.fmt.parseInt(u8, arg_iter.next() orelse "0", 10) catch 1;
std.process.exit(status);
},
// ...
}
}

echo

echo 是一个日常使用非常广泛的命令,它将文本或字符串打印到标准输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
if(command) |builtin_cmd|{
switch(builtin_cmd){
.echo => {
while (arg_iter.next()) |arg| {
try stdout.print("{s}", .{arg});
if (arg_iter.peek() != null) {
try stdout.print(" ", .{});
} else try stdout.print("\n", .{});
}
},
// ...
}
}

type

type 命令可以提示用户命令的类型,比如该命令是内建命令还是外部程序。当然,目前的命令都是内建命令,不过我们会在后续加入外部程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(command) |builtin_cmd|{
switch(builtin_cmd){
.type => {
const type_str = arg_iter.next() orelse "";
const type_enum = std.meta.stringToEnum(Builtin, type_str);
if (type_enum) |_| {
try stdout.print("{s} is a shell builtin\n", .{type_str});
} else {
try stdout.print("{s}: not found\n", .{type_str});
}
},
// ...
}
}

重新构建之后,我们终于可以正常使用 Shell 了。

1
2
3
4
5
6
PS > zig build run
> echo hello world
hello world
> type echo
echo is a shell builtin
> exit

至此,我们完成了 Shell 的核心 REPL,以及部分内建命令,不过此时的Shell还无法运行任何外部程序,如:ls,cat。并且目前的错误信息都是直接打印到 stdout,后续我们将完善这个初具雏形的 Shell,并进行更加规范化的处理。

Comments
On this page
用Zig写一个Shell 01:REPL与内建命令