122 lines
4.4 KiB
Zig
122 lines
4.4 KiB
Zig
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);
|
|
}
|