const std = @import("std"); // req: cli/002 fn usage(file: std.Io.File, io: std.Io) !void { var buffer: [1024]u8 = undefined; var writer = file.writer(io, &buffer); try writer.interface.writeAll( \\usage: lines FILE START [END] \\ \\Print a 1-based inclusive line range from FILE to stdout. \\START and END must be positive decimal integers; END defaults to START. \\No writes, no regex, no program language, no stdin prompts. \\ \\output: \\ stdout: selected file lines only, with original line order preserved \\ stderr: usage and diagnostics only \\ \\exit status: \\ 0 on success, 1 on runtime/input errors, 2 on usage errors \\ \\examples: \\ lines src/main.zig 40 80 \\ lines README.md 1 20 | grep usage \\ ); try writer.interface.flush(); } fn fail(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { var buffer: [512]u8 = undefined; var writer = std.Io.File.stderr().writer(io, &buffer); writer.interface.print(fmt, args) catch {}; writer.interface.flush() catch {}; std.process.exit(1); } fn parseLineNo(text: []const u8, name: []const u8, io: std.Io) u64 { const value = std.fmt.parseInt(u64, text, 10) catch fail(io, "lines: invalid {s}: {s}\n", .{ name, text }); if (value == 0) fail(io, "lines: {s} must be positive\n", .{name}); return value; } fn stdoutFailure(out: *std.Io.File.Writer, io: std.Io) noreturn { if (out.err) |err| switch (err) { error.BrokenPipe => std.process.exit(0), else => fail(io, "lines: stdout: {s}\n", .{@errorName(err)}), }; fail(io, "lines: stdout: WriteFailed\n", .{}); } fn writeStdout(out: *std.Io.File.Writer, io: std.Io, bytes: []const u8) void { out.interface.writeAll(bytes) catch stdoutFailure(out, io); } fn flushStdout(out: *std.Io.File.Writer, io: std.Io) void { out.interface.flush() catch stdoutFailure(out, io); } // req: cli/001 // req: robustness/002 // req: robustness/003 // req: performance/001 fn streamRange(file: std.Io.File, io: std.Io, start: u64, end: u64) void { var read_buffer: [64 * 1024]u8 = undefined; var reader_file = file.readerStreaming(io, &read_buffer); var chunk: [64 * 1024]u8 = undefined; var out_buffer: [64 * 1024]u8 = undefined; var stdout_file = std.Io.File.stdout().writerStreaming(io, &out_buffer); var line_no: u64 = 1; while (line_no <= end) { const n = reader_file.interface.readSliceShort(&chunk) catch |err| fail(io, "lines: read failed: {s}\n", .{@errorName(err)}); if (n == 0) break; var pos: usize = 0; while (pos < n and line_no <= end) { const rest = chunk[pos..n]; const line_end = if (std.mem.indexOfScalar(u8, rest, '\n')) |newline| pos + newline + 1 else n; if (line_no >= start) writeStdout(&stdout_file, io, chunk[pos..line_end]); if (line_end <= n and chunk[line_end - 1] == '\n') line_no += 1; pos = line_end; } } flushStdout(&stdout_file, io); } pub fn main(init: std.process.Init) !void { const allocator = init.gpa; var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); defer args.deinit(); _ = args.skip(); const path = args.next() orelse { try usage(std.Io.File.stderr(), init.io); std.process.exit(2); }; if (std.mem.eql(u8, path, "--help") or std.mem.eql(u8, path, "-h")) { try usage(std.Io.File.stdout(), init.io); return; } const start_text = args.next() orelse { try usage(std.Io.File.stderr(), init.io); std.process.exit(2); }; const maybe_end_text = args.next(); if (args.next() != null) { try usage(std.Io.File.stderr(), init.io); std.process.exit(2); } // req: cli/001 // req: cli/001.e1 // req: cli/003 const start = parseLineNo(start_text, "START", init.io); const end = if (maybe_end_text) |end_text| parseLineNo(end_text, "END", init.io) else start; if (end < start) fail(init.io, "lines: END must be >= START\n", .{}); const file = std.Io.Dir.cwd().openFile(init.io, path, .{ .mode = .read_only, .allow_directory = false }) catch |err| fail(init.io, "lines: {s}: {s}\n", .{ path, @errorName(err) }); defer file.close(init.io); streamRange(file, init.io, start, end); }