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