From 621be5a0576ee4ea851a5cd2aed69c62a1d72b1e Mon Sep 17 00:00:00 2001 From: drvxor Date: Sun, 23 Aug 2026 13:30:22 +0300 Subject: [PATCH] Add piping, Add type builtin, Add command -v builtin --- src/builtin/builtin.zig | 33 ++++++------ src/builtin/cmds/cd.zig | 2 +- src/builtin/cmds/command.zig | 39 ++++++++++++++ src/builtin/cmds/echo.zig | 8 +-- src/builtin/cmds/exit.zig | 2 +- src/builtin/cmds/pwd.zig | 6 +-- src/builtin/cmds/run.zig | 2 +- src/builtin/cmds/type.zig | 48 +++++++++++++++++ src/builtin/cmds/which.zig | 0 src/builtin/resolver.zig | 101 +++++++++++++++++++++++++++++++++++ src/builtin/types.zig | 20 ++++++- src/main.zig | 11 ++-- src/shell/execute.zig | 91 +++++++++++++++++++++++++++---- src/shell/lexer.zig | 65 +++++++++++++++++++--- src/shell/parser.zig | 49 +++++++++++++---- 15 files changed, 421 insertions(+), 56 deletions(-) create mode 100644 src/builtin/cmds/command.zig create mode 100644 src/builtin/cmds/type.zig delete mode 100644 src/builtin/cmds/which.zig create mode 100644 src/builtin/resolver.zig diff --git a/src/builtin/builtin.zig b/src/builtin/builtin.zig index 60bbb20..191250b 100644 --- a/src/builtin/builtin.zig +++ b/src/builtin/builtin.zig @@ -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; diff --git a/src/builtin/cmds/cd.zig b/src/builtin/cmds/cd.zig index 4e204b3..58a8ddf 100644 --- a/src/builtin/cmds/cd.zig +++ b/src/builtin/cmds/cd.zig @@ -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 diff --git a/src/builtin/cmds/command.zig b/src/builtin/cmds/command.zig new file mode 100644 index 0000000..833a33a --- /dev/null +++ b/src/builtin/cmds/command.zig @@ -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}); + }, + } +} diff --git a/src/builtin/cmds/echo.zig b/src/builtin/cmds/echo.zig index 79e6c38..7014a1e 100644 --- a/src/builtin/cmds/echo.zig +++ b/src/builtin/cmds/echo.zig @@ -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"); } diff --git a/src/builtin/cmds/exit.zig b/src/builtin/cmds/exit.zig index d4d9cfc..dfb43b8 100644 --- a/src/builtin/cmds/exit.zig +++ b/src/builtin/cmds/exit.zig @@ -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; } diff --git a/src/builtin/cmds/pwd.zig b/src/builtin/cmds/pwd.zig index a4374b1..5be0efa 100644 --- a/src/builtin/cmds/pwd.zig +++ b/src/builtin/cmds/pwd.zig @@ -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"); } diff --git a/src/builtin/cmds/run.zig b/src/builtin/cmds/run.zig index 3d50384..206ba7e 100644 --- a/src/builtin/cmds/run.zig +++ b/src/builtin/cmds/run.zig @@ -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; diff --git a/src/builtin/cmds/type.zig b/src/builtin/cmds/type.zig new file mode 100644 index 0000000..acbae33 --- /dev/null +++ b/src/builtin/cmds/type.zig @@ -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 }, + ); + }, + } +} diff --git a/src/builtin/cmds/which.zig b/src/builtin/cmds/which.zig deleted file mode 100644 index e69de29..0000000 diff --git a/src/builtin/resolver.zig b/src/builtin/resolver.zig new file mode 100644 index 0000000..e847d60 --- /dev/null +++ b/src/builtin/resolver.zig @@ -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; +} diff --git a/src/builtin/types.zig b/src/builtin/types.zig index 0509cec..c3feb4d 100644 --- a/src/builtin/types.zig +++ b/src/builtin/types.zig @@ -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, }; diff --git a/src/main.zig b/src/main.zig index 0de0485..6e9f413 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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, ); } diff --git a/src/shell/execute.zig b/src/shell/execute.zig index 55b10bc..4451f65 100644 --- a/src/shell/execute.zig +++ b/src/shell/execute.zig @@ -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); } diff --git a/src/shell/lexer.zig b/src/shell/lexer.zig index ddd4e91..81e8232 100644 --- a/src/shell/lexer.zig +++ b/src/shell/lexer.zig @@ -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), }, ); } diff --git a/src/shell/parser.zig b/src/shell/parser.zig index f43423a..f924462 100644 --- a/src/shell/parser.zig +++ b/src/shell/parser.zig @@ -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), }; }