Browse Source

Re-commiting whole course squashed into one commit

The history is non-sensical and messy (including every dumb typo)
so I squashed it all to avoid the rabbit hole of thinking how to split
it in a meaningful way.
Alois Mahdal 5 days ago
parent
commit
a4fd321347
11 changed files with 2353 additions and 10 deletions
  1. 2
    0
      .gitignore
  2. 1
    1
      codecrafters.yml
  3. 323
    0
      src/bltn.zig
  4. 117
    0
      src/cmd.zig
  5. 12
    8
      src/main.zig
  6. 110
    0
      src/pedantic.zig
  7. 311
    0
      src/shell.zig
  8. 1196
    0
      src/syntax.zig
  9. 155
    0
      src/util.zig
  10. 125
    0
      utils/tgen.sh
  11. 1
    1
      your_program.sh

+ 2
- 0
.gitignore View File

@@ -16,3 +16,5 @@ bin/
16 16
 **/.DS_Store
17 17
 *.swp
18 18
 *~
19
+
20
+your_program.strace

+ 1
- 1
codecrafters.yml View File

@@ -2,7 +2,7 @@
2 2
 #
3 3
 # These can be VERY verbose, so we suggest turning them off
4 4
 # unless you really need them.
5
-debug: false
5
+debug: true
6 6
 
7 7
 # Use this to change the Zig version used to run your code
8 8
 # on Codecrafters.

+ 323
- 0
src/bltn.zig View File

@@ -0,0 +1,323 @@
1
+const std = @import("std");
2
+
3
+const shell = @import("shell.zig");
4
+const util = @import("util.zig");
5
+const cmd = @import("cmd.zig");
6
+
7
+const StdIO = shell.StdIO;
8
+const Loop = shell.Loop;
9
+const Shell = shell.Shell;
10
+
11
+const PEDANTIC = false;
12
+
13
+pub const BltnCtx = struct {
14
+    loop: *Loop,
15
+    cmd_name: []const u8,
16
+    args: []const []const u8,
17
+    allocator: std.mem.Allocator,
18
+    shell: *Shell,
19
+    stdio: StdIO,
20
+
21
+    fn get_sopt(self: @This(), idx: usize) ?u8 {
22
+        if (self.args.len < 1) return null;
23
+        if (idx > self.args.len - 1) return null;
24
+        const candidate = self.args[idx];
25
+        if (candidate.len != 2) return null;
26
+        if (candidate[0] != '-') return null;
27
+        return candidate[1];
28
+    }
29
+
30
+    fn clone_arg(self: @This(), idx: usize) ![]const u8 {
31
+        std.debug.assert(idx < self.args.len);
32
+        return try self.allocator.dupe(u8, self.args[idx]);
33
+    }
34
+
35
+    fn clone_args(self: @This()) ![]const []const u8 {
36
+        var al = std.ArrayList([]u8).init(self.allocator);
37
+        for (self.args) |arg| try al.append(try self.allocator.dupe(u8, arg));
38
+        return try al.toOwnedSlice();
39
+    }
40
+
41
+    fn usage_unknown(
42
+        self: @This(),
43
+        comptime B: type,
44
+        arg: []const u8,
45
+    ) error{UsageError} {
46
+        self.stdio.err(B.USAGE_TEXT);
47
+        self.stdio.err("\n\n");
48
+        self.stdio.errf("unknown argument: '{s}'", .{arg});
49
+        return error.UsageError;
50
+    }
51
+
52
+    fn usage_missing(
53
+        self: @This(),
54
+        comptime B: type,
55
+        comptime label: []const u8,
56
+    ) error{UsageError} {
57
+        self.stdio.err(B.USAGE_TEXT);
58
+        self.stdio.err("\n\n");
59
+        self.stdio.err("no " ++ label ++ "?");
60
+        return error.UsageError;
61
+    }
62
+
63
+    fn usage_toomany(
64
+        self: @This(),
65
+        comptime B: type,
66
+    ) error{UsageError} {
67
+        self.stdio.err(B.USAGE_TEXT);
68
+        self.stdio.err("\n\n");
69
+        self.stdio.err("too many arguments");
70
+        return error.UsageError;
71
+    }
72
+
73
+    fn usage(
74
+        self: @This(),
75
+        comptime B: type,
76
+        comptime fmt: []const u8,
77
+        args: anytype,
78
+    ) error{UsageError} {
79
+        self.stdio.err(B.USAGE_TEXT);
80
+        self.stdio.err("\n\n");
81
+        self.stdio.errf(fmt, args);
82
+        return error.UsageError;
83
+    }
84
+};
85
+
86
+const ExitBltn = struct {
87
+    const Self = @This();
88
+    const Params = struct {
89
+        es: u7,
90
+    };
91
+
92
+    const USAGE_TEXT =
93
+        \\usage: exit [N]
94
+    ;
95
+
96
+    fn _parse(ctx: BltnCtx) !Params {
97
+        if (ctx.args.len == 0) return Params{ .es = shell.EXIT_OK };
98
+        if (ctx.args.len > 1) return ctx.usage_toomany(Self);
99
+        const es_str = ctx.args[0];
100
+        const es = std.fmt.parseInt(u7, es_str, 10) catch {
101
+            return ctx.usage(
102
+                Self,
103
+                "N must be a base-10 integer between 0 and 127: '{s}'",
104
+                .{es_str},
105
+            );
106
+        };
107
+        return Params{ .es = es };
108
+    }
109
+
110
+    fn run(ctx: BltnCtx) !void {
111
+        const params: Params = try Self._parse(ctx);
112
+        ctx.shell.exit(params.es);
113
+    }
114
+};
115
+
116
+const EchoBltn = struct {
117
+    const Self = @This();
118
+    const Params = struct {
119
+        operands: []const []const u8,
120
+    };
121
+
122
+    const USAGE_TEXT =
123
+        \\usage: echo [STRING..]
124
+    ;
125
+
126
+    fn _parse(ctx: BltnCtx) !Params {
127
+        return Params{ .operands = try ctx.clone_args() };
128
+    }
129
+
130
+    fn run(ctx: BltnCtx) !void {
131
+        const params: Params = try Self._parse(ctx);
132
+        const w = ctx.stdio.stdout_f.writer().any();
133
+        for (params.operands, 0..) |operand, idx| {
134
+            if (idx > 0) try w.writeByte(' ');
135
+            try w.writeAll(operand);
136
+        }
137
+        try w.writeByte('\n');
138
+    }
139
+};
140
+
141
+const TypeBltn = struct {
142
+    const Self = @This();
143
+    const Params = struct {
144
+        cmd_name: []const u8,
145
+    };
146
+
147
+    const USAGE_TEXT =
148
+        \\usage: type COMMAND_NAME
149
+    ;
150
+
151
+    fn _parse(ctx: BltnCtx) !Params {
152
+        if (ctx.args.len == 0) return ctx.usage_missing(Self, "COMMAND_NAME");
153
+        if (ctx.args.len > 1) return ctx.usage_toomany(Self);
154
+        const cmd_name = try ctx.clone_arg(0);
155
+        if (std.mem.indexOfAny(u8, cmd_name, shell.CMD_NAME_INVALID)) |pos| return ctx.usage(
156
+            Self,
157
+            "COMMAND_NAME contains invalid character: {x} at position {d}",
158
+            .{ cmd_name[pos], pos },
159
+        );
160
+        return Params{ .cmd_name = cmd_name };
161
+    }
162
+
163
+    fn run(ctx: BltnCtx) !void {
164
+        const params = try Self._parse(ctx);
165
+        if (Bltn.exists(params.cmd_name)) {
166
+            try ctx.stdio.outf("{s} is a shell builtin", .{params.cmd_name});
167
+        } else if (try cmd.find_exec_path(params.cmd_name, ctx.allocator)) |exec_path| {
168
+            try ctx.stdio.outf("{s} is {s}", .{ params.cmd_name, exec_path });
169
+        } else {
170
+            try ctx.stdio.outf("{s}: not found", .{params.cmd_name});
171
+        }
172
+    }
173
+};
174
+
175
+const PwdBltn = struct {
176
+    const Self = @This();
177
+    const Params = struct {
178
+        mode: Mode,
179
+    };
180
+    const Mode = enum { L, P };
181
+    const USAGE_TEXT =
182
+        \\usage: pwd [-L|-P]
183
+    ;
184
+
185
+    fn _parse(ctx: BltnCtx) !Params {
186
+        var cur: usize = 0;
187
+        var mode: Mode = .L;
188
+        while (cur < ctx.args.len) {
189
+            const arg = ctx.args[cur];
190
+            const maybe_opt_letter = ctx.get_sopt(cur);
191
+            if (maybe_opt_letter == null) return ctx.usage_unknown(Self, arg);
192
+            switch (maybe_opt_letter.?) {
193
+                'L' => mode = .L,
194
+                'P' => mode = .P,
195
+                else => return ctx.usage_unknown(Self, arg),
196
+            }
197
+            cur += 1;
198
+        }
199
+        return Params{ .mode = mode };
200
+    }
201
+
202
+    fn run(ctx: BltnCtx) !void {
203
+        const params = try Self._parse(ctx);
204
+        const out = b: {
205
+            if (PEDANTIC) {
206
+                const PwdComputer = @import("pedantic.zig").PwdComputer;
207
+                const pc = try PwdComputer.create(ctx.allocator);
208
+                defer pc.destroy();
209
+                switch (params.mode) {
210
+                    .P => break :b pc.getpath_P(),
211
+                    .L => break :b pc.getpath_L(),
212
+                }
213
+            } else {
214
+                break :b try std.process.getCwdAlloc(ctx.allocator);
215
+            }
216
+        };
217
+        try ctx.stdio.out(out);
218
+    }
219
+};
220
+
221
+const CdBltn = struct {
222
+    const USAGE_TEXT =
223
+        \\usage: cd PATH
224
+    ;
225
+    const Self = @This();
226
+    const Target = union(enum) {
227
+        path: []const u8,
228
+        home: void,
229
+        old: void,
230
+    };
231
+
232
+    fn _parse_target(ctx: BltnCtx) !Target {
233
+        if (ctx.args.len == 0) return Target{ .home = {} };
234
+        if (ctx.args.len > 1) return ctx.usage_toomany(Self);
235
+        if (ctx.args[0].len == 1 and ctx.args[0][0] == '-') return Target{ .old = {} };
236
+        return Target{ .path = try ctx.clone_arg(0) };
237
+    }
238
+
239
+    fn _err2msg(err: anyerror) ?[]const u8 {
240
+        return (switch (err) {
241
+            error.AccessDenied => "Access denied",
242
+            error.FileNotFound => "No such file or directory",
243
+            error.NotDir => "Not a directory",
244
+            error.BadPathName => "Invalid path name",
245
+            else => null,
246
+        });
247
+    }
248
+
249
+    fn _chdir(ctx: BltnCtx, path: []const u8) !void {
250
+        const initial_path = try std.process.getCwdAlloc(ctx.allocator);
251
+        defer ctx.allocator.free(initial_path);
252
+        const untilded = try util.realpathWithTilde(ctx.allocator, path);
253
+        try std.process.changeCurDir(untilded);
254
+        try ctx.shell.setOldPwd(initial_path);
255
+    }
256
+
257
+    fn run(ctx: BltnCtx) !void {
258
+        const target = try Self._parse_target(ctx);
259
+        const path = switch (target) {
260
+            .path => |path| path,
261
+            .home => try std.process.getEnvVarOwned(ctx.allocator, "HOME"),
262
+            .old => ctx.shell.vars.OLDPWD orelse {
263
+                ctx.stdio.err("cd: OLDPWD not set");
264
+                return;
265
+            },
266
+        };
267
+        Self._chdir(ctx, path) catch |e| {
268
+            const msg = Self._err2msg(e) orelse return e;
269
+            ctx.stdio.errf("cd: {s}: {s}", .{ path, msg });
270
+        };
271
+    }
272
+};
273
+
274
+const _VarsBltn = struct {
275
+    const USAGE_TEXT =
276
+        \\usage: _vars
277
+    ;
278
+    const Self = @This();
279
+    const Params = struct {};
280
+
281
+    fn run(ctx: BltnCtx) !void {
282
+        if (ctx.args.len > 0) return ctx.usage_toomany(Self);
283
+        if (ctx.shell.vars.OLDPWD) |path| ctx.stdio.errf("OLDPWD={s}", .{path});
284
+        ctx.stdio.errf("ES={d}", .{ctx.shell.vars.ES});
285
+    }
286
+};
287
+
288
+const Bltn = enum {
289
+    exit,
290
+    echo,
291
+    type,
292
+    pwd,
293
+    cd,
294
+    _vars,
295
+
296
+    fn exists(cmd_name: []const u8) bool {
297
+        if (Bltn.select(cmd_name)) |_| return true;
298
+        return false;
299
+    }
300
+
301
+    fn select(cmd_name: []const u8) ?Bltn {
302
+        if (std.mem.eql(u8, cmd_name, "exit")) return .exit;
303
+        if (std.mem.eql(u8, cmd_name, "echo")) return .echo;
304
+        if (std.mem.eql(u8, cmd_name, "type")) return .type;
305
+        if (std.mem.eql(u8, cmd_name, "pwd")) return .pwd;
306
+        if (std.mem.eql(u8, cmd_name, "cd")) return .cd;
307
+        if (std.mem.eql(u8, cmd_name, "_vars")) return ._vars;
308
+        return null;
309
+    }
310
+};
311
+
312
+pub fn run_bltn(ctx: BltnCtx) !void {
313
+    const bltn = Bltn.select(ctx.cmd_name) orelse return error.NoSuchBltn;
314
+    std.log.debug("running bltn: {any}", .{bltn});
315
+    return switch (bltn) {
316
+        .exit => ExitBltn.run(ctx),
317
+        .echo => EchoBltn.run(ctx),
318
+        .type => TypeBltn.run(ctx),
319
+        .pwd => PwdBltn.run(ctx),
320
+        .cd => CdBltn.run(ctx),
321
+        ._vars => _VarsBltn.run(ctx),
322
+    };
323
+}

+ 117
- 0
src/cmd.zig View File

@@ -0,0 +1,117 @@
1
+const std = @import("std");
2
+
3
+const util = @import("util.zig");
4
+const shell_ = @import("shell.zig");
5
+
6
+const Shell = shell_.Shell;
7
+const StdIO = shell_.StdIO;
8
+const Files = shell_.StdIO.Files;
9
+const Term = std.process.Child.Term;
10
+const CMD_NAME_INVALID = shell_.CMD_NAME_INVALID;
11
+const EXEC_PATH: enum { resolve, keep } = .keep;
12
+
13
+fn resolve_exec_path(
14
+    cmd_name: []const u8,
15
+    dir_entry: []const u8,
16
+    allocator: std.mem.Allocator,
17
+) ![]u8 {
18
+    const dir_path = try util.realpathWithTilde(allocator, dir_entry);
19
+    defer allocator.free(dir_path);
20
+    const dir_fd = try std.fs.openDirAbsolute(dir_path, .{});
21
+    const stat = try dir_fd.statFile(cmd_name);
22
+    if (stat.kind != .file) return error.NotAFile;
23
+    const file_path = try std.fs.path.join(allocator, &.{ dir_path, cmd_name });
24
+    errdefer allocator.free(file_path);
25
+    try std.posix.access(file_path, std.posix.X_OK);
26
+    return file_path;
27
+}
28
+
29
+pub fn find_exec_path(
30
+    cmd_name: []const u8,
31
+    allocator: std.mem.Allocator,
32
+) !?[]u8 {
33
+    std.debug.assert(std.mem.indexOfAny(u8, cmd_name, CMD_NAME_INVALID) == null);
34
+    const entrylist = try std.process.getEnvVarOwned(allocator, "PATH");
35
+    defer allocator.free(entrylist);
36
+    var iter = std.mem.tokenizeScalar(u8, entrylist, ':');
37
+    while (iter.next()) |entry| {
38
+        const file_path = resolve_exec_path(cmd_name, entry, allocator) catch continue;
39
+        return file_path;
40
+    }
41
+    return null;
42
+}
43
+
44
+/// stolen from std.process.Child
45
+fn statusToTerm(status: u32) Term {
46
+    return if (std.posix.W.IFEXITED(status))
47
+        Term{ .Exited = std.posix.W.EXITSTATUS(status) }
48
+    else if (std.posix.W.IFSIGNALED(status))
49
+        Term{ .Signal = std.posix.W.TERMSIG(status) }
50
+    else if (std.posix.W.IFSTOPPED(status))
51
+        Term{ .Stopped = std.posix.W.STOPSIG(status) }
52
+    else
53
+        Term{ .Unknown = status };
54
+}
55
+
56
+pub fn run_command(
57
+    argv: []const []const u8,
58
+    files: Files,
59
+    allocator: std.mem.Allocator,
60
+) !u8 {
61
+    if (argv.len == 0) return error.NoArgv;
62
+
63
+    // copy env
64
+    const env = try std.process.createEnvironFromExisting(
65
+        allocator,
66
+        @ptrCast(std.os.environ.ptr),
67
+        .{},
68
+    );
69
+
70
+    // check that the command exits
71
+    const exec_path = try find_exec_path(argv[0], allocator) orelse return error.NoSuchCommand;
72
+    // FIXME: THIS IS NOT SAFE, since there's space for race condition between
73
+    //        the check and the execvpeZ call below.
74
+    //        I haven't found any better way to check, though: execvpeZ() call below
75
+    //        is running in child process so its error.FileNotFound is not really useful
76
+
77
+    // set up argv
78
+    const argv_ = a: {
79
+        var al = std.ArrayList([]const u8).init(allocator);
80
+        try al.append(switch (EXEC_PATH) {
81
+            .keep => argv[0],
82
+            .resolve => exec_path,
83
+        });
84
+        try al.appendSlice(argv[1..]);
85
+        break :a try al.toOwnedSlice();
86
+    };
87
+
88
+    // make it into zero-terminated
89
+    const argv_buf = try allocator.allocSentinel(?[*:0]const u8, argv_.len, null);
90
+    for (argv_, 0..) |arg, i| {
91
+        argv_buf[i] = (try allocator.dupeZ(u8, arg)).ptr;
92
+    }
93
+
94
+    // fork()!
95
+    const child_pid = try std.posix.fork();
96
+    if (child_pid == 0) {
97
+        // WE ARE THE CHILD
98
+
99
+        // copy the file handles
100
+        if (files.stdin) |f| std.posix.dup2(f.handle, std.posix.STDIN_FILENO) catch {};
101
+        if (files.stdout) |f| std.posix.dup2(f.handle, std.posix.STDOUT_FILENO) catch {};
102
+        if (files.stderr) |f| std.posix.dup2(f.handle, std.posix.STDERR_FILENO) catch {};
103
+        const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, env.ptr);
104
+        _ = err catch std.process.exit(shell_.EXIT_ERROR); // nobody should see this, though..
105
+    } else {
106
+        // WE ARE THE PARENT
107
+
108
+        // close all the files and wait
109
+        // if (files.stdin) |f| f.close();
110
+        // if (files.stdout) |f| f.close();
111
+        // if (files.stderr) |f| f.close();
112
+        const res = std.posix.waitpid(child_pid, 0);
113
+        const term = statusToTerm(res.status);
114
+        if (term == .Exited) return term.Exited;
115
+        return error.UnsupportedWaitResult;
116
+    }
117
+}

+ 12
- 8
src/main.zig View File

@@ -1,14 +1,18 @@
1 1
 const std = @import("std");
2 2
 
3
+const shell = @import("shell.zig");
4
+
5
+pub const std_options: std.Options = .{ .log_level = .info };
6
+
3 7
 pub fn main() !void {
4
-    // Uncomment this block to pass the first stage
5
-    // const stdout = std.io.getStdOut().writer();
6
-    // try stdout.print("$ ", .{});
8
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
9
+    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
10
+    defer arena.deinit();
7 11
 
8
-    const stdin = std.io.getStdIn().reader();
9
-    var buffer: [1024]u8 = undefined;
10
-    const user_input = try stdin.readUntilDelimiter(&buffer, '\n');
12
+    var s = shell.Shell.init(arena.allocator());
13
+    try s.run();
14
+}
11 15
 
12
-    // TODO: Handle user input
13
-    _ = user_input;
16
+test {
17
+    std.testing.refAllDecls(@This());
14 18
 }

+ 110
- 0
src/pedantic.zig View File

@@ -0,0 +1,110 @@
1
+const std = @import("std");
2
+
3
+pub const PwdComputer = struct {
4
+    byenv: []const u8,
5
+    byenv_resolved: []const u8,
6
+    byenv_realpath: []const u8,
7
+    actual: []const u8,
8
+    actual_resolved: []const u8,
9
+    allocator: std.mem.Allocator,
10
+
11
+    // POSIX (speaks in POSIXese)
12
+    //
13
+    // About pwd options (<https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pwd.html>):
14
+    //
15
+    // > The following options shall be supported by the implementation:
16
+    // >
17
+    // >  * `-L` - If the PWD environment variable contains an absolute pathname
18
+    // >    of the current directory and the pathname does not contain any components
19
+    // >    that are dot or dot-dot, pwd shall write this pathname to standard output,
20
+    // >    except that if the PWD environment variable is longer than {PATH_MAX} bytes
21
+    // >    including the terminating null, it is unspecified whether pwd writes this
22
+    // >    pathname to standard output or behaves as if the -P option had been specified.
23
+    // >
24
+    // >    Otherwise, the -L option shall behave as the -P option.
25
+    // >
26
+    // > *  `-P` - The pathname written to standard output shall not contain any components
27
+    // >    that refer to files of type symbolic link. If there are multiple pathnames
28
+    // >    that the pwd utility could write to standard output, one beginning with a single
29
+    // >    *slash* character and one or more beginning with two *slash* characters,
30
+    // >    then it shall write the pathname beginning with a single *slash* character.
31
+    // >
32
+    // >    The pathname shall not contain any unnecessary *slash* characters after the
33
+    // >    leading one or two *slash* characters.
34
+    // >
35
+    // > If both -L and -P are specified, the last one shall apply. If neither -L nor -P is
36
+    // > specified, the pwd utility shall behave as if -L had been specified.
37
+    //
38
+    // About PWD (<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html>):
39
+    //
40
+    // > This variable shall represent an absolute pathname of the current working directory.
41
+    // > It shall not contain any components that are dot or dot-dot. The value is set by the
42
+    // > cd utility, and by the sh utility during initialization.
43
+
44
+    pub fn create(allocator: std.mem.Allocator) !PwdComputer {
45
+        const byenv = std.process.getEnvVarOwned(allocator, "PWD") catch |e| switch (e) {
46
+            error.EnvironmentVariableNotFound => return error.UndefinedBehavior,
47
+            else => return e,
48
+        };
49
+        const actual = try std.process.getCwdAlloc(allocator);
50
+        return PwdComputer{
51
+            .byenv = byenv,
52
+            .byenv_resolved = try std.fs.path.resolvePosix(allocator, &.{byenv}),
53
+            .byenv_realpath = try std.fs.realpathAlloc(allocator, byenv),
54
+            .actual = actual,
55
+            .actual_resolved = try std.fs.path.resolvePosix(allocator, &.{actual}),
56
+            .allocator = allocator,
57
+        };
58
+    }
59
+
60
+    pub fn destroy(self: @This()) void {
61
+        self.allocator.free(self.byenv);
62
+        self.allocator.free(self.byenv_resolved);
63
+        self.allocator.free(self.actual);
64
+        self.allocator.free(self.actual_resolved);
65
+    }
66
+
67
+    fn _has_dot_or_dotdot(path: []const u8) bool {
68
+        var it = std.mem.tokenizeScalar(u8, path, '/');
69
+        while (it.next()) |component| switch (component.len) {
70
+            0 => continue,
71
+            1 => if (component[0] == '.') return true,
72
+            2 => if (component[0] == '.' and component[1] == '.') return true,
73
+            else => continue,
74
+        };
75
+        return false;
76
+    }
77
+
78
+    fn _is_too_long(path: []const u8) bool {
79
+        return (path.len + 1) > std.posix.PATH_MAX;
80
+    }
81
+
82
+    fn _l_applies(self: @This()) bool {
83
+        // > If the PWD environment variable contains an absolute pathname
84
+        if (self.byenv.len < 1) return false; // empty
85
+        if (self.byenv[0] != '/') return false; // not absolute
86
+        // > ... of the current directory
87
+        if (!std.mem.eql(u8, self.byenv_resolved, self.actual_resolved)) return false;
88
+        // > ... and the pathname does not contain any components that are dot or dot-dot,
89
+        if (_has_dot_or_dotdot(self.byenv)) return false;
90
+        return true;
91
+    }
92
+
93
+    pub fn getpath_P(self: @This()) []const u8 {
94
+        return self.byenv_realpath;
95
+    }
96
+
97
+    pub fn getpath_L(self: @This()) []const u8 {
98
+        if (!self._l_applies()) return self.getpath_P();
99
+        // > .. except that if the PWD environment variable is longer than {PATH_MAX} bytes
100
+        // > including the terminating null..
101
+        if ((self.byenv.len + 1) > std.posix.PATH_MAX) {
102
+            // > .. it is unspecified whether pwd writes this pathname to standard output
103
+            // > or behaves as if the -P option had been specified
104
+            //
105
+            // We choose the second. option
106
+            return self.getpath_P();
107
+        }
108
+        return self.byenv;
109
+    }
110
+};

+ 311
- 0
src/shell.zig View File

@@ -0,0 +1,311 @@
1
+const std = @import("std");
2
+
3
+const bltn_ = @import("bltn.zig");
4
+const cmd_ = @import("cmd.zig");
5
+const syntax = @import("syntax.zig");
6
+const util = @import("util.zig");
7
+
8
+const Bltn = bltn_.Bltn;
9
+const RedirOp = syntax.RedirOp;
10
+const run_command = cmd_.run_command;
11
+
12
+pub const EXIT_OK = 0;
13
+pub const EXIT_NO = 1;
14
+pub const EXIT_USAGE_ERROR = 2;
15
+pub const EXIT_ERROR = 3;
16
+pub const EXIT_ERRPANIC = 4;
17
+pub const EXIT_IOPANIC = 5;
18
+
19
+pub const CMD_NAME_INVALID = "/\x00\n";
20
+
21
+pub const DEVEL_MODE: enum { devel, release } = .devel;
22
+pub const DEVEL_MODE_SHOW_BUFFER: bool = false;
23
+
24
+const BUFSIZE_REPL = 8192;
25
+const BUFSIZE_INPUT = 1024;
26
+
27
+pub const StdIO = struct {
28
+    stdin_f: std.fs.File,
29
+    stdout_f: std.fs.File,
30
+    stderr_f: std.fs.File,
31
+    shell_stderr_f: std.fs.File,
32
+    stdin_redir: ?RedirOp,
33
+    stdout_redir: ?RedirOp,
34
+    stderr_redir: ?RedirOp,
35
+
36
+    fn _handle_stderr_error(self: @This(), e: anyerror) void {
37
+        // in case of external stdin, we can fall
38
+        if (self.stderr_redir == null) iopanic(); // not redirected
39
+        const path = self.stderr_redir.?.path;
40
+        const w = self.shell_stderr_f.writer();
41
+        w.print("cannot write to redirected stderr: {!} when writing to {s}\n", .{ e, path }) catch iopanic();
42
+    }
43
+
44
+    /// Try to write details of error *e* to file handle *err_f* and die.
45
+    /// This exits the process with EXIT_ERRPANIC, except when the write fails
46
+    /// in which case it exits with EXIT_IOPANIC.
47
+    fn errpanic(err_f: std.fs.File, e: anyerror) noreturn {
48
+        err_f.writeAll("panic: ") catch iopanic();
49
+        err_f.writeAll(@errorName(e)) catch iopanic();
50
+        err_f.writeAll("\n") catch iopanic();
51
+        std.process.exit(EXIT_ERRPANIC);
52
+    }
53
+
54
+    pub const Files = struct {
55
+        stdin: ?std.fs.File = null,
56
+        stdout: ?std.fs.File = null,
57
+        stderr: ?std.fs.File = null,
58
+    };
59
+
60
+    fn redird_files(self: @This()) Files {
61
+        var fs = Files{};
62
+        if (self.stdin_redir != null) fs.stdin = self.stdin_f;
63
+        if (self.stdout_redir != null) fs.stdout = self.stdout_f;
64
+        if (self.stderr_redir != null) fs.stderr = self.stderr_f;
65
+        return fs;
66
+    }
67
+
68
+    fn close(self: @This()) void {
69
+        if (self.stdin_redir) |op| {
70
+            std.log.debug("closing stdin redirect: {s}", .{op.path});
71
+            self.stdin_f.close();
72
+        }
73
+        if (self.stdout_redir) |op| {
74
+            std.log.debug("closing stdout redirect: {s}", .{op.path});
75
+            self.stdout_f.close();
76
+        }
77
+        if (self.stderr_redir) |op| {
78
+            std.log.debug("closing stderr redirect: {s}", .{op.path});
79
+            self.stderr_f.close();
80
+        }
81
+    }
82
+
83
+    pub fn err(self: @This(), msg: []const u8) void {
84
+        self.stderr_f.writeAll(msg) catch |e| self._handle_stderr_error(e);
85
+        self.stderr_f.writeAll("\n") catch |e| self._handle_stderr_error(e);
86
+    }
87
+
88
+    pub fn errf(self: @This(), comptime fmt: []const u8, args: anytype) void {
89
+        self.stderr_f.writer().print(fmt, args) catch |e| self._handle_stderr_error(e);
90
+        self.stderr_f.writeAll("\n") catch |e| self._handle_stderr_error(e);
91
+    }
92
+
93
+    pub fn outw(self: @This(), msg: []const u8) !void {
94
+        try self.stdout_f.writeAll(msg);
95
+    }
96
+
97
+    pub fn out(self: @This(), msg: []const u8) !void {
98
+        try self.stdout_f.writeAll(msg);
99
+        try self.stdout_f.writeAll("\n");
100
+    }
101
+
102
+    pub fn outf(self: @This(), comptime fmt: []const u8, args: anytype) !void {
103
+        try self.stdout_f.writer().print(fmt, args);
104
+        try self.stdout_f.writeAll("\n");
105
+    }
106
+};
107
+
108
+/// Context of single REPL iteration
109
+pub const Loop = struct {
110
+    shell: *Shell,
111
+    allocator: std.mem.Allocator,
112
+
113
+    /// Get input from user
114
+    fn _get_input(self: *@This()) ![]const u8 {
115
+        try self.shell.outw("$ ");
116
+        const r = self.shell.stdin_f.reader();
117
+        return try r.readUntilDelimiter(self.shell.input_buffer.?, '\n');
118
+    }
119
+
120
+    /// Open path for IO redirect
121
+    fn _open_redir_file(op: RedirOp) !std.fs.File {
122
+        const cwd_fd = std.fs.cwd();
123
+        switch (op.dir) {
124
+            .in => return try cwd_fd.openFile(op.path, .{ .mode = .read_only }),
125
+            .out => return try cwd_fd.createFile(op.path, .{}),
126
+            .app => {
127
+                // zls fails here; thinks `res` is `File` correct is `?File`
128
+                const res: ?std.fs.File = cwd_fd.openFile(op.path, .{ .mode = .write_only }) catch |e| switch (e) {
129
+                    error.FileNotFound => null,
130
+                    else => return e,
131
+                };
132
+                if (res) |fd| {
133
+                    try fd.seekFromEnd(0);
134
+                    return fd;
135
+                }
136
+                return try cwd_fd.createFile(op.path, .{});
137
+            },
138
+        }
139
+    }
140
+
141
+    /// Create StdIO object with redirects applied and the proper filehandles open
142
+    fn _create_stdio(
143
+        self: @This(),
144
+        stdin_redir: ?RedirOp,
145
+        stdout_redir: ?RedirOp,
146
+        stderr_redir: ?RedirOp,
147
+    ) !StdIO {
148
+        return StdIO{
149
+            .stdin_f = if (stdin_redir) |op| try _open_redir_file(op) else self.shell.stdin_f,
150
+            .stdout_f = if (stdout_redir) |op| try _open_redir_file(op) else self.shell.stdout_f,
151
+            .stderr_f = if (stderr_redir) |op| try _open_redir_file(op) else self.shell.stderr_f,
152
+            .shell_stderr_f = self.shell.stderr_f,
153
+            .stdin_redir = stdin_redir,
154
+            .stdout_redir = stdout_redir,
155
+            .stderr_redir = stderr_redir,
156
+        };
157
+    }
158
+
159
+    /// Do one loop of REPL
160
+    fn run(self: *@This()) !void {
161
+        const user_input = self._get_input() catch |e| switch (e) {
162
+            error.EndOfStream => self.shell.exit_eof(),
163
+            else => return e,
164
+        };
165
+
166
+        if (user_input.len == 0) return;
167
+        const stmt = try syntax.parse_stmt(user_input, self.allocator);
168
+        if (stmt.argv.len == 0) return;
169
+
170
+        const stdio = try self._create_stdio(
171
+            stmt.stdin_redir,
172
+            stmt.stdout_redir,
173
+            stmt.stderr_redir,
174
+        );
175
+        defer stdio.close();
176
+
177
+        const ctx = bltn_.BltnCtx{
178
+            .shell = self.shell,
179
+            .stdio = stdio,
180
+            .loop = self,
181
+            .cmd_name = stmt.argv[0],
182
+            .args = stmt.argv[1..],
183
+            .allocator = self.allocator,
184
+        };
185
+
186
+        bltn_.run_bltn(ctx) catch |e| switch (e) {
187
+            error.UsageError => {},
188
+            error.NoSuchBltn => {
189
+                const es = run_command(
190
+                    stmt.argv,
191
+                    stdio.redird_files(),
192
+                    self.allocator,
193
+                ) catch |run_e| switch (run_e) {
194
+                    error.NoSuchCommand => {
195
+                        self.shell.errf("{s}: command not found", .{stmt.argv[0]});
196
+                        return;
197
+                    },
198
+                    else => return run_e,
199
+                };
200
+                _ = es;
201
+            },
202
+            else => return e,
203
+        };
204
+    }
205
+};
206
+
207
+/// Just die after realizing even IO does not work
208
+///
209
+/// Use this as last resort, when catching from a call which is not expected
210
+/// to have any other mode of failure than stderr writing error.
211
+///
212
+/// In any other cases (eg. when the call involves formatting, ie. the failure
213
+/// could be something like memory allocation), catch the error and use
214
+/// `errpanic(fh, e)` at least, so that user has a chance of understanding what
215
+/// is wrong.
216
+///
217
+/// Since EXIT_IOPANIC is not used anywhere else, at least seeing that exit
218
+/// status can hint towards the fact that there was error but stderr was broken
219
+/// so we could not provide any info.
220
+pub fn iopanic() noreturn {
221
+    std.process.exit(EXIT_IOPANIC);
222
+}
223
+
224
+/// Try to write details of error *e* to file handle *err_f* and die.
225
+/// This exits the process with EXIT_ERRPANIC, except when the write fails
226
+/// in which case it exits with EXIT_IOPANIC.
227
+pub fn errpanic(err_f: std.fs.File, e: anyerror) noreturn {
228
+    err_f.writeAll("panic: ") catch iopanic();
229
+    err_f.writeAll(@errorName(e)) catch iopanic();
230
+    err_f.writeAll("\n") catch iopanic();
231
+    std.process.exit(EXIT_ERRPANIC);
232
+}
233
+
234
+pub const Shell = struct {
235
+    stdin_f: std.fs.File,
236
+    stdout_f: std.fs.File,
237
+    stderr_f: std.fs.File,
238
+    allocator: std.mem.Allocator,
239
+    repl_buffer: ?[]u8 = null,
240
+    input_buffer: ?[]u8 = null,
241
+    vars: Vars,
242
+
243
+    const Vars = struct {
244
+        OLDPWD: ?[]const u8 = null,
245
+        ES: u8 = 0,
246
+    };
247
+
248
+    pub fn init(allocator: std.mem.Allocator) Shell {
249
+        return Shell{
250
+            .stdout_f = std.io.getStdOut(),
251
+            .stdin_f = std.io.getStdIn(),
252
+            .stderr_f = std.io.getStdErr(),
253
+            .allocator = allocator,
254
+            .vars = Vars{},
255
+        };
256
+    }
257
+
258
+    pub fn setOldPwd(self: *@This(), path: []const u8) !void {
259
+        if (self.vars.OLDPWD) |pwd| {
260
+            self.allocator.free(pwd);
261
+        }
262
+        self.vars.OLDPWD = try self.allocator.dupe(u8, path);
263
+    }
264
+
265
+    fn errf(self: @This(), comptime fmt: []const u8, args: anytype) void {
266
+        self.stderr_f.writer().print(fmt, args) catch |e| errpanic(self.stderr_f, e);
267
+        self.stderr_f.writeAll("\n") catch iopanic();
268
+    }
269
+
270
+    fn outw(self: @This(), msg: []const u8) !void {
271
+        try self.stdout_f.writeAll(msg);
272
+    }
273
+
274
+    fn exit_eof(self: @This()) noreturn {
275
+        self.stdout_f.writeAll("\n") catch iopanic();
276
+        std.process.exit(EXIT_OK);
277
+    }
278
+
279
+    pub fn exit(self: @This(), es: u7) noreturn {
280
+        _ = self;
281
+        std.process.exit(@as(u8, es));
282
+    }
283
+
284
+    fn repl(self: *@This()) !void {
285
+        var fba = std.heap.FixedBufferAllocator.init(self.repl_buffer.?);
286
+        while (true) {
287
+            fba.reset();
288
+            var loop = Loop{
289
+                .shell = self,
290
+                .allocator = fba.allocator(),
291
+            };
292
+            try loop.run();
293
+        }
294
+    }
295
+
296
+    pub fn run(self: *@This()) !void {
297
+        self.repl_buffer = self.allocator.alloc(u8, BUFSIZE_REPL) catch |e| errpanic(self.stderr_f, e);
298
+        self.input_buffer = self.allocator.alloc(u8, BUFSIZE_INPUT) catch |e| errpanic(self.stderr_f, e);
299
+        defer self.allocator.free(self.repl_buffer.?);
300
+        defer self.allocator.free(self.input_buffer.?);
301
+        _ = self.repl() catch |e| {
302
+            switch (DEVEL_MODE) {
303
+                .devel => {
304
+                    if (DEVEL_MODE_SHOW_BUFFER) self.errf("REPL buffer: {s}", .{util.qb(self.repl_buffer.?, 100000)});
305
+                    return e;
306
+                },
307
+                .release => errpanic(self.stderr_f, e),
308
+            }
309
+        };
310
+    }
311
+};

+ 1196
- 0
src/syntax.zig
File diff suppressed because it is too large
View File


+ 155
- 0
src/util.zig View File

@@ -0,0 +1,155 @@
1
+const std = @import("std");
2
+
3
+pub fn realpathWithTilde(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
4
+    const replaced = try subst_tilde(allocator, path);
5
+    defer allocator.free(replaced);
6
+    return try std.fs.realpathAlloc(allocator, replaced);
7
+}
8
+
9
+/// leverage expectEqualStrings() when possible
10
+pub fn assert_equal_slices(
11
+    comptime T: type,
12
+    comptime fmtfn: *const fn (T) []const u8,
13
+    oracle: []const T,
14
+    result: []const T,
15
+) !void {
16
+    var ew = std.io.getStdErr().writer();
17
+    const max_checks = @max(oracle.len, result.len);
18
+    for (0..max_checks) |idx| {
19
+        if (idx >= result.len) {
20
+            const missing = oracle[idx..];
21
+            for (missing) |item| try ew.print(
22
+                "expected but missing: {s} at index {d}\n",
23
+                .{ fmtfn(item), idx },
24
+            );
25
+            return error.TestFailMissingItems;
26
+        }
27
+        if (idx >= oracle.len) {
28
+            const extra = result[idx..];
29
+            for (extra) |item| try ew.print(
30
+                "unexpected extra item: {s} at index {d}\n",
31
+                .{ fmtfn(item), idx },
32
+            );
33
+            return error.TestFailExtraItems;
34
+        }
35
+        try std.testing.expectEqualStrings(
36
+            fmtfn(oracle[idx]),
37
+            fmtfn(result[idx]),
38
+        );
39
+    }
40
+}
41
+
42
+/// leverage expectEqualStrings() when possible
43
+pub fn assert_optional_strings(
44
+    oracle: ?[]const u8,
45
+    result: ?[]const u8,
46
+) !void {
47
+    if (oracle == null and result == null) return;
48
+    if (oracle != null and result == null) {
49
+        try std.io.getStdErr().writer().print("expected \"{s}\", got null\n", .{oracle.?});
50
+        return error.TestFailOptinalString;
51
+    }
52
+    if (oracle == null and result != null) {
53
+        try std.io.getStdErr().writer().print("expected null, got \"{s}\"\n", .{result.?});
54
+        return error.TestFailOptinalString;
55
+    }
56
+    try std.testing.expectEqualStrings(oracle.?, result.?);
57
+}
58
+
59
+pub fn subst_tilde(allocator: std.mem.Allocator, path: []const u8) ![]u8 {
60
+    var home: []const u8 = undefined;
61
+    var delim: []const u8 = "/";
62
+    var replaced: []u8 = undefined;
63
+    if (!std.mem.startsWith(u8, path, "~")) {
64
+        replaced = try allocator.dupe(u8, path);
65
+        return replaced;
66
+    }
67
+    home = try std.process.getEnvVarOwned(allocator, "HOME");
68
+    defer allocator.free(home);
69
+    if (std.mem.eql(u8, path, "~")) {
70
+        replaced = try allocator.dupe(u8, home);
71
+        return replaced;
72
+    }
73
+    if (!std.mem.startsWith(u8, path, "~/")) {
74
+        replaced = try allocator.dupe(u8, path);
75
+        return replaced;
76
+    }
77
+    if (std.mem.endsWith(u8, home, "/")) delim = "";
78
+    return try std.fmt.allocPrint(allocator, "{s}{s}{s}", .{ home, delim, path[2..] });
79
+}
80
+
81
+const QMAX: usize = 64;
82
+
83
+pub fn qb(blob: []const u8, max: ?usize) []const u8 {
84
+    var al = std.ArrayList(u8).init(std.heap.page_allocator);
85
+    qblob(blob, max orelse QMAX, al.writer().any()) catch unreachable;
86
+    return al.toOwnedSlice() catch unreachable;
87
+}
88
+
89
+pub fn qbs(blobs: []const []const u8, max: ?usize) []const u8 {
90
+    var al = std.ArrayList(u8).init(std.heap.page_allocator);
91
+    al.writer().writeAll("&.{") catch unreachable;
92
+    for (blobs, 0..) |blob, idx| {
93
+        if (idx > 0) al.writer().writeByte(',') catch unreachable;
94
+        qblob(blob, max orelse QMAX, al.writer().any()) catch unreachable;
95
+    }
96
+    al.writer().writeByte('}') catch unreachable;
97
+    return al.toOwnedSlice() catch unreachable;
98
+}
99
+
100
+pub fn qs(str: []const u8, max: ?usize) []const u8 {
101
+    var al = std.ArrayList(u8).init(std.heap.page_allocator);
102
+    qstr(str, max orelse QMAX, al.writer().any()) catch unreachable;
103
+    return al.toOwnedSlice() catch unreachable;
104
+}
105
+
106
+pub fn qss(strs: []const []const u8, max: ?usize) []const u8 {
107
+    var al = std.ArrayList(u8).init(std.heap.page_allocator);
108
+    al.writer().writeAll("&.{") catch unreachable;
109
+    for (strs, 0..) |str, idx| {
110
+        if (idx > 0) al.writer().writeByte(',') catch unreachable;
111
+        qstr(str, max orelse QMAX, al.writer().any()) catch unreachable;
112
+    }
113
+    al.writer().writeByte('}') catch unreachable;
114
+    return al.toOwnedSlice() catch unreachable;
115
+}
116
+
117
+pub fn qblob(blob: []const u8, max: usize, writer: std.io.AnyWriter) !void {
118
+    const last_idx = @min(blob.len, max);
119
+    try writer.print("[{d}]|", .{blob.len});
120
+    try escape_nonascii(blob[0..last_idx], writer);
121
+    if (blob.len > max) _ = try writer.write("..");
122
+    try writer.writeByte('|');
123
+}
124
+
125
+pub fn qstr(str: []const u8, max: usize, writer: std.io.AnyWriter) !void {
126
+    const last_idx = @min(str.len, max);
127
+    try writer.writeByte('"');
128
+    try escape(str[0..last_idx], writer);
129
+    if (str.len > max) try writer.print("..({d} more)", .{str.len - max});
130
+    try writer.writeByte('"');
131
+}
132
+
133
+pub fn escape(str: []const u8, writer: std.io.AnyWriter) !void {
134
+    for (str) |c| _ = switch (c) {
135
+        '\n' => try writer.write("\\n"),
136
+        '\r' => try writer.write("\\r"),
137
+        '\t' => try writer.write("\\t"),
138
+        '\\' => try writer.write("\\\\"),
139
+        '\'' => try writer.write("\\'"),
140
+        '"' => try writer.write("\\\""),
141
+        else => try writer.writeByte(c),
142
+    };
143
+}
144
+
145
+pub fn escape_nonascii(str: []const u8, writer: std.io.AnyWriter) !void {
146
+    for (str) |c| _ = switch (c) {
147
+        '\n' => try writer.write("\\n"),
148
+        '\r' => try writer.write("\\r"),
149
+        '\t' => try writer.write("\\t"),
150
+        '\\' => try writer.write("\\\\"),
151
+        '\'' => try writer.write("\\'"),
152
+        '"' => try writer.write("\\\""),
153
+        else => if (std.ascii.isASCII(c)) try writer.writeByte(c) else try writer.print("\\x{x}", .{c}),
154
+    };
155
+}

+ 125
- 0
utils/tgen.sh View File

@@ -0,0 +1,125 @@
1
+#!/bin/bash
2
+
3
+
4
+warn() {
5
+    echo "$@" >&2
6
+}
7
+
8
+die() {
9
+    echo "$@" >&2
10
+    exit 3
11
+}
12
+
13
+content_desc() {
14
+    local c=$1
15
+    case $c in
16
+        \ ) echo space ;;
17
+        \!) echo exclamation mark ;;
18
+        \$) echo dollar sign ;;
19
+        \&) echo ampersand ;;
20
+        \() echo opening bracket ;;
21
+        \)) echo closing bracket ;;
22
+        \*) echo asterisk ;;
23
+        \;) echo semicolon ;;
24
+        \?) echo question mark ;;
25
+        \\) echo backslash ;;
26
+        \`) echo tick mark;;
27
+        \>) echo greater than ;;
28
+        \<) echo less than ;;
29
+        \{) echo opening curly brace ;;
30
+        \|) echo pipe symbol ;;
31
+        \}) echo closing curly brace ;;
32
+        BS) echo backslash ;;
33
+        SQ) echo single quote ;;
34
+        DQ) echo double quote ;;
35
+        TAB) echo tab ;;
36
+        ES) echo empty string ;;
37
+        NL) echo newline ;;
38
+        A) echo letter A ;;
39
+    esac
40
+}
41
+
42
+mode_name() {
43
+    local mode=$1
44
+    case $mode in
45
+        bs) echo backslash escaping ;;
46
+        sq) echo single quoting ;;
47
+        dq) echo double quoting ;;
48
+    esac
49
+}
50
+
51
+
52
+mktest() {
53
+    local mode=$1; shift
54
+    local mode_name
55
+    local content_desc
56
+    mode_name="$(mode_name "$mode")"
57
+    local c
58
+    local self=$0
59
+    printf 'test "parse: %s (generated)" {\n' "$mode_name"
60
+    printf '    // TEST GENERATED BY %s\n' "$self"
61
+    #shellcheck disable=SC2016
62
+    for c in "$@"; do
63
+        content_desc="$(content_desc "$c")"
64
+        # every backslash needs to be doubled because of printf.
65
+        # some backslashes need to be doubled again becase of syntax (eg. `"\\\\" is 2 backslashes in Zig)
66
+        case $mode:$c in
67
+            bs:BS)      printf '    try parse_t("\\\\\\\\", Result{ .tokens = &.{"\\\\"} });  // %s `\\` (%s)\n' "$mode_name" "$content_desc" ;;
68
+            bs:DQ)      printf '    try parse_t("\\\\\\"", Result{ .tokens = &.{"\\""} });  // %s `"` (%s)\n' "$mode_name" "$content_desc" ;;
69
+            bs:ES)      true ;;     # impossible case
70
+            bs:NL)      printf '    try parse_t("\\\\\\n", Result{ .tokens = &.{"\\n"} });  // %s `\\n` (%s)\n' "$mode_name" "$content_desc" ;;
71
+            bs:SQ)      printf '    try parse_t("\\\\%s", Result{ .tokens = &.{"%s"} });  // %s `%s` (%s)\n' "'" "'" "$mode_name" "'" "$content_desc" ;;
72
+            bs:TAB)     printf '    try parse_t("\\\\\\t", Result{ .tokens = &.{"\\t"} });  // %s `\\t` (%s)\n' "$mode_name" "$content_desc"  ;;
73
+            bs:*)       printf '    try parse_t("\\\\%s", Result{ .tokens = &.{"%s"} });  // %s `%s` (%s)\n' "$c" "$c" "$mode_name" "$c" "$content_desc" ;;
74
+            sq:NL)      printf '    try parse_t("%s\\n%s", Result{ .tokens = &.{"\\n"} });  // %s `\\n` (%s)\n' "'" "'" "$mode_name" "$content_desc" ;;
75
+            sq:TAB)     printf '    try parse_t("%s\\t%s", Result{ .tokens = &.{"\\t"} });  // %s `\\t` (%s)\n' "'" "'" "$mode_name" "$content_desc" ;;
76
+            sq:DQ)      printf '    try parse_t("%s\\"%s", Result{ .tokens = &.{"\\""} });  // %s `"` (%s)\n' "'" "'" "$mode_name" "$content_desc" ;;
77
+            sq:ES)      printf '    try parse_t("%s%s", Result{ .tokens = &.{""} });  // %s `\\` (%s)\n' "'" "'" "$mode_name" "$content_desc" ;;
78
+            sq:BS)      printf '    try parse_t("%s\\\\%s", Result{ .tokens = &.{"\\\\"} });  // %s `\\` (%s)\n' "'" "'" "$mode_name" "$content_desc" ;;
79
+            sq:SQ)      true ;;     # impossible case
80
+            sq:*)       printf '    try parse_t("'"'"'%s'"'"'", Result{ .tokens = &.{"%s"} });  // %s `%s` (%s)\n' "$c" "$c" "$mode_name" "$c" "$content_desc" ;;
81
+            dq:BS)      printf '    try parse_t("\\"\\\\\\\\\\"", Result{ .tokens = &.{"\\\\"} });  // %s `\\` (%s)\n' "$mode_name" "$content_desc" ;;
82
+            dq:DQ)      printf '    try parse_t("\\"\\\\\\"\\"", Result{ .tokens = &.{"\\""} });  // %s `"` (%s)\n' "$mode_name" "$content_desc" ;;
83
+            dq:ES)      printf '    try parse_t("\\"\\"", Result{ .tokens = &.{""} });  // %s `\\` (%s)\n' "$mode_name" "$content_desc" ;;
84
+            dq:NL)      printf '    try parse_t("\\"\\n\\"", Result{ .tokens = &.{"\\n"} });  // %s `\\n` (%s)\n' "$mode_name" "$content_desc" ;;
85
+            dq:SQ)      printf '    try parse_t("\\"%s\\"", Result{ .tokens = &.{"%s"} });  // %s `%s` (%s)\n' "'" "'" "$mode_name" "'" "$content_desc" ;;
86
+            dq:TAB)     printf '    try parse_t("\\"\\t\\"", Result{ .tokens = &.{"\\t"} });  // %s `\\t` (tab)\n' "$mode_name" ;;
87
+            dq:*)       printf '    try parse_t("\\"%s\\"", Result{ .tokens = &.{"%s"} });  // %s `%s` (%s)\n' "$c" "$c" "$mode_name" "$c" "$content_desc" ;;
88
+            *)  die "unknown character or mode: c=$c mode=$mode" ;;
89
+        esac
90
+    done
91
+    printf '}\n'
92
+    echo
93
+}
94
+
95
+main() {
96
+    local cases=(
97
+        '!'
98
+        '$'
99
+        '&'
100
+        '('
101
+        ')'
102
+        '*'
103
+        ';'
104
+        '?'
105
+        '`'
106
+        '{'
107
+        '>'
108
+        '<'
109
+        '|'
110
+        '}'
111
+        ' '
112
+        SQ
113
+        DQ
114
+        BS
115
+        TAB
116
+        NL
117
+        ES
118
+        A
119
+    )
120
+    mktest bs "${cases[@]}"
121
+    mktest sq "${cases[@]}"
122
+    mktest dq "${cases[@]}"
123
+}
124
+
125
+main "$@"

+ 1
- 1
your_program.sh View File

@@ -21,4 +21,4 @@ set -e # Exit early if any commands fail
21 21
 #
22 22
 # - Edit this to change how your program runs locally
23 23
 # - Edit .codecrafters/run.sh to change how your program runs remotely
24
-exec zig-out/bin/main "$@"
24
+exec strace -f -o your_program.strace zig-out/bin/main "$@"