Add piping, Add type builtin, Add command -v builtin

This commit is contained in:
2026-08-23 13:30:22 +03:00
parent 35f5d5777e
commit 621be5a057
15 changed files with 421 additions and 56 deletions
+17 -16
View File
@@ -1,25 +1,24 @@
const std = @import("std");
const parser = @import("../shell/parser.zig");
const cd = @import("cmds/cd.zig");
const echo = @import("cmds/echo.zig");
const exit = @import("cmds/exit.zig");
const pwd = @import("cmds/pwd.zig");
const run = @import("cmds/run.zig");
const types = @import("types.zig");
const CommandFn = *const fn (types.BuiltinCommandContext) anyerror!void;
const CommandFn = *const fn (types.CommandContext) anyerror!void;
const commands = std.StaticStringMap(CommandFn).initComptime(.{
.{ "cd", cd.execute },
.{ "pwd", pwd.execute },
.{ "run", run.execute },
.{ "echo", echo.execute },
.{ "exit", exit.execute },
.{ "cd", @import("cmds/cd.zig").execute },
.{ "pwd", @import("cmds/pwd.zig").execute },
.{ "type", @import("cmds/type.zig").execute },
.{ "command", @import("cmds/command.zig").execute },
.{ "run", @import("cmds/run.zig").execute },
.{ "echo", @import("cmds/echo.zig").execute },
.{ "exit", @import("cmds/exit.zig").execute },
});
pub fn find(name: []const u8) ?CommandFn {
return commands.get(name);
}
pub fn execute(
io: std.Io,
allocator: std.mem.Allocator,
@@ -29,9 +28,7 @@ pub fn execute(
if (command.argv.len == 0)
return true;
const name = command.argv[0];
const execute_fn = commands.get(name) orelse
const execute_fn = commands.get(command.argv[0]) orelse
return false;
try execute_fn(.{
@@ -39,6 +36,10 @@ pub fn execute(
.allocator = allocator,
.shell = shell,
.argv = command.argv,
.stdin = std.Io.File.stdin(),
.stdout = std.Io.File.stdout(),
.stderr = std.Io.File.stderr(),
});
return true;
+1 -1
View File
@@ -2,7 +2,7 @@ const std = @import("std");
const types = @import("../types.zig");
pub fn execute(ctx: types.BuiltinCommandContext) !void {
pub fn execute(ctx: types.CommandContext) !void {
const path = if (ctx.argv.len >= 2)
ctx.argv[1]
else
+39
View File
@@ -0,0 +1,39 @@
const std = @import("std");
const resolver = @import("../resolver.zig");
const types = @import("../types.zig");
pub fn execute(ctx: types.CommandContext) !void {
if (ctx.argv.len < 2)
return;
if (!std.mem.eql(u8, ctx.argv[1], "-v"))
return;
if (ctx.argv.len < 3) {
std.debug.print("xsh: command: -v: missing command name\n", .{});
return;
}
const name = ctx.argv[2];
const result = try resolver.resolve(ctx, name) orelse {
return;
};
switch (result) {
.builtin => {
std.debug.print("{s}\n", .{name});
},
.alias => |value| {
std.debug.print("{s}\n", .{value});
},
.executable => |path| {
defer ctx.allocator.free(path);
std.debug.print("{s}\n", .{path});
},
}
}
+4 -4
View File
@@ -2,14 +2,14 @@ const std = @import("std");
const types = @import("../types.zig");
pub fn execute(ctx: types.BuiltinCommandContext) !void {
pub fn execute(ctx: types.CommandContext) !void {
for (ctx.argv[1..], 0..) |argument, index| {
if (index != 0) {
try std.Io.File.stdout().writeStreamingAll(ctx.io, " ");
try ctx.stdout.writeStreamingAll(ctx.io, " ");
}
try std.Io.File.stdout().writeStreamingAll(ctx.io, argument);
try ctx.stdout.writeStreamingAll(ctx.io, argument);
}
try std.Io.File.stdout().writeStreamingAll(ctx.io, "\n");
try ctx.stdout.writeStreamingAll(ctx.io, "\n");
}
+1 -1
View File
@@ -1,5 +1,5 @@
const types = @import("../types.zig");
pub fn execute(ctx: types.BuiltinCommandContext) !void {
pub fn execute(ctx: types.CommandContext) !void {
ctx.shell.running = false;
}
+3 -3
View File
@@ -2,10 +2,10 @@ const std = @import("std");
const types = @import("../types.zig");
pub fn execute(ctx: types.BuiltinCommandContext) !void {
pub fn execute(ctx: types.CommandContext) !void {
const path = try std.process.currentPathAlloc(ctx.io, ctx.allocator);
defer ctx.allocator.free(path);
try std.Io.File.stdout().writeStreamingAll(ctx.io, path);
try std.Io.File.stdout().writeStreamingAll(ctx.io, "\n");
try ctx.stdout.writeStreamingAll(ctx.io, path);
try ctx.stdout.writeStreamingAll(ctx.io, "\n");
}
+1 -1
View File
@@ -4,7 +4,7 @@ const script = @import("../../script/script.zig");
const types = @import("../types.zig");
pub fn execute(ctx: types.BuiltinCommandContext) !void {
pub fn execute(ctx: types.CommandContext) !void {
if (ctx.argv.len < 2) {
std.debug.print("xsh: run: missing script path\n", .{});
return;
+48
View File
@@ -0,0 +1,48 @@
const std = @import("std");
const resolver = @import("../resolver.zig");
const types = @import("../types.zig");
pub fn execute(ctx: types.CommandContext) !void {
if (ctx.argv.len < 2) {
std.debug.print("xsh: type: missing command name\n", .{});
return;
}
if (ctx.argv.len > 2) {
std.debug.print("xsh: type: too many arguments\n", .{});
return;
}
const name = ctx.argv[1];
const result = try resolver.resolve(
ctx,
name,
) orelse {
std.debug.print("xsh: type: {s}: not found\n", .{name});
return;
};
switch (result) {
.builtin => {
std.debug.print("{s} is a shell builtin\n", .{name});
},
.alias => |value| {
std.debug.print(
"{s} is aliased to '{s}'\n",
.{ name, value },
);
},
.executable => |path| {
defer ctx.allocator.free(path);
std.debug.print(
"{s} is {s}\n",
.{ name, path },
);
},
}
}
View File
+101
View File
@@ -0,0 +1,101 @@
const std = @import("std");
const builtin = @import("builtin.zig");
const types = @import("types.zig");
pub fn resolve(
ctx: types.CommandContext,
name: []const u8,
) !?types.ResolvedCommand {
if (builtin.find(name) != null)
return .builtin;
if (std.mem.indexOfAny(u8, name, "/\\") != null)
return .{
.executable = try ctx.allocator.dupe(u8, name),
};
const path = ctx.shell.environ.get("PATH") orelse
return null;
if (@import("builtin").os.tag == .windows) {
const pathext = ctx.shell.environ.get("PATHEXT") orelse
".COM;.EXE;.BAT;.CMD";
var dirs = std.mem.splitScalar(u8, path, ';');
while (dirs.next()) |dir| {
if (dir.len == 0)
continue;
// Try the exact name first.
{
const candidate = try std.fs.path.join(
ctx.allocator,
&.{ dir, name },
);
if (std.Io.Dir.accessAbsolute(
ctx.io,
candidate,
.{},
)) {
return .{ .executable = candidate };
} else |_| {
ctx.allocator.free(candidate);
}
}
// Then try PATHEXT.
var extensions = std.mem.splitScalar(u8, pathext, ';');
while (extensions.next()) |ext| {
const candidate_name = try std.fmt.allocPrint(
ctx.allocator,
"{s}{s}",
.{ name, ext },
);
defer ctx.allocator.free(candidate_name);
const candidate = try std.fs.path.join(
ctx.allocator,
&.{ dir, candidate_name },
);
if (std.Io.Dir.accessAbsolute(
ctx.io,
candidate,
.{},
)) {
return .{ .executable = candidate };
} else |_| {
ctx.allocator.free(candidate);
}
}
}
} else {
var dirs = std.mem.splitScalar(u8, path, ':');
while (dirs.next()) |dir| {
if (dir.len == 0)
continue;
const candidate = try std.fs.path.join(
ctx.allocator,
&.{ dir, name },
);
if (std.Io.Dir.accessAbsolute(
ctx.io,
candidate,
.{},
)) {
return .{ .executable = candidate };
} else |_| {
ctx.allocator.free(candidate);
}
}
}
return null;
}
+19 -1
View File
@@ -6,9 +6,27 @@ pub const Shell = struct {
environ: *const std.process.Environ.Map,
};
pub const BuiltinCommandContext = struct {
pub const CommandContext = struct {
io: std.Io,
allocator: std.mem.Allocator,
shell: *Shell,
argv: []const []const u8,
stdin: std.Io.File,
stdout: std.Io.File,
stderr: std.Io.File,
};
pub const ResolvedCommand = union(enum) {
builtin,
alias: []const u8,
executable: []const u8,
};
pub const Command = struct {
argv: []const []const u8,
};
pub const Pipeline = struct {
commands: []const Command,
};
+8 -3
View File
@@ -56,15 +56,20 @@ pub fn main(init: std.process.Init) !void {
}
}
fn runPipeline(allocator: std.mem.Allocator, io: std.Io, shell: *builtin_types.Shell, source: []const u8) !void {
fn runPipeline(
allocator: std.mem.Allocator,
io: std.Io,
shell: *builtin_types.Shell,
source: []const u8,
) !void {
const tokens = try lexer.lex(allocator, source);
const command = try parser.parse(allocator, tokens.items);
const pipeline = try parser.parse(allocator, tokens.items);
try execute.run(
io,
allocator,
shell,
command,
pipeline,
);
}
+82 -9
View File
@@ -9,17 +9,90 @@ pub fn run(
io: std.Io,
allocator: std.mem.Allocator,
shell: *builtin_types.Shell,
command: parser.Command,
pipeline: parser.Pipeline,
) !void {
if (try builtin.execute(io, allocator, shell, command))
if (pipeline.commands.len == 0)
return;
var child = try std.process.spawn(
io,
.{
.argv = command.argv,
},
);
if (pipeline.commands.len == 1) {
const command = pipeline.commands[0];
_ = try child.wait(io);
if (try builtin.execute(
io,
allocator,
shell,
command,
))
return;
var child = try std.process.spawn(io, .{
.argv = command.argv,
});
_ = try child.wait(io);
return;
}
if (pipeline.commands.len != 2)
return error.UnsupportedPipeline;
const left = pipeline.commands[0];
const right = pipeline.commands[1];
var left_child = try std.process.spawn(io, .{
.argv = left.argv,
.stdout = .pipe,
});
defer {
if (left_child.stdout) |stdout|
stdout.close(io);
}
var right_child = try std.process.spawn(io, .{
.argv = right.argv,
.stdin = .pipe,
});
defer {
if (right_child.stdin) |stdin|
stdin.close(io);
}
const left_stdout = left_child.stdout orelse
return error.MissingPipe;
const right_stdin = right_child.stdin orelse
return error.MissingPipe;
var buffer: [4096]u8 = undefined;
var buffers = [_][]u8{
&buffer,
};
while (true) {
const n = left_stdout.readStreaming(
io,
&buffers,
) catch |err| switch (err) {
error.EndOfStream => break,
else => return err,
};
if (n == 0)
break;
try right_stdin.writeStreamingAll(
io,
buffer[0..n],
);
}
right_stdin.close(io);
right_child.stdin = null;
left_stdout.close(io);
left_child.stdout = null;
_ = try left_child.wait(io);
_ = try right_child.wait(io);
}
+58 -7
View File
@@ -9,18 +9,69 @@ pub fn lex(
source: []const u8,
) !std.ArrayList(Token) {
var tokens: std.ArrayList(Token) = .empty;
var current: std.ArrayList(u8) = .empty;
var iterator = std.mem.tokenizeAny(
u8,
source,
" \t\r\n",
);
var quote: ?u8 = null;
while (iterator.next()) |word| {
for (source) |char| {
if (quote) |q| {
if (char == q) {
quote = null;
} else {
try current.append(allocator, char);
}
continue;
}
switch (char) {
'\'', '"' => {
quote = char;
},
' ', '\t', '\r', '\n' => {
if (current.items.len != 0) {
try tokens.append(
allocator,
.{
.text = try current.toOwnedSlice(allocator),
},
);
}
},
'|' => {
if (current.items.len != 0) {
try tokens.append(
allocator,
.{
.text = try current.toOwnedSlice(allocator),
},
);
}
try tokens.append(
allocator,
.{
.text = "|",
},
);
},
else => {
try current.append(allocator, char);
},
}
}
if (quote != null)
return error.UnterminatedQuote;
if (current.items.len != 0) {
try tokens.append(
allocator,
.{
.text = word,
.text = try current.toOwnedSlice(allocator),
},
);
}
+39 -10
View File
@@ -6,24 +6,53 @@ pub const Command = struct {
argv: []const []const u8,
};
pub const Pipeline = struct {
commands: []const Command,
};
pub fn parse(
allocator: std.mem.Allocator,
tokens: []const lexer.Token,
) !Command {
if (tokens.len == 0) {
) !Pipeline {
if (tokens.len == 0)
return error.EmptyCommand;
var commands: std.ArrayList(Command) = .empty;
var argv: std.ArrayList([]const u8) = .empty;
for (tokens) |token| {
if (std.mem.eql(u8, token.text, "|")) {
if (argv.items.len == 0)
return error.EmptyCommand;
try commands.append(
allocator,
.{
.argv = try argv.toOwnedSlice(allocator),
},
);
argv = .empty;
continue;
}
try argv.append(
allocator,
token.text,
);
}
const argv = try allocator.alloc(
[]const u8,
tokens.len,
if (argv.items.len == 0)
return error.EmptyCommand;
try commands.append(
allocator,
.{
.argv = try argv.toOwnedSlice(allocator),
},
);
for (tokens, 0..) |token, index| {
argv[index] = token.text;
}
return .{
.argv = argv,
.commands = try commands.toOwnedSlice(allocator),
};
}