This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323const std = @import("std");const builtin = @import("builtin");const Allocator = std.mem.Allocator;const ArenaAllocator = std.heap.ArenaAllocator;const formatterpkg = @import("formatter.zig");
/// A command to execute (argv0 and args).////// A command is specified as a simple string such as "nvim a b c"./// By default, we expect the downstream to do some sort of shell expansion/// on this string.////// If a command is already expanded and the user does NOT want to do/// shell expansion (because this usually requires a round trip into/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g./// `direct:nvim a b c`.////// The whitespace before or around the prefix is ignored. For example,/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent.////// If the command is not absolute, it'll be looked up via the PATH./// For the shell-expansion case, we let the shell do this. For the/// direct case, we do this directly.pub const Command = union(enum) { const Self = @This();
/// Execute a command directly, e.g. via `exec`. The format here /// is already structured to be ready to passed directly to `exec` /// with index zero being the command to execute. /// /// Index zero is not guaranteed to be an absolute path, and may require /// PATH lookup. It is up to the downstream to do this, usually via /// delegation to something like `execvp`. direct: []const [:0]const u8,
/// Execute a command via shell expansion. This provides the command /// as a single string that is expected to be expanded in some way /// (up to the downstream). Usually `/bin/sh -c`. shell: [:0]const u8,
pub fn parseCLI( self: *Self, alloc: Allocator, input_: ?[]const u8, ) !void { // Input is required. Whitespace on the edges isn't needed. // Commands must be non-empty. const input = input_ orelse return error.ValueRequired; const trimmed = std.mem.trim(u8, input, " "); if (trimmed.len == 0) return error.ValueRequired;
// If we have a `:` then we MIGHT have a prefix to specify what // tag we should use. const tag: std.meta.Tag(Self), const str: []const u8 = tag: { if (std.mem.indexOfScalar(u8, trimmed, ':')) |idx| { const prefix = trimmed[0..idx]; if (std.mem.eql(u8, prefix, "direct")) { break :tag .{ .direct, trimmed[idx + 1 ..] }; } else if (std.mem.eql(u8, prefix, "shell")) { break :tag .{ .shell, trimmed[idx + 1 ..] }; } }
break :tag .{ .shell, trimmed }; };
switch (tag) { .shell => { // We have a shell command, so we can just dupe it. const copy = try alloc.dupeZ(u8, std.mem.trim(u8, str, " ")); self.* = .{ .shell = copy }; },
.direct => { // We're not shell expanding, so the arguments are naively // split on spaces. var builder: std.ArrayListUnmanaged([:0]const u8) = .empty; var args = std.mem.splitScalar( u8, std.mem.trim(u8, str, " "), ' ', ); while (args.next()) |arg| { const copy = try alloc.dupeZ(u8, arg); try builder.append(alloc, copy); }
self.* = .{ .direct = try builder.toOwnedSlice(alloc) }; }, } }
/// Creates a command as a single string, joining arguments as /// necessary with spaces. Its not guaranteed that this is a valid /// command; it is only meant to be human readable. pub fn string( self: *const Self, alloc: Allocator, ) Allocator.Error![:0]const u8 { return switch (self.*) { .shell => |v| try alloc.dupeZ(u8, v), .direct => |v| try std.mem.joinZ(alloc, " ", v), }; }
/// Get an iterator over the arguments array. This may allocate /// depending on the active tag of the command. /// /// For direct commands, this is very cheap and just iterates over /// the array. There is no allocation. /// /// For shell commands, this will use Zig's ArgIteratorGeneral as /// a best effort shell string parser. This is not guaranteed to be /// 100% accurate, but it works for common cases. This requires allocation. pub fn argIterator( self: *const Self, alloc: Allocator, ) Allocator.Error!ArgIterator { return switch (self.*) { .direct => |v| .{ .direct = .{ .args = v } }, .shell => |v| .{ .shell = try .init(alloc, v) }, }; }
/// Iterates over each argument in the command. pub const ArgIterator = union(enum) { shell: std.process.ArgIteratorGeneral(.{}), direct: struct { i: usize = 0, args: []const [:0]const u8, },
/// Return the next argument. This may or may not be a copy /// depending on the active tag. If you want to ensure that every /// argument is a copy, use the `clone` method first. pub fn next(self: *ArgIterator) ?[:0]const u8 { return switch (self.*) { .shell => |*v| v.next(), .direct => |*v| { if (v.i >= v.args.len) return null; defer v.i += 1; return v.args[v.i]; }, }; }
pub fn deinit(self: *ArgIterator) void { switch (self.*) { .shell => |*v| v.deinit(), .direct => {}, } } };
pub fn clone( self: *const Self, alloc: Allocator, ) Allocator.Error!Self { return switch (self.*) { .shell => |v| .{ .shell = try alloc.dupeZ(u8, v) }, .direct => |v| direct: { const copy = try alloc.alloc([:0]const u8, v.len); for (v, 0..) |arg, i| copy[i] = try alloc.dupeZ(u8, arg); break :direct .{ .direct = copy }; }, }; }
pub fn formatEntry(self: Self, formatter: anytype) !void { switch (self) { .shell => |v| try formatter.formatEntry([]const u8, v),
.direct => |v| { var buf: [4096]u8 = undefined; var fbs = std.io.fixedBufferStream(&buf); const writer = fbs.writer(); writer.writeAll("direct:") catch return error.OutOfMemory; for (v) |arg| { writer.writeAll(arg) catch return error.OutOfMemory; writer.writeByte(' ') catch return error.OutOfMemory; }
const written = fbs.getWritten(); try formatter.formatEntry( []const u8, written[0..@intCast(written.len - 1)], ); }, } }
test "Command: parseCLI errors" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var v: Self = undefined; try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); try testing.expectError(error.ValueRequired, v.parseCLI(alloc, " ")); }
test "Command: parseCLI shell expanded" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var v: Self = undefined; try v.parseCLI(alloc, "echo hello"); try testing.expect(v == .shell); try testing.expectEqualStrings(v.shell, "echo hello");
// Spaces are stripped try v.parseCLI(alloc, " echo hello "); try testing.expect(v == .shell); try testing.expectEqualStrings(v.shell, "echo hello"); }
test "Command: parseCLI direct" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var v: Self = undefined; try v.parseCLI(alloc, "direct:echo hello"); try testing.expect(v == .direct); try testing.expectEqual(v.direct.len, 2); try testing.expectEqualStrings(v.direct[0], "echo"); try testing.expectEqualStrings(v.direct[1], "hello");
// Spaces around the prefix try v.parseCLI(alloc, " direct: echo hello"); try testing.expect(v == .direct); try testing.expectEqual(v.direct.len, 2); try testing.expectEqualStrings(v.direct[0], "echo"); try testing.expectEqualStrings(v.direct[1], "hello"); }
test "Command: argIterator shell" { const testing = std.testing; const alloc = testing.allocator;
var v: Self = .{ .shell = "echo hello world" }; var it = try v.argIterator(alloc); defer it.deinit();
try testing.expectEqualStrings(it.next().?, "echo"); try testing.expectEqualStrings(it.next().?, "hello"); try testing.expectEqualStrings(it.next().?, "world"); try testing.expect(it.next() == null); }
test "Command: argIterator direct" { const testing = std.testing; const alloc = testing.allocator;
var v: Self = .{ .direct = &.{ "echo", "hello world" } }; var it = try v.argIterator(alloc); defer it.deinit();
try testing.expectEqualStrings(it.next().?, "echo"); try testing.expectEqualStrings(it.next().?, "hello world"); try testing.expect(it.next() == null); }
test "Command: string shell" { const testing = std.testing; const alloc = testing.allocator;
var v: Self = .{ .shell = "echo hello world" }; const str = try v.string(alloc); defer alloc.free(str); try testing.expectEqualStrings(str, "echo hello world"); }
test "Command: string direct" { const testing = std.testing; const alloc = testing.allocator;
var v: Self = .{ .direct = &.{ "echo", "hello world" } }; const str = try v.string(alloc); defer alloc.free(str); try testing.expectEqualStrings(str, "echo hello world"); }
test "Command: formatConfig shell" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var buf = std.ArrayList(u8).init(alloc); defer buf.deinit();
var v: Self = undefined; try v.parseCLI(alloc, "echo hello"); try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items); }
test "Command: formatConfig direct" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var buf = std.ArrayList(u8).init(alloc); defer buf.deinit();
var v: Self = undefined; try v.parseCLI(alloc, "direct: echo hello"); try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items); }};
test { _ = Command;}