os/shell: introduce ShellCommandBuilder (#9881)
This builder is an efficient way to construct space-separated shell command strings. We use it in setupBash to avoid using an intermediate array of arguments to construct our bash command line.pull/9885/head
commit
dd06d8a13b
|
|
@ -1,7 +1,84 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const Writer = std.Io.Writer;
|
||||
|
||||
/// Builder for constructing space-separated shell command strings.
|
||||
/// Uses a caller-provided allocator (typically with stackFallback).
|
||||
pub const ShellCommandBuilder = struct {
|
||||
buffer: std.Io.Writer.Allocating,
|
||||
|
||||
pub fn init(allocator: Allocator) ShellCommandBuilder {
|
||||
return .{ .buffer = .init(allocator) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ShellCommandBuilder) void {
|
||||
self.buffer.deinit();
|
||||
}
|
||||
|
||||
/// Append an argument to the command with automatic space separation.
|
||||
pub fn appendArg(self: *ShellCommandBuilder, arg: []const u8) (Allocator.Error || Writer.Error)!void {
|
||||
if (arg.len == 0) return;
|
||||
if (self.buffer.written().len > 0) {
|
||||
try self.buffer.writer.writeByte(' ');
|
||||
}
|
||||
try self.buffer.writer.writeAll(arg);
|
||||
}
|
||||
|
||||
/// Get the final null-terminated command string, transferring ownership to caller.
|
||||
/// Calling deinit() after this is safe but unnecessary.
|
||||
pub fn toOwnedSlice(self: *ShellCommandBuilder) Allocator.Error![:0]const u8 {
|
||||
return try self.buffer.toOwnedSliceSentinel(0);
|
||||
}
|
||||
};
|
||||
|
||||
test ShellCommandBuilder {
|
||||
// Empty command
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try testing.expectEqualStrings("", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Single arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Multiple args
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
try cmd.appendArg("-l");
|
||||
try testing.expectEqualStrings("bash --posix -l", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// Empty arg
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
defer cmd.deinit();
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("");
|
||||
try testing.expectEqualStrings("bash", cmd.buffer.written());
|
||||
}
|
||||
|
||||
// toOwnedSlice
|
||||
{
|
||||
var cmd = ShellCommandBuilder.init(testing.allocator);
|
||||
try cmd.appendArg("bash");
|
||||
try cmd.appendArg("--posix");
|
||||
const result = try cmd.toOwnedSlice();
|
||||
defer testing.allocator.free(result);
|
||||
try testing.expectEqualStrings("bash --posix", result);
|
||||
try testing.expectEqual(@as(u8, 0), result[result.len]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writer that escapes characters that shells treat specially to reduce the
|
||||
/// risk of injection attacks or other such weirdness. Specifically excludes
|
||||
/// linefeeds so that they can be used to delineate lists of file paths.
|
||||
|
|
|
|||
|
|
@ -259,8 +259,9 @@ fn setupBash(
|
|||
resource_dir: []const u8,
|
||||
env: *EnvMap,
|
||||
) !?config.Command {
|
||||
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 2);
|
||||
defer args.deinit(alloc);
|
||||
var stack_fallback = std.heap.stackFallback(4096, alloc);
|
||||
var cmd = internal_os.shell.ShellCommandBuilder.init(stack_fallback.get());
|
||||
defer cmd.deinit();
|
||||
|
||||
// Iterator that yields each argument in the original command line.
|
||||
// This will allocate once proportionate to the command line length.
|
||||
|
|
@ -269,9 +270,9 @@ fn setupBash(
|
|||
|
||||
// Start accumulating arguments with the executable and initial flags.
|
||||
if (iter.next()) |exe| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, exe));
|
||||
try cmd.appendArg(exe);
|
||||
} else return null;
|
||||
try args.append(alloc, "--posix");
|
||||
try cmd.appendArg("--posix");
|
||||
|
||||
// Stores the list of intercepted command line flags that will be passed
|
||||
// to our shell integration script: --norc --noprofile
|
||||
|
|
@ -304,17 +305,17 @@ fn setupBash(
|
|||
if (std.mem.indexOfScalar(u8, arg, 'c') != null) {
|
||||
return null;
|
||||
}
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
} else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) {
|
||||
// All remaining arguments should be passed directly to the shell
|
||||
// command. We shouldn't perform any further option processing.
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
while (iter.next()) |remaining_arg| {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, remaining_arg));
|
||||
try cmd.appendArg(remaining_arg);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
try args.append(alloc, try alloc.dupeZ(u8, arg));
|
||||
try cmd.appendArg(arg);
|
||||
}
|
||||
}
|
||||
try env.put("GHOSTTY_BASH_INJECT", buf[0..inject.end]);
|
||||
|
|
@ -352,8 +353,7 @@ fn setupBash(
|
|||
);
|
||||
try env.put("ENV", integ_dir);
|
||||
|
||||
// Join the accumulated arguments to form the final command string.
|
||||
return .{ .shell = try std.mem.joinZ(alloc, " ", args.items) };
|
||||
return .{ .shell = try cmd.toOwnedSlice() };
|
||||
}
|
||||
|
||||
test "bash" {
|
||||
|
|
|
|||
Loading…
Reference in New Issue