const builtin = @import("builtin"); const std = @import("std"); const fatal = std.zig.fatal; const mem = std.mem; const fs = std.fs; const process = std.process; const Allocator = std.mem.Allocator; const testing = std.testing; const getExternalExecutor = std.zig.system.getExternalExecutor; const max_doc_file_size = 10 * 1024 * 1024; const usage = \\Usage: doctest [options] -i input -o output \\ \\ Compiles and possibly runs a code example, capturing output and rendering \\ it to HTML documentation. \\ \\Options: \\ -h, --help Print this help and exit \\ -i input Source code file path \\ -o output Where to write output HTML docs to \\ --zig zig Path to the zig compiler \\ --zig-lib-dir dir Override the zig compiler library path \\ --cache-root dir Path to local .zig-cache/ \\ ; pub fn main() !void { var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena_instance.deinit(); const arena = arena_instance.allocator(); var args_it = try process.argsWithAllocator(arena); if (!args_it.skip()) fatal("missing argv[0]", .{}); var opt_input: ?[]const u8 = null; var opt_output: ?[]const u8 = null; var opt_zig: ?[]const u8 = null; var opt_zig_lib_dir: ?[]const u8 = null; var opt_cache_root: ?[]const u8 = null; while (args_it.next()) |arg| { if (mem.startsWith(u8, arg, "-")) { if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) { try std.io.getStdOut().writeAll(usage); process.exit(0); } else if (mem.eql(u8, arg, "-i")) { opt_input = args_it.next() orelse fatal("expected parameter after -i", .{}); } else if (mem.eql(u8, arg, "-o")) { opt_output = args_it.next() orelse fatal("expected parameter after -o", .{}); } else if (mem.eql(u8, arg, "--zig")) { opt_zig = args_it.next() orelse fatal("expected parameter after --zig", .{}); } else if (mem.eql(u8, arg, "--zig-lib-dir")) { opt_zig_lib_dir = args_it.next() orelse fatal("expected parameter after --zig-lib-dir", .{}); } else if (mem.eql(u8, arg, "--cache-root")) { opt_cache_root = args_it.next() orelse fatal("expected parameter after --cache-root", .{}); } else { fatal("unrecognized option: '{s}'", .{arg}); } } else { fatal("unexpected positional argument: '{s}'", .{arg}); } } const input_path = opt_input orelse fatal("missing input file (-i)", .{}); const output_path = opt_output orelse fatal("missing output file (-o)", .{}); const zig_path = opt_zig orelse fatal("missing zig compiler path (--zig)", .{}); const cache_root = opt_cache_root orelse fatal("missing cache root path (--cache-root)", .{}); const source_bytes = try fs.cwd().readFileAlloc(arena, input_path, std.math.maxInt(u32)); const code = try parseManifest(arena, source_bytes); const source = stripManifest(source_bytes); const tmp_dir_path = try std.fmt.allocPrint(arena, "{s}/tmp/{x}", .{ cache_root, std.crypto.random.int(u64), }); fs.cwd().makePath(tmp_dir_path) catch |err| fatal("unable to create tmp dir '{s}': {s}", .{ tmp_dir_path, @errorName(err) }); defer fs.cwd().deleteTree(tmp_dir_path) catch |err| std.log.err("unable to delete '{s}': {s}", .{ tmp_dir_path, @errorName(err), }); var out_file = try fs.cwd().createFile(output_path, .{}); defer out_file.close(); var bw = std.io.bufferedWriter(out_file.writer()); const out = bw.writer(); try printSourceBlock(arena, out, source, fs.path.basename(input_path)); try printOutput(arena, out, code, input_path, zig_path, opt_zig_lib_dir, tmp_dir_path); try bw.flush(); } fn printOutput( arena: Allocator, out: anytype, code: Code, input_path: []const u8, zig_exe: []const u8, opt_zig_lib_dir: ?[]const u8, tmp_dir_path: []const u8, ) !void { var env_map = try process.getEnvMap(arena); try env_map.put("CLICOLOR_FORCE", "1"); const host = try std.zig.system.resolveTargetQuery(.{}); const obj_ext = builtin.object_format.fileExt(builtin.cpu.arch); const print = std.debug.print; var shell_buffer = std.ArrayList(u8).init(arena); defer shell_buffer.deinit(); var shell_out = shell_buffer.writer(); const code_name = std.fs.path.stem(input_path); switch (code.id) { .exe => |expected_outcome| code_block: { var build_args = std.ArrayList([]const u8).init(arena); defer build_args.deinit(); try build_args.appendSlice(&[_][]const u8{ zig_exe, "build-exe", "--name", code_name, "--color", "on", input_path, }); if (opt_zig_lib_dir) |zig_lib_dir| { try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } try shell_out.print("$ zig build-exe {s}.zig ", .{code_name}); switch (code.mode) { .Debug => {}, else => { try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } for (code.link_objects) |link_object| { const name_with_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ link_object, obj_ext }); try build_args.append(name_with_ext); try shell_out.print("{s} ", .{name_with_ext}); } if (code.link_libc) { try build_args.append("-lc"); try shell_out.print("-lc ", .{}); } if (code.target_str) |triple| { try build_args.appendSlice(&[_][]const u8{ "-target", triple }); try shell_out.print("-target {s} ", .{triple}); } if (code.verbose_cimport) { try build_args.append("--verbose-cimport"); try shell_out.print("--verbose-cimport ", .{}); } for (code.additional_options) |option| { try build_args.append(option); try shell_out.print("{s} ", .{option}); } try shell_out.print("\n", .{}); if (expected_outcome == .build_fail) { const result = try process.Child.run(.{ .allocator = arena, .argv = build_args.items, .cwd = tmp_dir_path, .env_map = &env_map, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code == 0) { print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr}); dumpArgs(build_args.items); fatal("example incorrectly compiled", .{}); } }, else => { print("{s}\nThe following command crashed:\n", .{result.stderr}); dumpArgs(build_args.items); fatal("example compile crashed", .{}); }, } const escaped_stderr = try escapeHtml(arena, result.stderr); const colored_stderr = try termColor(arena, escaped_stderr); try shell_out.writeAll(colored_stderr); break :code_block; } const exec_result = run(arena, &env_map, tmp_dir_path, build_args.items) catch fatal("example failed to compile", .{}); if (code.verbose_cimport) { const escaped_build_stderr = try escapeHtml(arena, exec_result.stderr); try shell_out.writeAll(escaped_build_stderr); } if (code.target_str) |triple| { if (mem.startsWith(u8, triple, "wasm32") or mem.startsWith(u8, triple, "riscv64-linux") or (mem.startsWith(u8, triple, "x86_64-linux") and builtin.os.tag != .linux or builtin.cpu.arch != .x86_64)) { // skip execution break :code_block; } } const target_query = try std.Target.Query.parse(.{ .arch_os_abi = code.target_str orelse "native", }); const target = try std.zig.system.resolveTargetQuery(target_query); const path_to_exe = try std.fmt.allocPrint(arena, "./{s}{s}", .{ code_name, target.exeFileExt(), }); const run_args = &[_][]const u8{path_to_exe}; var exited_with_signal = false; const result = if (expected_outcome == .fail) blk: { const result = try process.Child.run(.{ .allocator = arena, .argv = run_args, .env_map = &env_map, .cwd = tmp_dir_path, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code == 0) { print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr}); dumpArgs(run_args); fatal("example incorrectly compiled", .{}); } }, .Signal => exited_with_signal = true, else => {}, } break :blk result; } else blk: { break :blk run(arena, &env_map, tmp_dir_path, run_args) catch fatal("example crashed", .{}); }; const escaped_stderr = try escapeHtml(arena, result.stderr); const escaped_stdout = try escapeHtml(arena, result.stdout); const colored_stderr = try termColor(arena, escaped_stderr); const colored_stdout = try termColor(arena, escaped_stdout); try shell_out.print("$ ./{s}\n{s}{s}", .{ code_name, colored_stdout, colored_stderr }); if (exited_with_signal) { try shell_out.print("(process terminated by signal)", .{}); } try shell_out.writeAll("\n"); }, .@"test" => { var test_args = std.ArrayList([]const u8).init(arena); defer test_args.deinit(); try test_args.appendSlice(&[_][]const u8{ zig_exe, "test", input_path, }); if (opt_zig_lib_dir) |zig_lib_dir| { try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } try shell_out.print("$ zig test {s}.zig ", .{code_name}); switch (code.mode) { .Debug => {}, else => { try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode), }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } if (code.link_libc) { try test_args.append("-lc"); try shell_out.print("-lc ", .{}); } if (code.target_str) |triple| { try test_args.appendSlice(&[_][]const u8{ "-target", triple }); try shell_out.print("-target {s} ", .{triple}); const target_query = try std.Target.Query.parse(.{ .arch_os_abi = triple, }); const target = try std.zig.system.resolveTargetQuery( target_query, ); switch (getExternalExecutor(host, &target, .{ .link_libc = code.link_libc, })) { .native => {}, else => { try test_args.appendSlice(&[_][]const u8{"--test-no-exec"}); try shell_out.writeAll("--test-no-exec"); }, } } const result = run(arena, &env_map, null, test_args.items) catch fatal("test failed", .{}); const escaped_stderr = try escapeHtml(arena, result.stderr); const escaped_stdout = try escapeHtml(arena, result.stdout); try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout }); }, .test_error => |error_match| { var test_args = std.ArrayList([]const u8).init(arena); defer test_args.deinit(); try test_args.appendSlice(&[_][]const u8{ zig_exe, "test", "--color", "on", input_path, }); if (opt_zig_lib_dir) |zig_lib_dir| { try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } try shell_out.print("$ zig test {s}.zig ", .{code_name}); switch (code.mode) { .Debug => {}, else => { try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } if (code.link_libc) { try test_args.append("-lc"); try shell_out.print("-lc ", .{}); } const result = try process.Child.run(.{ .allocator = arena, .argv = test_args.items, .env_map = &env_map, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code == 0) { print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr}); dumpArgs(test_args.items); fatal("example incorrectly compiled", .{}); } }, else => { print("{s}\nThe following command crashed:\n", .{result.stderr}); dumpArgs(test_args.items); fatal("example compile crashed", .{}); }, } if (mem.indexOf(u8, result.stderr, error_match) == null) { print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match }); fatal("example did not have expected compile error", .{}); } const escaped_stderr = try escapeHtml(arena, result.stderr); const colored_stderr = try termColor(arena, escaped_stderr); try shell_out.print("\n{s}\n", .{colored_stderr}); }, .test_safety => |error_match| { var test_args = std.ArrayList([]const u8).init(arena); defer test_args.deinit(); try test_args.appendSlice(&[_][]const u8{ zig_exe, "test", input_path, }); if (opt_zig_lib_dir) |zig_lib_dir| { try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } var mode_arg: []const u8 = ""; switch (code.mode) { .Debug => {}, .ReleaseSafe => { try test_args.append("-OReleaseSafe"); mode_arg = "-OReleaseSafe"; }, .ReleaseFast => { try test_args.append("-OReleaseFast"); mode_arg = "-OReleaseFast"; }, .ReleaseSmall => { try test_args.append("-OReleaseSmall"); mode_arg = "-OReleaseSmall"; }, } const result = try process.Child.run(.{ .allocator = arena, .argv = test_args.items, .env_map = &env_map, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code == 0) { print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr}); dumpArgs(test_args.items); fatal("example test incorrectly succeeded", .{}); } }, else => { print("{s}\nThe following command crashed:\n", .{result.stderr}); dumpArgs(test_args.items); fatal("example compile crashed", .{}); }, } if (mem.indexOf(u8, result.stderr, error_match) == null) { print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match }); fatal("example did not have expected runtime safety error message", .{}); } const escaped_stderr = try escapeHtml(arena, result.stderr); const colored_stderr = try termColor(arena, escaped_stderr); try shell_out.print("$ zig test {s}.zig {s}\n{s}\n", .{ code_name, mode_arg, colored_stderr, }); }, .obj => |maybe_error_match| { const name_plus_obj_ext = try std.fmt.allocPrint(arena, "{s}{s}", .{ code_name, obj_ext }); var build_args = std.ArrayList([]const u8).init(arena); defer build_args.deinit(); try build_args.appendSlice(&[_][]const u8{ zig_exe, "build-obj", "--color", "on", "--name", code_name, input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}{c}{s}", .{ tmp_dir_path, fs.path.sep, name_plus_obj_ext, }), }); if (opt_zig_lib_dir) |zig_lib_dir| { try build_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } try shell_out.print("$ zig build-obj {s}.zig ", .{code_name}); switch (code.mode) { .Debug => {}, else => { try build_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } if (code.target_str) |triple| { try build_args.appendSlice(&[_][]const u8{ "-target", triple }); try shell_out.print("-target {s} ", .{triple}); } for (code.additional_options) |option| { try build_args.append(option); try shell_out.print("{s} ", .{option}); } if (maybe_error_match) |error_match| { const result = try process.Child.run(.{ .allocator = arena, .argv = build_args.items, .env_map = &env_map, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code == 0) { print("{s}\nThe following command incorrectly succeeded:\n", .{result.stderr}); dumpArgs(build_args.items); fatal("example build incorrectly succeeded", .{}); } }, else => { print("{s}\nThe following command crashed:\n", .{result.stderr}); dumpArgs(build_args.items); fatal("example compile crashed", .{}); }, } if (mem.indexOf(u8, result.stderr, error_match) == null) { print("{s}\nExpected to find '{s}' in stderr\n", .{ result.stderr, error_match }); fatal("example did not have expected compile error message", .{}); } const escaped_stderr = try escapeHtml(arena, result.stderr); const colored_stderr = try termColor(arena, escaped_stderr); try shell_out.print("\n{s} ", .{colored_stderr}); } else { _ = run(arena, &env_map, null, build_args.items) catch fatal("example failed to compile", .{}); } try shell_out.writeAll("\n"); }, .lib => { const bin_basename = try std.zig.binNameAlloc(arena, .{ .root_name = code_name, .target = builtin.target, .output_mode = .Lib, }); var test_args = std.ArrayList([]const u8).init(arena); defer test_args.deinit(); try test_args.appendSlice(&[_][]const u8{ zig_exe, "build-lib", input_path, try std.fmt.allocPrint(arena, "-femit-bin={s}{s}{s}", .{ tmp_dir_path, fs.path.sep_str, bin_basename, }), }); if (opt_zig_lib_dir) |zig_lib_dir| { try test_args.appendSlice(&.{ "--zig-lib-dir", zig_lib_dir }); } try shell_out.print("$ zig build-lib {s}.zig ", .{code_name}); switch (code.mode) { .Debug => {}, else => { try test_args.appendSlice(&[_][]const u8{ "-O", @tagName(code.mode) }); try shell_out.print("-O {s} ", .{@tagName(code.mode)}); }, } if (code.target_str) |triple| { try test_args.appendSlice(&[_][]const u8{ "-target", triple }); try shell_out.print("-target {s} ", .{triple}); } if (code.link_mode) |link_mode| { switch (link_mode) { .static => { try test_args.append("-static"); try shell_out.print("-static ", .{}); }, .dynamic => { try test_args.append("-dynamic"); try shell_out.print("-dynamic ", .{}); }, } } for (code.additional_options) |option| { try test_args.append(option); try shell_out.print("{s} ", .{option}); } const result = run(arena, &env_map, null, test_args.items) catch fatal("test failed", .{}); const escaped_stderr = try escapeHtml(arena, result.stderr); const escaped_stdout = try escapeHtml(arena, result.stdout); try shell_out.print("\n{s}{s}\n", .{ escaped_stderr, escaped_stdout }); }, } if (!code.just_check_syntax) { try printShell(out, shell_buffer.items, false); } } fn dumpArgs(args: []const []const u8) void { for (args) |arg| std.debug.print("{s} ", .{arg}) else std.debug.print("\n", .{}); } fn printSourceBlock(arena: Allocator, out: anytype, source_bytes: []const u8, name: []const u8) !void { try out.print("
{s}
", .{
        "zig", name,
    });
    try tokenizeAndPrint(arena, out, source_bytes);
    try out.writeAll("
"); } fn tokenizeAndPrint(arena: Allocator, out: anytype, raw_src: []const u8) !void { const src_non_terminated = mem.trim(u8, raw_src, " \r\n"); const src = try arena.dupeZ(u8, src_non_terminated); try out.writeAll(""); var tokenizer = std.zig.Tokenizer.init(src); var index: usize = 0; var next_tok_is_fn = false; while (true) { const prev_tok_was_fn = next_tok_is_fn; next_tok_is_fn = false; const token = tokenizer.next(); if (mem.indexOf(u8, src[index..token.loc.start], "//")) |comment_start_off| { // render one comment const comment_start = index + comment_start_off; const comment_end_off = mem.indexOf(u8, src[comment_start..token.loc.start], "\n"); const comment_end = if (comment_end_off) |o| comment_start + o else token.loc.start; try writeEscapedLines(out, src[index..comment_start]); try out.writeAll(""); try writeEscaped(out, src[comment_start..comment_end]); try out.writeAll(""); index = comment_end; tokenizer.index = index; continue; } try writeEscapedLines(out, src[index..token.loc.start]); switch (token.tag) { .eof => break, .keyword_addrspace, .keyword_align, .keyword_and, .keyword_asm, .keyword_async, .keyword_await, .keyword_break, .keyword_catch, .keyword_comptime, .keyword_const, .keyword_continue, .keyword_defer, .keyword_else, .keyword_enum, .keyword_errdefer, .keyword_error, .keyword_export, .keyword_extern, .keyword_for, .keyword_if, .keyword_inline, .keyword_noalias, .keyword_noinline, .keyword_nosuspend, .keyword_opaque, .keyword_or, .keyword_orelse, .keyword_packed, .keyword_anyframe, .keyword_pub, .keyword_resume, .keyword_return, .keyword_linksection, .keyword_callconv, .keyword_struct, .keyword_suspend, .keyword_switch, .keyword_test, .keyword_threadlocal, .keyword_try, .keyword_union, .keyword_unreachable, .keyword_usingnamespace, .keyword_var, .keyword_volatile, .keyword_allowzero, .keyword_while, .keyword_anytype, => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); }, .keyword_fn => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); next_tok_is_fn = true; }, .string_literal, .multiline_string_literal_line, .char_literal, => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); }, .builtin => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); }, .doc_comment, .container_doc_comment, => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); }, .identifier => { const tok_bytes = src[token.loc.start..token.loc.end]; if (mem.eql(u8, tok_bytes, "undefined") or mem.eql(u8, tok_bytes, "null") or mem.eql(u8, tok_bytes, "true") or mem.eql(u8, tok_bytes, "false")) { try out.writeAll(""); try writeEscaped(out, tok_bytes); try out.writeAll(""); } else if (prev_tok_was_fn) { try out.writeAll(""); try writeEscaped(out, tok_bytes); try out.writeAll(""); } else { const is_int = blk: { if (src[token.loc.start] != 'i' and src[token.loc.start] != 'u') break :blk false; var i = token.loc.start + 1; if (i == token.loc.end) break :blk false; while (i != token.loc.end) : (i += 1) { if (src[i] < '0' or src[i] > '9') break :blk false; } break :blk true; }; const isType = std.zig.isPrimitive; if (is_int or isType(tok_bytes)) { try out.writeAll(""); try writeEscaped(out, tok_bytes); try out.writeAll(""); } else { try writeEscaped(out, tok_bytes); } } }, .number_literal => { try out.writeAll(""); try writeEscaped(out, src[token.loc.start..token.loc.end]); try out.writeAll(""); }, .bang, .pipe, .pipe_pipe, .pipe_equal, .equal, .equal_equal, .equal_angle_bracket_right, .bang_equal, .l_paren, .r_paren, .semicolon, .percent, .percent_equal, .l_brace, .r_brace, .l_bracket, .r_bracket, .period, .period_asterisk, .ellipsis2, .ellipsis3, .caret, .caret_equal, .plus, .plus_plus, .plus_equal, .plus_percent, .plus_percent_equal, .plus_pipe, .plus_pipe_equal, .minus, .minus_equal, .minus_percent, .minus_percent_equal, .minus_pipe, .minus_pipe_equal, .asterisk, .asterisk_equal, .asterisk_asterisk, .asterisk_percent, .asterisk_percent_equal, .asterisk_pipe, .asterisk_pipe_equal, .arrow, .colon, .slash, .slash_equal, .comma, .ampersand, .ampersand_equal, .question_mark, .angle_bracket_left, .angle_bracket_left_equal, .angle_bracket_angle_bracket_left, .angle_bracket_angle_bracket_left_equal, .angle_bracket_angle_bracket_left_pipe, .angle_bracket_angle_bracket_left_pipe_equal, .angle_bracket_right, .angle_bracket_right_equal, .angle_bracket_angle_bracket_right, .angle_bracket_angle_bracket_right_equal, .tilde, => try writeEscaped(out, src[token.loc.start..token.loc.end]), .invalid, .invalid_periodasterisks => fatal("syntax error", .{}), } index = token.loc.end; } try out.writeAll(""); } fn writeEscapedLines(out: anytype, text: []const u8) !void { return writeEscaped(out, text); } const Code = struct { id: Id, mode: std.builtin.OptimizeMode, link_objects: []const []const u8, target_str: ?[]const u8, link_libc: bool, link_mode: ?std.builtin.LinkMode, disable_cache: bool, verbose_cimport: bool, just_check_syntax: bool, additional_options: []const []const u8, const Id = union(enum) { @"test", test_error: []const u8, test_safety: []const u8, exe: ExpectedOutcome, obj: ?[]const u8, lib, }; const ExpectedOutcome = enum { succeed, fail, build_fail, }; }; fn stripManifest(source_bytes: []const u8) []const u8 { const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse fatal("missing manifest comment", .{}); return source_bytes[0 .. manifest_start + 1]; } fn parseManifest(arena: Allocator, source_bytes: []const u8) !Code { const manifest_start = mem.lastIndexOf(u8, source_bytes, "\n\n// ") orelse fatal("missing manifest comment", .{}); var it = mem.tokenizeScalar(u8, source_bytes[manifest_start..], '\n'); const first_line = skipPrefix(it.next().?); var just_check_syntax = false; const id: Code.Id = if (mem.eql(u8, first_line, "syntax")) blk: { just_check_syntax = true; break :blk .{ .obj = null }; } else if (mem.eql(u8, first_line, "test")) .@"test" else if (mem.eql(u8, first_line, "lib")) .lib else if (mem.eql(u8, first_line, "obj")) .{ .obj = null } else if (mem.startsWith(u8, first_line, "test_error=")) .{ .test_error = first_line["test_error=".len..] } else if (mem.startsWith(u8, first_line, "test_safety=")) .{ .test_safety = first_line["test_safety=".len..] } else if (mem.startsWith(u8, first_line, "exe=")) .{ .exe = std.meta.stringToEnum(Code.ExpectedOutcome, first_line["exe=".len..]) orelse fatal("bad exe expected outcome in line '{s}'", .{first_line}) } else if (mem.startsWith(u8, first_line, "obj=")) .{ .obj = first_line["obj=".len..] } else fatal("unrecognized manifest id: '{s}'", .{first_line}); var mode: std.builtin.OptimizeMode = .Debug; var link_mode: ?std.builtin.LinkMode = null; var link_objects: std.ArrayListUnmanaged([]const u8) = .empty; var additional_options: std.ArrayListUnmanaged([]const u8) = .empty; var target_str: ?[]const u8 = null; var link_libc = false; var disable_cache = false; var verbose_cimport = false; while (it.next()) |prefixed_line| { const line = skipPrefix(prefixed_line); if (mem.startsWith(u8, line, "optimize=")) { mode = std.meta.stringToEnum(std.builtin.OptimizeMode, line["optimize=".len..]) orelse fatal("bad optimization mode line: '{s}'", .{line}); } else if (mem.startsWith(u8, line, "link_mode=")) { link_mode = std.meta.stringToEnum(std.builtin.LinkMode, line["link_mode=".len..]) orelse fatal("bad link mode line: '{s}'", .{line}); } else if (mem.startsWith(u8, line, "link_object=")) { try link_objects.append(arena, line["link_object=".len..]); } else if (mem.startsWith(u8, line, "additional_option=")) { try additional_options.append(arena, line["additional_option=".len..]); } else if (mem.startsWith(u8, line, "target=")) { target_str = line["target=".len..]; } else if (mem.eql(u8, line, "link_libc")) { link_libc = true; } else if (mem.eql(u8, line, "disable_cache")) { disable_cache = true; } else if (mem.eql(u8, line, "verbose_cimport")) { verbose_cimport = true; } else { fatal("unrecognized manifest line: {s}", .{line}); } } return .{ .id = id, .mode = mode, .additional_options = try additional_options.toOwnedSlice(arena), .link_objects = try link_objects.toOwnedSlice(arena), .target_str = target_str, .link_libc = link_libc, .link_mode = link_mode, .disable_cache = disable_cache, .verbose_cimport = verbose_cimport, .just_check_syntax = just_check_syntax, }; } fn skipPrefix(line: []const u8) []const u8 { if (!mem.startsWith(u8, line, "// ")) { fatal("line does not start with '// ': '{s}", .{line}); } return line[3..]; } fn escapeHtml(allocator: Allocator, input: []const u8) ![]u8 { var buf = std.ArrayList(u8).init(allocator); defer buf.deinit(); const out = buf.writer(); try writeEscaped(out, input); return try buf.toOwnedSlice(); } fn writeEscaped(out: anytype, input: []const u8) !void { for (input) |c| { try switch (c) { '&' => out.writeAll("&"), '<' => out.writeAll("<"), '>' => out.writeAll(">"), '"' => out.writeAll("""), else => out.writeByte(c), }; } } fn termColor(allocator: Allocator, input: []const u8) ![]u8 { // The SRG sequences generates by the Zig compiler are in the format: // ESC [ ; m // or // ESC [ m // // where // foreground-color is 31 (red), 32 (green), 36 (cyan) // n is 0 (reset), 1 (bold), 2 (dim) // // Note that 37 (white) is currently not used by the compiler. // // See std.debug.TTY.Color. const supported_sgr_colors = [_]u8{ 31, 32, 36 }; const supported_sgr_numbers = [_]u8{ 0, 1, 2 }; var buf = std.ArrayList(u8).init(allocator); defer buf.deinit(); var out = buf.writer(); var sgr_param_start_index: usize = undefined; var sgr_num: u8 = undefined; var sgr_color: u8 = undefined; var i: usize = 0; var state: enum { start, escape, lbracket, number, after_number, arg, arg_number, expect_end, } = .start; var last_new_line: usize = 0; var open_span_count: usize = 0; while (i < input.len) : (i += 1) { const c = input[i]; switch (state) { .start => switch (c) { '\x1b' => state = .escape, '\n' => { try out.writeByte(c); last_new_line = buf.items.len; }, else => try out.writeByte(c), }, .escape => switch (c) { '[' => state = .lbracket, else => return error.UnsupportedEscape, }, .lbracket => switch (c) { '0'...'9' => { sgr_param_start_index = i; state = .number; }, else => return error.UnsupportedEscape, }, .number => switch (c) { '0'...'9' => {}, else => { sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10); sgr_color = 0; state = .after_number; i -= 1; }, }, .after_number => switch (c) { ';' => state = .arg, 'D' => state = .start, 'K' => { buf.items.len = last_new_line; state = .start; }, else => { state = .expect_end; i -= 1; }, }, .arg => switch (c) { '0'...'9' => { sgr_param_start_index = i; state = .arg_number; }, else => return error.UnsupportedEscape, }, .arg_number => switch (c) { '0'...'9' => {}, else => { // Keep the sequence consistent, foreground color first. // 32;1m is equivalent to 1;32m, but the latter will // generate an incorrect HTML class without notice. sgr_color = sgr_num; if (!in(&supported_sgr_colors, sgr_color)) return error.UnsupportedForegroundColor; sgr_num = try std.fmt.parseInt(u8, input[sgr_param_start_index..i], 10); if (!in(&supported_sgr_numbers, sgr_num)) return error.UnsupportedNumber; state = .expect_end; i -= 1; }, }, .expect_end => switch (c) { 'm' => { state = .start; while (open_span_count != 0) : (open_span_count -= 1) { try out.writeAll(""); } if (sgr_num == 0) { if (sgr_color != 0) return error.UnsupportedColor; continue; } if (sgr_color != 0) { try out.print("", .{ sgr_color, sgr_num }); } else { try out.print("", .{sgr_num}); } open_span_count += 1; }, else => return error.UnsupportedEscape, }, } } return try buf.toOwnedSlice(); } // Returns true if number is in slice. fn in(slice: []const u8, number: u8) bool { return mem.indexOfScalar(u8, slice, number) != null; } fn run( allocator: Allocator, env_map: *process.EnvMap, cwd: ?[]const u8, args: []const []const u8, ) !process.Child.RunResult { const result = try process.Child.run(.{ .allocator = allocator, .argv = args, .env_map = env_map, .cwd = cwd, .max_output_bytes = max_doc_file_size, }); switch (result.term) { .Exited => |exit_code| { if (exit_code != 0) { std.debug.print("{s}\nThe following command exited with code {}:\n", .{ result.stderr, exit_code }); dumpArgs(args); return error.ChildExitError; } }, else => { std.debug.print("{s}\nThe following command crashed:\n", .{result.stderr}); dumpArgs(args); return error.ChildCrashed; }, } return result; } fn printShell(out: anytype, shell_content: []const u8, escape: bool) !void { const trimmed_shell_content = mem.trim(u8, shell_content, " \r\n"); try out.writeAll("
Shell
");
    var cmd_cont: bool = false;
    var iter = std.mem.splitScalar(u8, trimmed_shell_content, '\n');
    while (iter.next()) |orig_line| {
        const line = mem.trimRight(u8, orig_line, " \r");
        if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] != '\\') {
            try out.writeAll("$ ");
            const s = std.mem.trimLeft(u8, line[1..], " ");
            if (escape) {
                try writeEscaped(out, s);
            } else {
                try out.writeAll(s);
            }
            try out.writeAll("" ++ "\n");
        } else if (!cmd_cont and line.len > 1 and mem.eql(u8, line[0..2], "$ ") and line[line.len - 1] == '\\') {
            try out.writeAll("$ ");
            const s = std.mem.trimLeft(u8, line[1..], " ");
            if (escape) {
                try writeEscaped(out, s);
            } else {
                try out.writeAll(s);
            }
            try out.writeAll("\n");
            cmd_cont = true;
        } else if (line.len > 0 and line[line.len - 1] != '\\' and cmd_cont) {
            if (escape) {
                try writeEscaped(out, line);
            } else {
                try out.writeAll(line);
            }
            try out.writeAll("" ++ "\n");
            cmd_cont = false;
        } else {
            if (escape) {
                try writeEscaped(out, line);
            } else {
                try out.writeAll(line);
            }
            try out.writeAll("\n");
        }
    }

    try out.writeAll("
"); } test "term supported colors" { const test_allocator = testing.allocator; { const input = "A\x1b[31;1mred\x1b[0mB"; const expect = "AredB"; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { const input = "A\x1b[32;1mgreen\x1b[0mB"; const expect = "AgreenB"; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { const input = "A\x1b[36;1mcyan\x1b[0mB"; const expect = "AcyanB"; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { const input = "A\x1b[1mbold\x1b[0mB"; const expect = "AboldB"; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { const input = "A\x1b[2mdim\x1b[0mB"; const expect = "AdimB"; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } } test "term output from zig" { // Use data generated by https://github.com/perillo/zig-tty-test-data, // with zig version 0.11.0-dev.1898+36d47dd19. const test_allocator = testing.allocator; { // 1.1-with-build-progress.out const input = "Semantic Analysis [1324] \x1b[25D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLVM Emit Object... \x1b[20D\x1b[0KLLD Link... \x1b[12D\x1b[0K"; const expect = ""; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { // 2.1-with-reference-traces.out const input = "\x1b[1msrc/2.1-with-reference-traces.zig:3:7: \x1b[31;1merror: \x1b[0m\x1b[1mcannot assign to constant\n\x1b[0m x += 1;\n \x1b[32;1m~~^~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n main: src/2.1-with-reference-traces.zig:7:5\n callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m"; const expect = \\src/2.1-with-reference-traces.zig:3:7: error: cannot assign to constant \\ x += 1; \\ ~~^~~~ \\referenced by: \\ main: src/2.1-with-reference-traces.zig:7:5 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces \\ \\ ; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { // 2.2-without-reference-traces.out const input = "\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: \x1b[31;1merror: \x1b[0m\x1b[1minvalid type given to fixedBufferStream\n\x1b[0m else => @compileError(\"invalid type given to fixedBufferStream\"),\n \x1b[32;1m^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\x1b[0m\x1b[1m/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: \x1b[36;1mnote: \x1b[0m\x1b[1mcalled from here\n\x1b[0mpub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) {\n; \x1b[32;1m~~~~~^~~~~~~~~~~~~~~~~\n\x1b[0m"; const expect = \\/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:128:29: error: invalid type given to fixedBufferStream \\ else => @compileError("invalid type given to fixedBufferStream"), \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ \\/usr/local/lib/zig/lib/std/io/fixed_buffer_stream.zig:116:66: note: called from here \\pub fn fixedBufferStream(buffer: anytype) FixedBufferStream(Slice(@TypeOf(buffer))) { \\; ~~~~~^~~~~~~~~~~~~~~~~ \\ ; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { // 2.3-with-notes.out const input = "\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[31;1merror: \x1b[0m\x1b[1mexpected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat'\n\x1b[0m bar(w);\n \x1b[32;1m^\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:6:9: \x1b[36;1mnote: \x1b[0m\x1b[1mpointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp'\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:2:13: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Wat = opaque {};\n \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:1:14: \x1b[36;1mnote: \x1b[0m\x1b[1mopaque declared here\n\x1b[0mconst Derp = opaque {};\n \x1b[32;1m^~~~~~~~~\n\x1b[0m\x1b[1msrc/2.3-with-notes.zig:4:18: \x1b[36;1mnote: \x1b[0m\x1b[1mparameter type declared here\n\x1b[0mextern fn bar(d: *Derp) void;\n \x1b[32;1m^~~~~\n\x1b[0m\x1b[0m\x1b[2mreferenced by:\n main: src/2.3-with-notes.zig:10:5\n callMain: /usr/local/lib/zig/lib/std/start.zig:607:17\n remaining reference traces hidden; use '-freference-trace' to see all reference traces\n\n\x1b[0m"; const expect = \\src/2.3-with-notes.zig:6:9: error: expected type '*2.3-with-notes.Derp', found '*2.3-with-notes.Wat' \\ bar(w); \\ ^ \\src/2.3-with-notes.zig:6:9: note: pointer type child '2.3-with-notes.Wat' cannot cast into pointer type child '2.3-with-notes.Derp' \\src/2.3-with-notes.zig:2:13: note: opaque declared here \\const Wat = opaque {}; \\ ^~~~~~~~~ \\src/2.3-with-notes.zig:1:14: note: opaque declared here \\const Derp = opaque {}; \\ ^~~~~~~~~ \\src/2.3-with-notes.zig:4:18: note: parameter type declared here \\extern fn bar(d: *Derp) void; \\ ^~~~~ \\referenced by: \\ main: src/2.3-with-notes.zig:10:5 \\ callMain: /usr/local/lib/zig/lib/std/start.zig:607:17 \\ remaining reference traces hidden; use '-freference-trace' to see all reference traces \\ \\ ; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { // 3.1-with-error-return-traces.out const input = "error: Error\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:5:5\x1b[0m: \x1b[2m0x20b008 in callee (3.1-with-error-return-traces)\x1b[0m\n return error.Error;\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:9:5\x1b[0m: \x1b[2m0x20b113 in caller (3.1-with-error-return-traces)\x1b[0m\n try callee();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.1-with-error-return-traces.zig:13:5\x1b[0m: \x1b[2m0x20b153 in main (3.1-with-error-return-traces)\x1b[0m\n try caller();\n \x1b[32;1m^\x1b[0m\n"; const expect = \\error: Error \\/home/zig/src/3.1-with-error-return-traces.zig:5:5: 0x20b008 in callee (3.1-with-error-return-traces) \\ return error.Error; \\ ^ \\/home/zig/src/3.1-with-error-return-traces.zig:9:5: 0x20b113 in caller (3.1-with-error-return-traces) \\ try callee(); \\ ^ \\/home/zig/src/3.1-with-error-return-traces.zig:13:5: 0x20b153 in main (3.1-with-error-return-traces) \\ try caller(); \\ ^ \\ ; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } { // 3.2-with-stack-trace.out const input = "\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:561:19\x1b[0m: \x1b[2m0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace)\x1b[0m\n while (it.next()) |return_address| {\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/debug.zig:157:80\x1b[0m: \x1b[2m0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace)\x1b[0m\n writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| {\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:5:36\x1b[0m: \x1b[2m0x20d3b2 in foo (3.2-with-stack-trace)\x1b[0m\n std.debug.dumpCurrentStackTrace(null);\n \x1b[32;1m^\x1b[0m\n\x1b[1m/home/zig/src/3.2-with-stack-trace.zig:9:8\x1b[0m: \x1b[2m0x20b458 in main (3.2-with-stack-trace)\x1b[0m\n foo();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:607:22\x1b[0m: \x1b[2m0x20a965 in posixCallMainAndExit (3.2-with-stack-trace)\x1b[0m\n root.main();\n \x1b[32;1m^\x1b[0m\n\x1b[1m/usr/local/lib/zig/lib/std/start.zig:376:5\x1b[0m: \x1b[2m0x20a411 in _start (3.2-with-stack-trace)\x1b[0m\n @call(.never_inline, posixCallMainAndExit, .{});\n \x1b[32;1m^\x1b[0m\n"; const expect = \\/usr/local/lib/zig/lib/std/debug.zig:561:19: 0x22a107 in writeCurrentStackTrace__anon_5898 (3.2-with-stack-trace) \\ while (it.next()) |return_address| { \\ ^ \\/usr/local/lib/zig/lib/std/debug.zig:157:80: 0x20bb23 in dumpCurrentStackTrace (3.2-with-stack-trace) \\ writeCurrentStackTrace(stderr, debug_info, detectTTYConfig(io.getStdErr()), start_addr) catch |err| { \\ ^ \\/home/zig/src/3.2-with-stack-trace.zig:5:36: 0x20d3b2 in foo (3.2-with-stack-trace) \\ std.debug.dumpCurrentStackTrace(null); \\ ^ \\/home/zig/src/3.2-with-stack-trace.zig:9:8: 0x20b458 in main (3.2-with-stack-trace) \\ foo(); \\ ^ \\/usr/local/lib/zig/lib/std/start.zig:607:22: 0x20a965 in posixCallMainAndExit (3.2-with-stack-trace) \\ root.main(); \\ ^ \\/usr/local/lib/zig/lib/std/start.zig:376:5: 0x20a411 in _start (3.2-with-stack-trace) \\ @call(.never_inline, posixCallMainAndExit, .{}); \\ ^ \\ ; const result = try termColor(test_allocator, input); defer test_allocator.free(result); try testing.expectEqualSlices(u8, expect, result); } } test "printShell" { const test_allocator = std.testing.allocator; { const shell_out = \\$ zig build test.zig ; const expected = \\
Shell
$ zig build test.zig
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \\build output ; const expected = \\
Shell
$ zig build test.zig
            \\build output
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = "$ zig build test.zig\r\nbuild output\r\n"; const expected = \\
Shell
$ zig build test.zig
            \\build output
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \\build output \\$ ./test ; const expected = \\
Shell
$ zig build test.zig
            \\build output
            \\$ ./test
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \\ \\$ ./test \\output ; const expected = \\
Shell
$ zig build test.zig
            \\
            \\$ ./test
            \\output
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \\$ ./test \\output ; const expected = \\
Shell
$ zig build test.zig
            \\$ ./test
            \\output
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \ \\ --build-option \\build output \\$ ./test \\output ; const expected = \\
Shell
$ zig build test.zig \
            \\ --build-option
            \\build output
            \\$ ./test
            \\output
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { // intentional space after "--build-option1 \" const shell_out = \\$ zig build test.zig \ \\ --build-option1 \ \\ --build-option2 \\$ ./test ; const expected = \\
Shell
$ zig build test.zig \
            \\ --build-option1 \
            \\ --build-option2
            \\$ ./test
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \ \\$ ./test ; const expected = \\
Shell
$ zig build test.zig \
            \\$ ./test
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$ zig build test.zig \\$ ./test \\$1 ; const expected = \\
Shell
$ zig build test.zig
            \\$ ./test
            \\$1
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } { const shell_out = \\$zig build test.zig ; const expected = \\
Shell
$zig build test.zig
            \\
; var buffer = std.ArrayList(u8).init(test_allocator); defer buffer.deinit(); try printShell(buffer.writer(), shell_out, false); try testing.expectEqualSlices(u8, expected, buffer.items); } }