// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Command line front end for the zuucode library: a small `uuencode` and //! `uudecode` that reads a file or standard input and writes to standard //! output. const std = @import("std"); const Io = std.Io; const zuucode = @import("zuucode"); const usage = \\usage: zuucode [options] [file] \\ \\Encodes `file` (or standard input) to standard output, and decodes it \\again with -d. Without -d the output is a complete uuencoded file with \\`begin` and `end` lines unless --raw is given. \\ \\options: \\ -d, --decode decode instead of encode \\ -n, --name NAME name for the `begin` line (default: the input file name) \\ -m, --mode MODE octal mode for the `begin` line (default: 644) \\ --charset SET encode a zero group as `backtick` (default) or `space` \\ --crlf terminate encoded lines with CRLF instead of LF \\ --raw omit (when encoding) the `begin` and `end` lines \\ --strict reject truncated lines and a body with no terminator \\ -h, --help show this help \\ ; const Command = struct { decode: bool = false, raw: bool = false, name: ?[]const u8 = null, mode: u32 = 0o644, path: ?[]const u8 = null, encode_options: zuucode.EncodeOptions = .{}, decode_options: zuucode.DecodeOptions = .{}, }; pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; const args = try init.minimal.args.toSlice(init.arena.allocator()); var stderr_buffer: [512]u8 = undefined; var stderr_writer: Io.File.Writer = .init(.stderr(), io, &stderr_buffer); const stderr = &stderr_writer.interface; const command = parseArgs(args) catch |err| switch (err) { error.HelpRequested => { try stderr.writeAll(usage); try stderr.flush(); return; }, else => |e| { try stderr.print("zuucode: {s}\n\n{s}", .{ @errorName(e), usage }); try stderr.flush(); std.process.exit(2); }, }; run(io, gpa, command) catch |err| { try stderr.print("zuucode: {s}\n", .{@errorName(err)}); try stderr.flush(); std.process.exit(1); }; } fn run(io: Io, gpa: std.mem.Allocator, command: Command) !void { const source = try readInput(io, gpa, command.path); defer gpa.free(source); var stdout_buffer: [4096]u8 = undefined; var stdout_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer); const stdout = &stdout_writer.interface; if (command.decode) { const decoded = if (command.raw) try zuucode.decodeAlloc(gpa, source, command.decode_options) else (try zuucode.decodeFileAlloc(gpa, source, command.decode_options)).data; defer gpa.free(decoded); try stdout.writeAll(decoded); } else if (command.raw) { try zuucode.encodeStream(stdout, source, command.encode_options); } else { const header: zuucode.Header = .{ .name = command.name orelse command.path orelse "-", .mode = command.mode, }; try zuucode.encodeFile(stdout, header, source, command.encode_options); } try stdout.flush(); } fn parseArgs(args: []const [:0]const u8) !Command { var command: Command = .{}; var index: usize = 1; while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { return error.HelpRequested; } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--decode")) { command.decode = true; } else if (std.mem.eql(u8, arg, "--raw")) { command.raw = true; } else if (std.mem.eql(u8, arg, "--crlf")) { command.encode_options.line_ending = .crlf; } else if (std.mem.eql(u8, arg, "--strict")) { command.decode_options = .{ .pad_truncated_lines = false, .require_terminator = true }; } else if (std.mem.eql(u8, arg, "-n") or std.mem.eql(u8, arg, "--name")) { index += 1; if (index == args.len) return error.MissingArgument; command.name = args[index]; } else if (std.mem.eql(u8, arg, "-m") or std.mem.eql(u8, arg, "--mode")) { index += 1; if (index == args.len) return error.MissingArgument; command.mode = std.fmt.parseInt(u32, args[index], 8) catch return error.InvalidMode; } else if (std.mem.eql(u8, arg, "--charset")) { index += 1; if (index == args.len) return error.MissingArgument; command.encode_options.charset = std.meta.stringToEnum(zuucode.Charset, args[index]) orelse return error.InvalidCharset; } else if (arg.len > 1 and arg[0] == '-' and !std.mem.eql(u8, arg, "-")) { return error.UnknownOption; } else if (command.path != null) { return error.TooManyInputs; } else if (!std.mem.eql(u8, arg, "-")) { command.path = arg; } } return command; } fn readInput(io: Io, gpa: std.mem.Allocator, path: ?[]const u8) ![]u8 { var buffer: [8192]u8 = undefined; if (path) |p| { const file = try Io.Dir.cwd().openFile(io, p, .{}); defer file.close(io); var file_reader: Io.File.Reader = file.reader(io, &buffer); return file_reader.interface.allocRemaining(gpa, .unlimited); } var stdin_reader: Io.File.Reader = .initStreaming(.stdin(), io, &buffer); return stdin_reader.interface.allocRemaining(gpa, .unlimited); } test "the CLI round trips through the library" { const gpa = std.testing.allocator; const payload = "encode me, then decode me back again"; const text = try zuucode.encodeFileAlloc(gpa, .{ .name = "round-trip" }, payload, .{}); defer gpa.free(text); const file = try zuucode.decodeFileAlloc(gpa, text, .{ .require_terminator = true }); defer gpa.free(file.data); try std.testing.expectEqualStrings(payload, file.data); } test "argument parsing" { const parsed = try parseArgs(&.{ "zuucode", "-d", "--strict", "input.uu" }); try std.testing.expect(parsed.decode); try std.testing.expect(parsed.decode_options.require_terminator); try std.testing.expectEqualStrings("input.uu", parsed.path.?); const encoding = try parseArgs(&.{ "zuucode", "-m", "600", "--charset", "space", "--crlf" }); try std.testing.expectEqual(@as(u32, 0o600), encoding.mode); try std.testing.expectEqual(zuucode.Charset.space, encoding.encode_options.charset); try std.testing.expectEqual(zuucode.LineEnding.crlf, encoding.encode_options.line_ending); try std.testing.expectError(error.UnknownOption, parseArgs(&.{ "zuucode", "--nope" })); try std.testing.expectError(error.MissingArgument, parseArgs(&.{ "zuucode", "-n" })); try std.testing.expectError(error.InvalidCharset, parseArgs(&.{ "zuucode", "--charset", "b64" })); }