#QP2 Tests receiving empty output/whitespace

I’m stuck on Stage #QP2.

I’ve found some issue that similar to this issue. I’m using linenoise to help me add command autocomplete feature. But somehow the test receiving whitespace when testing previous stage and I can’t reproduce it in my local.

Here are my logs:

[tester::#QP2] Running tests for Stage #QP2 (Command Completion - Builtin completion)
[tester::#QP2] Running ./your_program.sh
[tester::#QP2] ✓ Received prompt ($ )
[tester::#QP2] Typed "ech"
[tester::#QP2] ✓ Prompt line matches "$ ech"
[tester::#QP2] Pressed "<TAB>" (expecting autocomplete to "echo" followed by a space)
[tester::#QP2] ✓ Prompt line matches "echo "
[your-program] $ echo 
[tester::#QP2] Tearing down shell
[tester::#QP2] Running ./your_program.sh
[tester::#QP2] ✓ Received prompt ($ )
[tester::#QP2] Typed "exi"
[tester::#QP2] ✓ Prompt line matches "$ exi"
[tester::#QP2] Pressed "<TAB>" (expecting autocomplete to "exit" followed by a space)
[tester::#QP2] ✓ Prompt line matches "exit "
[your-program] $ exit 
[tester::#QP2] Tearing down shell
[tester::#QP2] Test passed.
[tester::#UN3] Running tests for Stage #UN3 (Redirection - Append stderr)
[tester::#UN3] [setup] export PATH=/tmp/raspberry/pineapple/banana:$PATH
[tester::#UN3] Running ./your_program.sh
[your-program] $ ls -1 nonexistent >> /tmp/rat/bee.md
[your-program]                                       
[tester::#UN3] ^ Line does not match expected value.
[tester::#UN3] Expected: "ls: nonexistent: No such file or directory"
[tester::#UN3] Received: "                                      " (trailing space)
[tester::#UN3] Test failed

And here’s a snippet of my code that handle ls:

const std = @import("std");
const builtin = @import("builtin");
const builtins = @import("builtins.zig");
const path_resolver = @import("path_resolver.zig");
const input_handler = @import("input_handler.zig");
const parser = @import("parser.zig");
const c = @cImport({
    @cInclude("linenoise.h");
});

const Commands = @import("command.zig").Commands;

export fn completionHook(buf: [*c]const u8, lc: [*c]c.linenoiseCompletions) void {
    const input = std.mem.span(buf);

    for (std.enums.values(Commands)) |cmd| {
        const cmd_str = @tagName(cmd);
        if (cmd == .invalid) continue;
        var buffer: [1024]u8 = undefined;
        const formatted = std.fmt.bufPrintSentinel(
            &buffer,
            "{s} ",
            .{cmd_str},
            0,
        ) catch unreachable;
        if (std.mem.startsWith(u8, cmd_str, input)) {
            c.linenoiseAddCompletion(lc, formatted);
        }
    }
}

pub fn main(init: std.process.Init) !void {
    c.linenoiseSetCompletionCallback(completionHook);
    var stderr = std.Io.File.stderr().writer(init.io, &.{});
    var stdout = std.Io.File.stdout().writer(init.io, &.{});

    const env = init.environ_map;

    try stdout.interface.print("", .{});

    while (true) {
        const raw_line = c.linenoise("$ ");
        if (raw_line == null) break;
        defer c.linenoiseFree(raw_line);

        const line = std.mem.span(raw_line);

        if (line.len == 0) continue;
        const trimmed = if (builtin.os.tag == .windows)
            std.mem.trim(u8, line, "\r")
        else
            line;

        var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
        defer arena.deinit();

        const allocator = arena.allocator();
        const t_command = try input_handler.tokenize(allocator, trimmed);
        const parsed = parser.parse(allocator, t_command) catch |err| {
            try stdout.interface.print("parse error: {s}\n", .{@errorName(err)});
            continue;
        };

        if (parsed.argv.len == 0) continue;

        const command_str = parsed.argv[0];
        const command = Commands.fromString(command_str) orelse .invalid;
        const args = parsed.argv[1..];

        var file_buffer: [4096]u8 = undefined;
        var file_writer_storage: ?std.Io.File.Writer = null;
        var out: *std.Io.Writer = &stdout.interface; // default target

        var err_file_buffer: [4096]u8 = undefined;
        var err_file_writer_storage: ?std.Io.File.Writer = null;
        var out_err: *std.Io.Writer = &stderr.interface;

        var stdout_redirect: ?parser.Redirect = null;
        var stderr_redirect: ?parser.Redirect = null;
        for (parsed.redirects) |r| {
            if (r.fd == 1) stdout_redirect = r;
            if (r.fd == 2) stderr_redirect = r;
        }

        if (stdout_redirect) |r| {
            const file = std.Io.Dir.cwd().createFile(init.io, r.target, .{
                .truncate = r.kind == .out,
                .read = r.kind == .append,
            }) catch {
                try out_err.print("shell: {s}: Unable to create file\n", .{r.target});
                continue;
            };

            file_writer_storage = file.writer(init.io, &file_buffer);

            if (r.kind == .append) {
                // const stat = try file.stat(init.io);
                try file_writer_storage.?.seekTo(try file.length(init.io));
            }

            out = &file_writer_storage.?.interface;
        }

        if (stderr_redirect) |r| {
            const file = std.Io.Dir.cwd().createFile(init.io, r.target, .{
                .truncate = r.kind == .out,
                .read = r.kind == .append,
            }) catch {
                try out_err.print("shell: {s}: Unable to create file\n", .{r.target});
                continue;
            };
            err_file_writer_storage = file.writer(init.io, &err_file_buffer);
            if (r.kind == .append) {
                // const stat = try file.stat(init.io);
                try err_file_writer_storage.?.seekTo(try file.length(init.io));
            }
            out_err = &err_file_writer_storage.?.interface;
        }
        defer if (file_writer_storage) |*fw| {
            fw.interface.flush() catch {};
            fw.file.close(init.io);
        };
        defer if (err_file_writer_storage) |*fw| {
            fw.interface.flush() catch {};
            fw.file.close(init.io);
        };

        switch (command) {
            .exit => break,
            .echo => try builtins.handleEcho(out, args, allocator),
            .type => try builtins.handleType(out, out_err, init.io, env.get("PATH") orelse "", args),
            .pwd => try builtins.handlePwd(init.io, out, out_err),
            .cd => try builtins.handleCd(init.io, args, out_err, env),
            .invalid => try builtins.handleInvalid(
                command_str,
                args,
                env.get("PATH") orelse "",
                init.io,
                allocator,
                out,
                out_err,
            ),
        }
    }
}

pub fn handleInvalid(
    cmd: []const u8,
    args: [][]const u8,
    path: []const u8,
    io: anytype,
    allocator: std.mem.Allocator,
    out: *std.Io.Writer,
    out_err: *std.Io.Writer,
) !void {
    try path_resolver.executeProgram(
        allocator,
        cmd,
        args,
        io,
        path,
        out,
        out_err,
    );
}
pub fn executeProgram(
    allocator: std.mem.Allocator,
    command: []const u8,
    args: [][]const u8,
    io: anytype,
    path_env: []const u8,
    out: *std.Io.Writer,
    out_err: *std.Io.Writer,
) !void {
    const program_path = try findExecutable(allocator, io, path_env, command);

    if (program_path) |_| {
        var argv = std.ArrayList([]const u8).empty;
        defer argv.deinit(allocator);

        try argv.append(allocator, command);
        try argv.appendSlice(allocator, args);

        var child_proc = try std.process.spawn(
            io,
            .{
                .argv = argv.items,
                .stdout = .pipe,
                .stderr = .pipe,
            },
        );

        if (child_proc.stdout) |*child_stdout| {
            var read_buf: [4096]u8 = undefined;
            var reader = child_stdout.readerStreaming(io, &read_buf);

            while (true) {
                const bytes_read = reader.interface.readSliceShort(&read_buf) catch |err| {
                    if (err == error.EndOfStream) break;
                    return err;
                };
                if (bytes_read == 0) break;
                try out.writeAll(read_buf[0..bytes_read]);
            }
        }

        // Read stderr stream from child and forward it to our target error writer ('out_err')
        if (child_proc.stderr) |*child_stderr| {
            var read_buf: [4096]u8 = undefined;
            var reader = child_stderr.readerStreaming(io, &read_buf);

            while (true) {
                const bytes_read = reader.interface.readSliceShort(&read_buf) catch |err| {
                    if (err == error.EndOfStream) break;
                    return err;
                };
                if (bytes_read == 0) break;
                try out_err.writeAll(read_buf[0..bytes_read]);
            }
        }

        _ = try child_proc.wait(io);
    } else {
        try out_err.print("{s}: command not found\n", .{command});
    }

    // return null;
}

You can also see the github repo: GitHub - babanini95/codecrafters-shell-zig: shell made with zig · GitHub