aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/libghostty-vt/src/config/command.zig
blob: b48bee189573cfb40f510b02c776662c699df757 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
const std = @import("std");
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.Args.IteratorGeneral(.{}),
        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 deinit(self: *const Self, alloc: Allocator) void {
        switch (self.*) {
            .shell => |v| alloc.free(v),
            .direct => |l| {
                for (l) |v| alloc.free(v);
                alloc.free(l);
            },
        }
    }

    pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void {
        switch (self) {
            .shell => |v| try formatter.formatEntry([]const u8, v),

            .direct => |v| {
                var buf: [4096]u8 = undefined;
                var writer: std.Io.Writer = .fixed(&buf);
                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 = writer.buffered();
                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.Io.Writer.Allocating = .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.written());
    }

    test "Command: formatConfig direct" {
        const testing = std.testing;
        var arena = ArenaAllocator.init(testing.allocator);
        defer arena.deinit();
        const alloc = arena.allocator();

        var buf: std.Io.Writer.Allocating = .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.written());
    }
};

test {
    _ = Command;
}