用Zig写一个Shell 01:REPL与内建命令
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();
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
| 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,并进行更加规范化的处理。