const std = @import("std"); fn usage(file: std.Io.File, io: std.Io) !void { var buf: [1024]u8 = undefined; var w = file.writer(io, &buf); try w.interface.writeAll( \\usage: facts [PATH...] \\ \\Print file metadata as TSV. With no PATH args, reads one path per stdin line. \\Columns: path kind size mode mtime_ns \\ \\stdout is records only; diagnostics go to stderr. \\No filtering, sorting, deletion, exec, expression language, JSON, color, or hidden writes. \\ \\examples: \\ files src | facts | pick --header path size \\ facts README.md build.zig \\ ); try w.interface.flush(); } fn die(io: std.Io, comptime fmt: []const u8, args: anytype) noreturn { var buf: [512]u8 = undefined; var w = std.Io.File.stderr().writer(io, &buf); w.interface.print(fmt, args) catch {}; w.interface.flush() catch {}; std.process.exit(1); } fn isHelp(arg: []const u8) bool { return std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h"); } fn kindName(kind: std.Io.File.Kind) []const u8 { return switch (kind) { .file => "file", .directory => "dir", .sym_link => "symlink", .block_device => "block", .character_device => "char", .named_pipe => "fifo", .unix_domain_socket => "socket", .whiteout => "whiteout", .door => "door", .event_port => "eventport", .unknown => "unknown", }; } fn modeValue(stat: std.Io.File.Stat) u64 { if (@hasDecl(std.Io.File.Permissions, "toMode")) return @intCast(stat.permissions.toMode()); return 0; } fn printError(io: std.Io, path: []const u8, err: anyerror) !void { var err_buf: [512]u8 = undefined; var errw = std.Io.File.stderr().writer(io, &err_buf); try errw.interface.print("facts: {s}: {s}\n", .{ path, @errorName(err) }); try errw.interface.flush(); } fn unsupportedPathByte(path: []const u8) ?[]const u8 { for (path) |byte| switch (byte) { '\t' => return "tab", '\n' => return "newline", '\r' => return "carriage return", else => {}, }; return null; } fn printUnsupportedPath(io: std.Io, byte_name: []const u8) !void { var err_buf: [512]u8 = undefined; var errw = std.Io.File.stderr().writer(io, &err_buf); try errw.interface.print("facts: unsupported path: contains {s}\n", .{byte_name}); try errw.interface.flush(); } fn printFact(io: std.Io, writer: *std.Io.Writer, path: []const u8) !bool { if (unsupportedPathByte(path)) |byte_name| { try printUnsupportedPath(io, byte_name); return false; } const st = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| { try printError(io, path, err); return false; }; try writer.print("{s}\t{s}\t{}\t{o}\t{}\n", .{ path, kindName(st.kind), st.size, modeValue(st), st.mtime.nanoseconds }); return true; } fn printStdinError(io: std.Io, comptime message: []const u8) !void { var err_buf: [512]u8 = undefined; var errw = std.Io.File.stderr().writer(io, &err_buf); try errw.interface.writeAll("facts: stdin: " ++ message ++ "\n"); try errw.interface.flush(); } fn printStdinLine(io: std.Io, writer: *std.Io.Writer, raw: []const u8) !bool { if (raw.len == 0) return true; const ok = try printFact(io, writer, raw); try writer.flush(); return ok; } fn readStdinLines(io: std.Io, writer: *std.Io.Writer) !bool { var had_error = false; var read_buf: [8192]u8 = undefined; var line_buf: [std.Io.Dir.max_path_bytes]u8 = undefined; var line_len: usize = 0; var dropping_long_line = false; while (true) { const n = try std.posix.read(std.posix.STDIN_FILENO, &read_buf); if (n == 0) break; for (read_buf[0..n]) |byte| { if (byte == '\n') { if (!dropping_long_line) { if (!try printStdinLine(io, writer, line_buf[0..line_len])) had_error = true; } line_len = 0; dropping_long_line = false; continue; } if (dropping_long_line) continue; if (line_len == line_buf.len) { try printStdinError(io, "input path exceeds max path length"); had_error = true; line_len = 0; dropping_long_line = true; continue; } line_buf[line_len] = byte; line_len += 1; } } if (!dropping_long_line and line_len != 0) { if (!try printStdinLine(io, writer, line_buf[0..line_len])) had_error = true; } return had_error; } pub fn main(init: std.process.Init) !void { const allocator = init.gpa; var help_args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); defer help_args.deinit(); _ = help_args.skip(); while (help_args.next()) |arg| { if (isHelp(arg)) { try usage(std.Io.File.stdout(), init.io); return; } } var args = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); defer args.deinit(); _ = args.skip(); var out_buf: [8192]u8 = undefined; var out = std.Io.File.stdout().writer(init.io, &out_buf); try out.interface.writeAll("path\tkind\tsize\tmode\tmtime_ns\n"); var saw_arg = false; var had_error = false; while (args.next()) |arg| { saw_arg = true; if (!try printFact(init.io, &out.interface, arg)) had_error = true; } if (!saw_arg) { try out.interface.flush(); had_error = readStdinLines(init.io, &out.interface) catch |err| die(init.io, "facts: stdin: {s}\n", .{@errorName(err)}); } try out.interface.flush(); if (had_error) std.process.exit(1); }