Add piping, Add type builtin, Add command -v builtin
This commit is contained in:
+17
-16
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+79
-6
@@ -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(
|
||||
if (pipeline.commands.len == 1) {
|
||||
const command = pipeline.commands[0];
|
||||
|
||||
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
@@ -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 = word,
|
||||
.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 = try current.toOwnedSlice(allocator),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+38
-9
@@ -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;
|
||||
}
|
||||
|
||||
const argv = try allocator.alloc(
|
||||
[]const u8,
|
||||
tokens.len,
|
||||
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),
|
||||
},
|
||||
);
|
||||
|
||||
for (tokens, 0..) |token, index| {
|
||||
argv[index] = token.text;
|
||||
argv = .empty;
|
||||
continue;
|
||||
}
|
||||
|
||||
try argv.append(
|
||||
allocator,
|
||||
token.text,
|
||||
);
|
||||
}
|
||||
|
||||
if (argv.items.len == 0)
|
||||
return error.EmptyCommand;
|
||||
|
||||
try commands.append(
|
||||
allocator,
|
||||
.{
|
||||
.argv = try argv.toOwnedSlice(allocator),
|
||||
},
|
||||
);
|
||||
|
||||
return .{
|
||||
.argv = argv,
|
||||
.commands = try commands.toOwnedSlice(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user