Something went wrong. Try again.
ls but with io_uring
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012const std = @import("std");const builtin = @import("builtin");const ourio = @import("ourio");const zeit = @import("zeit");const natord = @import("natord.zig");const Icon = @import("icon.zig");const build_options = @import("build_options");
const posix = std.posix;const Io = std.Io;
const usage = \\Usage: \\ lsr [options] [path...] \\ \\ --help Print this message and exit \\ --version Print the version string \\ \\DISPLAY OPTIONS \\ -1, --oneline Print entries one per line \\ -a, --all Show files that start with a dot (ASCII 0x2E) \\ -A, --almost-all Like --all, but skips implicit "." and ".." directories \\ -C, --columns Print the output in columns \\ --color=WHEN When to use colors (always, auto, never) \\ --group-directories-first Print all directories before printing regular files \\ --hyperlinks=WHEN When to use OSC 8 hyperlinks (always, auto, never) \\ --icons=WHEN When to display icons (always, auto, never) \\ -l, --long Display extended file metadata \\ -r, --reverse Reverse the sort order \\ -t, --time Sort the entries by modification time, most recent first \\ --tree[=DEPTH] Display entries in a tree format (optional limit depth) \\;
const queue_size = 2048;
/// Slots held back from the initial directory stat batch: one for the directory close and one/// for each of the three metadata operations (/etc/localtime, /etc/passwd, /etc/group).const reserved_slots = 4;
pub const Options = struct { all: bool = false, @"almost-all": bool = false, color: When = .auto, shortview: enum { columns, oneline } = .oneline, @"group-directories-first": bool = true, hyperlinks: When = .auto, icons: When = .auto, long: bool = false, sort_by_mod_time: bool = false, reverse_sort: bool = false, tree: bool = false, tree_depth: ?usize = null,
directories: std.ArrayList([:0]const u8) = .empty, file: ?[]const u8 = null,
winsize: ?posix.winsize = null, colors: Colors = .none,
const When = enum { never, auto, always, };
pub const Colors = struct { reset: []const u8, dir: []const u8, executable: []const u8, symlink: []const u8, symlink_target: []const u8, symlink_missing: []const u8,
pub const none: Colors = .{ .reset = "", .dir = "", .executable = "", .symlink = "", .symlink_target = "", .symlink_missing = "", };
pub const default: Colors = .{ .reset = _reset, .dir = bold ++ blue, .executable = bold ++ green, .symlink = bold ++ purple, .symlink_target = bold ++ cyan, .symlink_missing = bold ++ red, };
pub const _reset = "\x1b[m"; pub const red = "\x1b[31m"; pub const green = "\x1b[32m"; pub const yellow = "\x1b[33m"; pub const blue = "\x1b[34m"; pub const purple = "\x1b[35m"; pub const cyan = "\x1b[36m"; pub const fg = "\x1b[37m";
pub const bold = "\x1b[1m"; };
fn useColor(self: Options) bool { switch (self.color) { .never => return false, .always => return true, .auto => return self.isatty(), } }
fn useIcons(self: Options) bool { switch (self.icons) { .never => return false, .always => return true, .auto => return self.isatty(), } }
fn useHyperlinks(self: Options) bool { switch (self.hyperlinks) { .never => return false, .always => return true, .auto => return self.isatty(), } }
fn showDotfiles(self: Options) bool { return self.@"almost-all" or self.all; }
fn isatty(self: Options) bool { return self.winsize != null; }};
fn dupeZ(allocator: std.mem.Allocator, src: []const u8) ![:0]u8 { const dst = try allocator.allocSentinel(u8, src.len, 0); @memcpy(dst, src); return dst;}
pub fn main(init: std.process.Init) !void { var arena = std.heap.ArenaAllocator.init(init.gpa); defer arena.deinit();
var bfa_buf: [1 << 20]u8 = undefined; var bfa: std.heap.BufferFirstAllocator = .init(&bfa_buf, arena.allocator()); const allocator = bfa.allocator();
const stdout_file = Io.File.stdout();
var stdout_buffer: [4096]u8 = undefined; var stdout_writer = stdout_file.writer(init.io, &stdout_buffer); const stdout = &stdout_writer.interface;
var stderr_writer = Io.File.stderr().writer(init.io, &.{}); const stderr = &stderr_writer.interface; var cmd: Command = .{ .arena = allocator, .std_io = init.io, .stderr = stderr };
cmd.opts.winsize = getWinsize(Io.File.stdout().handle);
cmd.opts.shortview = if (cmd.opts.isatty()) .columns else .oneline;
var args = std.process.Args.Iterator.init(init.minimal.args); // skip binary _ = args.next(); while (args.next()) |arg| { switch (optKind(arg)) { .short => { const str = arg[1..]; for (str) |b| { switch (b) { '1' => cmd.opts.shortview = .oneline, 'A' => cmd.opts.@"almost-all" = true, 'C' => cmd.opts.shortview = .columns, 'a' => cmd.opts.all = true, 'h' => {}, // human-readable: present for compatibility 'l' => cmd.opts.long = true, 'r' => cmd.opts.reverse_sort = true, 't' => cmd.opts.sort_by_mod_time = true, else => { try stderr.print("Invalid opt: '{c}'\n", .{b}); std.process.exit(1); }, } } }, .long => { var split = std.mem.splitScalar(u8, arg[2..], '='); const opt = split.first(); const val = split.rest(); if (eql(opt, "all")) { cmd.opts.all = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "long")) { cmd.opts.long = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "almost-all")) { cmd.opts.@"almost-all" = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "group-directories-first")) { cmd.opts.@"group-directories-first" = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "color")) { cmd.opts.color = std.meta.stringToEnum(Options.When, val) orelse { try stderr.print("Invalid color option: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "human-readable")) { // no-op: present for compatibility } else if (eql(opt, "hyperlinks")) { cmd.opts.hyperlinks = std.meta.stringToEnum(Options.When, val) orelse { try stderr.print("Invalid hyperlinks option: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "icons")) { cmd.opts.icons = std.meta.stringToEnum(Options.When, val) orelse { try stderr.print("Invalid color option: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "columns")) { const c = parseArgBool(val) orelse { try stderr.print("Invalid columns option: '{s}'\n", .{val}); std.process.exit(1); }; cmd.opts.shortview = if (c) .columns else .oneline; } else if (eql(opt, "oneline")) { const o = parseArgBool(val) orelse { try stderr.print("Invalid oneline option: '{s}'\n", .{val}); std.process.exit(1); }; cmd.opts.shortview = if (o) .oneline else .columns; } else if (eql(opt, "time")) { cmd.opts.sort_by_mod_time = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "reverse")) { cmd.opts.reverse_sort = parseArgBool(val) orelse { try stderr.print("Invalid boolean: '{s}'\n", .{val}); std.process.exit(1); }; } else if (eql(opt, "tree")) { if (val.len == 0) { cmd.opts.tree = true; cmd.opts.tree_depth = null; // unlimited depth } else { cmd.opts.tree = true; cmd.opts.tree_depth = std.fmt.parseInt(usize, val, 10) catch { try stderr.print("Invalid tree depth: '{s}'\n", .{val}); std.process.exit(1); }; } } else if (eql(opt, "help")) { try stdout.writeAll(usage); try stdout.flush(); return; } else if (eql(opt, "version")) { try stdout.print("lsr {s}\r\nzig {s}\r\n", .{ build_options.version, builtin.zig_version_string }); try stdout.flush(); return; } else { try stderr.print("Invalid opt: '{s}'\n", .{opt}); std.process.exit(1); } }, .positional => { try cmd.opts.directories.append(allocator, arg); }, } }
if (cmd.opts.useColor()) { cmd.opts.colors = .default; }
if (cmd.opts.directories.items.len == 0) { try cmd.opts.directories.append(allocator, "."); }
var ring: ourio.Ring = try .init(allocator, queue_size); defer ring.deinit();
// Pre-categorize positional args via lstat so that file args and directory args // can be handled differently: file args print together as a single block (matching // the convention of `ls`/`lsd`), while directory args list their contents. const arg_infos = try allocator.alloc(ArgInfo, cmd.opts.directories.items.len); for (cmd.opts.directories.items, arg_infos) |path, *info| { info.* = .{ .path = path }; } for (arg_infos) |*info| { _ = try ring.lstat(info.path, &info.statx, .{ .ptr = info, .cb = onArgLstat, .msg = @backingInt(Msg.arg_lstat), }); } try ring.run(.until_done);
var file_args: std.ArrayList(*ArgInfo) = .empty; var dir_args: std.ArrayList([:0]const u8) = .empty;
for (arg_infos) |*info| { if (info.err) |err| { switch (err) { error.FileNotFound => try stderr.print("cannot access '{s}': No such file or directory\n", .{info.path}), error.AccessDenied => try stderr.print("cannot access '{s}': Permission denied\n", .{info.path}), else => try stderr.print("cannot access '{s}': {s}\n", .{ info.path, @errorName(err) }), } continue; } if ((info.statx.mode & posix.S.IFMT) == posix.S.IFDIR) { try dir_args.append(allocator, info.path); } else { try file_args.append(allocator, info); } } try stderr.flush();
const has_files_section = file_args.items.len > 0; const print_headers = dir_args.items.len > 1 or (dir_args.items.len > 0 and has_files_section);
// Load /etc/localtime, /etc/passwd, /etc/group once for the whole run when --long is set. if (cmd.opts.long and (has_files_section or dir_args.items.len > 0)) { _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ .ptr = &cmd, .cb = onCompletion, .msg = @backingInt(Msg.localtime), }); _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ .ptr = &cmd, .cb = onCompletion, .msg = @backingInt(Msg.passwd), }); _ = try ring.open("/etc/group", .{ .CLOEXEC = true }, 0, .{ .ptr = &cmd, .cb = onCompletion, .msg = @backingInt(Msg.group), }); try ring.run(.until_done); }
var printed_any = false;
// Phase 1: print all file args as a single combined block. if (has_files_section) { const entries = try allocator.alloc(Entry, file_args.items.len); for (file_args.items, entries) |info, *entry| { entry.* = .{ .name = info.path, .kind = kindFromMode(info.statx.mode), .statx = info.statx, }; }
for (entries) |*entry| { if (entry.kind == .sym_link) { var buf: [Io.Dir.max_path_bytes]u8 = undefined; const n = Io.Dir.cwd().readLink(cmd.std_io, entry.name, &buf) catch 0; if (n > 0) { const link = buf[0..n]; const symlink: Symlink = .{ .name = try dupeZ(allocator, link) }; try cmd.symlinks.put(allocator, entry.name, symlink); } } }
std.sort.pdq(Entry, entries, cmd.opts, Entry.lessThan); if (cmd.opts.reverse_sort) std.mem.reverse(Entry, entries);
cmd.entries = entries; cmd.current_directory = ""; cmd.opts.file = null;
if (cmd.opts.tree) { try printTree(cmd, stdout); } else if (cmd.opts.long) { try printLong(&cmd, stdout); } else switch (cmd.opts.shortview) { .columns => try printShortColumns(cmd, stdout), .oneline => try printShortOnePerLine(cmd, stdout), } printed_any = true; }
// Phase 2: process each directory arg. for (dir_args.items, 0..) |directory, dir_idx| { cmd.entries = &.{}; cmd.entry_idx = 0; cmd.symlinks.clearRetainingCapacity(); cmd.opts.file = null; cmd.current_directory = directory;
_ = try ring.open(directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ .ptr = &cmd, .cb = onCompletion, .msg = @backingInt(Msg.cwd), });
try ring.run(.until_done);
if (cmd.entries.len == 0) { if (print_headers and dir_idx < dir_args.items.len - 1) { try stdout.writeAll("\r\n"); } continue; }
std.sort.pdq(Entry, cmd.entries, cmd.opts, Entry.lessThan);
if (cmd.opts.reverse_sort) { std.mem.reverse(Entry, cmd.entries); }
if (print_headers and !cmd.opts.tree) { if (printed_any) try stdout.writeAll("\r\n"); try stdout.print("{s}:\r\n", .{directory}); }
if (cmd.opts.tree) { if (printed_any) try stdout.writeAll("\r\n"); try printTree(cmd, stdout); } else if (cmd.opts.long) { try printLong(&cmd, stdout); } else switch (cmd.opts.shortview) { .columns => try printShortColumns(cmd, stdout), .oneline => try printShortOnePerLine(cmd, stdout), } printed_any = true; } try stdout.flush();}
fn printShortColumns(cmd: Command, writer: anytype) !void { const win_width = blk: { const ws = cmd.opts.winsize orelse break :blk 80; break :blk ws.col; }; if (win_width == 0) return printShortOnePerLine(cmd, writer);
const icon_width: u2 = if (cmd.opts.useIcons()) 2 else 0;
var n_cols = @min(win_width, cmd.entries.len);
const Column = struct { width: usize = 0, entries: []const Entry = &.{}, };
var columns: std.ArrayList(Column) = try .initCapacity(cmd.arena, n_cols);
outer: while (n_cols > 0) { columns.clearRetainingCapacity(); const n_rows = std.math.divCeil(usize, cmd.entries.len, n_cols) catch unreachable; const padding = (n_cols - 1) * 2;
// The number of columns that are short by one entry const short_cols = n_cols * n_rows - cmd.entries.len;
var idx: usize = 0; var line_width: usize = padding + icon_width * n_cols;
if (line_width > win_width) { n_cols -= 1; continue :outer; }
for (0..n_cols) |i| { const col_entries = if (isShortColumn(i, n_cols, short_cols)) n_rows - 1 else n_rows; const entries = cmd.entries[idx .. idx + col_entries]; idx += col_entries;
var max_width: usize = 0; for (entries) |entry| { max_width = @max(max_width, entry.name.len); }
// line_width already includes all icons and padding line_width += max_width;
const col_width = max_width + icon_width + 2;
columns.appendAssumeCapacity(.{ .entries = entries, .width = col_width, });
if (line_width > win_width) { n_cols -= 1; continue :outer; } }
break :outer; }
if (n_cols <= 1) return printShortOnePerLine(cmd, writer);
const n_rows = std.math.divCeil(usize, cmd.entries.len, columns.items.len) catch unreachable; for (0..n_rows) |row| { for (columns.items, 0..) |column, i| { if (row >= column.entries.len) continue; const entry = column.entries[row]; try printShortEntry(column.entries[row], cmd, writer);
if (i < columns.items.len - 1) { const spaces = column.width - (icon_width + entry.name.len); try writeRepeatedByte(writer, ' ', spaces); } } try writer.writeAll("\r\n"); }}
fn isShortColumn(idx: usize, n_cols: usize, n_short_cols: usize) bool { return idx + n_short_cols >= n_cols;}
fn printShortEntry(entry: Entry, cmd: Command, writer: anytype) !void { const opts = cmd.opts; const colors = opts.colors; if (opts.useIcons()) { const icon = Icon.get(entry);
if (opts.useColor()) { try writer.writeAll(icon.color); try writer.writeAll(icon.icon); try writer.writeAll(colors.reset); } else { try writer.writeAll(icon.icon); }
try writer.writeByte(' '); } switch (entry.kind) { .directory => try writer.writeAll(colors.dir), .sym_link => try writer.writeAll(colors.symlink), else => { if (entry.isExecutable()) { try writer.writeAll(colors.executable); } }, }
if (opts.useHyperlinks()) { const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); try writer.writeAll(entry.name); try writer.writeAll("\x1b]8;;\x1b\\"); try writer.writeAll(colors.reset); } else { try writer.writeAll(entry.name); try writer.writeAll(colors.reset); }}
fn printShortOneRow(cmd: Command, writer: anytype) !void { for (cmd.entries) |entry| { try printShortEntry(entry, cmd.opts, writer); try writer.writeAll(" "); } try writer.writeAll("\r\n");}
fn printShortOnePerLine(cmd: Command, writer: anytype) !void { for (cmd.entries) |entry| { try printShortEntry(entry, cmd, writer); try writer.writeAll("\r\n"); }}
fn drawTreePrefix(writer: anytype, prefix_list: []const bool, is_last: bool) !void { for (prefix_list) |is_last_at_level| { if (is_last_at_level) { try writer.writeAll(" "); } else { try writer.writeAll("│ "); } }
if (is_last) { try writer.writeAll("└── "); } else { try writer.writeAll("├── "); }}
fn writeRepeatedByte(writer: *std.Io.Writer, byte: u8, count: usize) !void { if (count == 0) return; var buf: [64]u8 = undefined; @memset(buf[0..], byte);
var remaining = count; while (remaining > 0) { const chunk = @min(buf.len, remaining); try std.Io.Writer.writeAll(writer, buf[0..chunk]); remaining -= chunk; }}
fn printTree(cmd: Command, writer: anytype) !void { const dir_name = if (std.mem.eql(u8, cmd.current_directory, ".")) blk: { var buf: [Io.Dir.max_path_bytes]u8 = undefined; const cwd_len = try std.process.currentPath(cmd.std_io, &buf); break :blk std.fs.path.basename(buf[0..cwd_len]); } else std.fs.path.basename(cmd.current_directory);
try writer.print("{s}\n", .{dir_name});
const max_depth = cmd.opts.tree_depth orelse std.math.maxInt(usize); var prefix_list: std.ArrayList(bool) = .empty;
for (cmd.entries, 0..) |entry, i| { if (std.mem.eql(u8, entry.name, ".") or std.mem.eql(u8, entry.name, "..")) continue; const is_last = i == cmd.entries.len - 1;
try drawTreePrefix(writer, prefix_list.items, is_last); try printShortEntry(entry, cmd, writer); try writer.writeAll("\r\n");
if (entry.kind == .directory and max_depth > 0) { const full_path = try std.fs.path.joinZ(cmd.arena, &.{ cmd.current_directory, entry.name });
try prefix_list.append(cmd.arena, is_last); try recurseTree(cmd, writer, full_path, &prefix_list, 1, max_depth);
_ = prefix_list.pop(); } }}
fn recurseTree(cmd: Command, writer: anytype, dir_path: [:0]const u8, prefix_list: *std.ArrayList(bool), depth: usize, max_depth: usize) !void { var dir = Io.Dir.cwd().openDir(cmd.std_io, dir_path, .{ .iterate = true }) catch { return; }; defer dir.close(cmd.std_io);
var entries: std.ArrayList(Entry) = .empty; var iter = dir.iterate();
while (try iter.next(cmd.std_io)) |dirent| { if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue;
const nameZ = try dupeZ(cmd.arena, dirent.name); try entries.append(cmd.arena, .{ .name = nameZ, .kind = dirent.kind, .statx = undefined, }); }
std.sort.pdq(Entry, entries.items, cmd.opts, Entry.lessThan);
if (cmd.opts.reverse_sort) { std.mem.reverse(Entry, entries.items); }
for (entries.items, 0..) |entry, i| { const is_last = i == entries.items.len - 1;
try drawTreePrefix(writer, prefix_list.items, is_last); try printTreeEntry(entry, cmd, writer, dir_path); try writer.writeAll("\r\n");
if (entry.kind == .directory and depth < max_depth) { const full_path = try std.fs.path.joinZ(cmd.arena, &.{ dir_path, entry.name });
try prefix_list.append(cmd.arena, is_last); try recurseTree(cmd, writer, full_path, prefix_list, depth + 1, max_depth);
_ = prefix_list.pop(); } }}
fn printTreeEntry(entry: Entry, cmd: Command, writer: anytype, dir_path: [:0]const u8) !void { const opts = cmd.opts; const colors = opts.colors;
if (opts.useIcons()) { const icon = Icon.get(entry);
if (opts.useColor()) { try writer.writeAll(icon.color); try writer.writeAll(icon.icon); try writer.writeAll(colors.reset); } else { try writer.writeAll(icon.icon); }
try writer.writeByte(' '); }
switch (entry.kind) { .directory => try writer.writeAll(colors.dir), .sym_link => try writer.writeAll(colors.symlink), else => { const full_path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); const stat_result = Io.Dir.cwd().statFile(cmd.std_io, full_path, .{}) catch null; if (stat_result) |stat| { if (stat.permissions.toMode() & (std.posix.S.IXUSR | std.posix.S.IXGRP | std.posix.S.IXOTH) != 0) { try writer.writeAll(colors.executable); } } }, }
if (opts.useHyperlinks()) { const path = try std.fs.path.join(cmd.arena, &.{ dir_path, entry.name }); try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); try writer.writeAll(entry.name); try writer.writeAll("\x1b]8;;\x1b\\"); try writer.writeAll(colors.reset); } else { try writer.writeAll(entry.name); try writer.writeAll(colors.reset); }}
fn printLong(cmd: *Command, writer: anytype) !void { const tz = cmd.tz.?; const now = zeit.instant(.{ .now = cmd.std_io }, &tz); const one_year_ago = try now.subtract(.{ .days = 365 }); const colors = cmd.opts.colors;
const longest_group, const longest_user, const longest_size, const longest_suffix = blk: { var n_group: usize = 0; var n_user: usize = 0; var n_size: usize = 0; var n_suff: usize = 0; for (cmd.entries) |entry| { const group = try cmd.getGroup(entry.statx.gid); const user = try cmd.getUser(entry.statx.uid);
var buf: [16]u8 = undefined; const size = try entry.humanReadableSize(&buf); const group_len: usize = if (group) |g| g.name.len else switch (entry.statx.gid) { 0...9 => 1, 10...99 => 2, 100...999 => 3, 1000...9999 => 4, 10000...99999 => 5, else => 6, };
const user_len: usize = if (user) |u| u.name.len else switch (entry.statx.uid) { 0...9 => 1, 10...99 => 2, 100...999 => 3, 1000...9999 => 4, 10000...99999 => 5, else => 6, };
n_group = @max(n_group, group_len); n_user = @max(n_user, user_len); n_size = @max(n_size, size.len); n_suff = @max(n_suff, entry.humanReadableSuffix().len); } break :blk .{ n_group, n_user, n_size, n_suff }; };
for (cmd.entries) |entry| { const user: User = try cmd.getUser(entry.statx.uid) orelse .{ .uid = entry.statx.uid, .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.uid}), }; const group: Group = try cmd.getGroup(entry.statx.gid) orelse .{ .gid = entry.statx.gid, .name = try std.fmt.allocPrint(cmd.arena, "{d}", .{entry.statx.gid}), }; // zeit panics on timestamps before the timezone's first transition // (Chrome bundles ship files with pre-1900 mtimes for reproducible builds). const sec = @max(entry.statx.mtime.sec, 0); const ts = @as(i128, sec) * std.time.ns_per_s; const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz }; const time = inst.time();
const mode = entry.modeStr();
try writer.writeAll(&mode); try writer.writeByte(' '); try writer.writeAll(user.name); try writeRepeatedByte(writer, ' ', longest_user - user.name.len); try writer.writeByte(' '); try writer.writeAll(group.name); try writeRepeatedByte(writer, ' ', longest_group - group.name.len); try writer.writeByte(' ');
var size_buf: [16]u8 = undefined; const size = try entry.humanReadableSize(&size_buf); const suffix = entry.humanReadableSuffix();
try writeRepeatedByte(writer, ' ', longest_size - size.len); try writer.writeAll(size); try writer.writeByte(' '); try writer.writeAll(suffix); try writeRepeatedByte(writer, ' ', longest_suffix - suffix.len); try writer.writeByte(' ');
try writer.print("{d: >2} {s} ", .{ time.day, time.month.shortName(), });
if (ts > one_year_ago.timestamp) { try writer.print("{d: >2}:{d:0>2} ", .{ time.hour, time.minute }); } else { try writer.print("{d: >5} ", .{@as(u32, @intCast(time.year))}); }
if (cmd.opts.useIcons()) { const icon = Icon.get(entry);
if (cmd.opts.useColor()) { try writer.writeAll(icon.color); try writer.writeAll(icon.icon); try writer.writeAll(colors.reset); } else { try writer.writeAll(icon.icon); }
try writer.writeByte(' '); }
switch (entry.kind) { .directory => try writer.writeAll(colors.dir), .sym_link => try writer.writeAll(colors.symlink), else => { if (entry.isExecutable()) { try writer.writeAll(colors.executable); } }, }
if (cmd.opts.useHyperlinks()) { const path = try std.fs.path.join(cmd.arena, &.{ cmd.current_directory, entry.name }); try writer.print("\x1b]8;;file://{s}\x1b\\", .{path}); try writer.writeAll(entry.name); try writer.writeAll("\x1b]8;;\x1b\\"); } else { try writer.writeAll(entry.name); } try writer.writeAll(colors.reset);
switch (entry.kind) { .sym_link => { try writer.writeAll(" -> ");
const symlink: Symlink = cmd.symlinks.get(entry.name) orelse .{ .name = "[missing]", .exists = false, };
const color = if (symlink.exists) colors.symlink_target else colors.symlink_missing;
try writer.writeAll(color); if (cmd.opts.useHyperlinks() and symlink.exists) { try writer.print("\x1b]8;;file://{s}\x1b\\", .{symlink.name}); try writer.writeAll(symlink.name); try writer.writeAll("\x1b]8;;\x1b\\"); } else { try writer.writeAll(symlink.name); } try writer.writeAll(colors.reset); },
else => {}, }
try writer.writeAll("\r\n"); }}
const Command = struct { arena: std.mem.Allocator, std_io: Io, opts: Options = .{}, entries: []Entry = &.{}, entry_idx: usize = 0, symlinks: std.StringHashMapUnmanaged(Symlink) = .empty, current_directory: [:0]const u8 = ".",
tz: ?zeit.TimeZone = null, groups: std.ArrayList(Group) = .empty, users: std.ArrayList(User) = .empty, stderr: *Io.Writer,
fn getUser(self: *Command, uid: posix.uid_t) !?User { for (self.users.items) |user| { if (user.uid == uid) return user; } if (std.c.getpwuid(uid)) |user| { if (user.name) |name| { const new_user = User{ .uid = uid, .name = std.mem.span(name), }; try self.users.append(self.arena, new_user); return new_user; } } return null; }
fn getGroup(self: *Command, gid: posix.gid_t) !?Group { for (self.groups.items) |group| { if (group.gid == gid) return group; } if (std.c.getgrgid(gid)) |group| { if (group.name) |name| { const new_group = Group{ .gid = gid, .name = std.mem.span(name), }; try self.groups.append(self.arena, new_group); return new_group; } } return null; }};
const Msg = enum(u16) { cwd, localtime, passwd, group, stat, arg_lstat,
read_localtime, read_passwd, read_group,};
const ArgInfo = struct { path: [:0]const u8, statx: ourio.Statx = undefined, err: ?anyerror = null,};
fn onArgLstat(_: *ourio.Ring, task: ourio.Task) anyerror!void { const info = task.userdataCast(ArgInfo); _ = task.result.?.statx catch |err| { info.err = err; return; };}
fn kindFromMode(mode: u16) Io.File.Kind { return switch (mode & posix.S.IFMT) { posix.S.IFDIR => .directory, posix.S.IFLNK => .sym_link, posix.S.IFREG => .file, posix.S.IFBLK => .block_device, posix.S.IFCHR => .character_device, posix.S.IFIFO => .named_pipe, posix.S.IFSOCK => .unix_domain_socket, else => .unknown, };}
const User = struct { uid: if (builtin.os.tag == .macos) i33 else posix.uid_t, name: []const u8,
fn lessThan(_: void, lhs: User, rhs: User) bool { return lhs.uid < rhs.uid; }};
const Group = struct { gid: if (builtin.os.tag == .macos) i33 else posix.gid_t, name: []const u8,
fn lessThan(_: void, lhs: Group, rhs: Group) bool { return lhs.gid < rhs.gid; }};
const MinimalEntry = struct { name: [:0]const u8, kind: Io.File.Kind,
fn lessThan(opts: Options, lhs: MinimalEntry, rhs: MinimalEntry) bool { if (opts.@"group-directories-first" and lhs.kind != rhs.kind and (lhs.kind == .directory or rhs.kind == .directory)) { return lhs.kind == .directory; }
return std.ascii.lessThanIgnoreCase(lhs.name, rhs.name); }};
const Symlink = struct { name: [:0]const u8, exists: bool = true,};
pub const Entry = struct { name: [:0]const u8, kind: Io.File.Kind, statx: ourio.Statx,
fn lessThan(opts: Options, lhs: Entry, rhs: Entry) bool { if (opts.@"group-directories-first" and lhs.kind != rhs.kind and (lhs.kind == .directory or rhs.kind == .directory)) { return lhs.kind == .directory; }
if (opts.sort_by_mod_time) { if (lhs.statx.mtime.sec == rhs.statx.mtime.sec) { return lhs.statx.mtime.nsec > rhs.statx.mtime.nsec; } return lhs.statx.mtime.sec > rhs.statx.mtime.sec; }
return natord.orderIgnoreCase(lhs.name, rhs.name) == .lt; }
fn modeStr(self: Entry) [10]u8 { var mode: [10]u8 = @splat('-'); switch (self.kind) { .block_device => mode[0] = 'b', .character_device => mode[0] = 'c', .directory => mode[0] = 'd', .named_pipe => mode[0] = 'p', .sym_link => mode[0] = 'l', else => {}, }
if (self.statx.mode & posix.S.IRUSR != 0) mode[1] = 'r'; if (self.statx.mode & posix.S.IWUSR != 0) mode[2] = 'w'; if (self.statx.mode & posix.S.IXUSR != 0) mode[3] = 'x';
if (self.statx.mode & posix.S.IRGRP != 0) mode[4] = 'r'; if (self.statx.mode & posix.S.IWGRP != 0) mode[5] = 'w'; if (self.statx.mode & posix.S.IXGRP != 0) mode[6] = 'x';
if (self.statx.mode & posix.S.IROTH != 0) mode[7] = 'r'; if (self.statx.mode & posix.S.IWOTH != 0) mode[8] = 'w'; if (self.statx.mode & posix.S.IXOTH != 0) mode[9] = 'x'; return mode; }
fn humanReadableSuffix(self: Entry) []const u8 { if (self.kind == .directory) return "-";
const buckets = [_]u64{ 1 << 40, // TB 1 << 30, // GB 1 << 20, // MB 1 << 10, // KB };
const suffixes = [_][]const u8{ "TB", "GB", "MB", "KB" };
for (buckets, suffixes) |bucket, suffix| { if (self.statx.size >= bucket) { return suffix; } } return "B"; }
fn humanReadableSize(self: Entry, out: []u8) ![]u8 { if (self.kind == .directory) return &.{};
const buckets = [_]u64{ 1 << 40, // TB 1 << 30, // GB 1 << 20, // MB 1 << 10, // KB };
for (buckets) |bucket| { if (self.statx.size >= bucket) { const size_f: f64 = @floatFromInt(self.statx.size); const bucket_f: f64 = @floatFromInt(bucket); const val = size_f / bucket_f; return std.fmt.bufPrint(out, "{d:0.1}", .{val}); } } return std.fmt.bufPrint(out, "{d}", .{self.statx.size}); }
pub fn isExecutable(self: Entry) bool { return self.statx.mode & (posix.S.IXUSR | posix.S.IXGRP | posix.S.IXOTH) != 0; }};
fn onCompletion(io: *ourio.Ring, task: ourio.Task) anyerror!void { const cmd = task.userdataCast(Command); const msg = task.msgToEnum(Msg); const result = task.result.?;
switch (msg) { .cwd => { const fd = result.open catch |err| { switch (err) { error.NotDir => { // Guard against infinite recursion if (cmd.opts.file != null) return err;
// if the user specified a file (or something that couldn't be opened as a // directory), then we open it's parent and apply a filter const dirname = std.fs.path.dirname(cmd.current_directory) orelse "."; cmd.opts.file = std.fs.path.basename(cmd.current_directory); cmd.current_directory = try dupeZ(cmd.arena, dirname); _ = try io.open( cmd.current_directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ .ptr = cmd, .cb = onCompletion, .msg = @backingInt(Msg.cwd), }, ); return; }, error.AccessDenied => { try cmd.stderr.print("cannot access '{s}': Permission denied\n", .{task.req.open.path}); try cmd.stderr.flush(); return; }, error.FileNotFound => { try cmd.stderr.print("cannot access '{s}': No such file or directory\n", .{task.req.open.path}); try cmd.stderr.flush(); return; }, else => return err, } }; // we are async, no need to defer! _ = try io.close(fd, .{}); const dir: Io.Dir = .{ .handle = fd };
if (cmd.opts.useHyperlinks()) { var buf: [Io.Dir.max_path_bytes]u8 = undefined; const resolved: []const u8 = if (builtin.os.tag == .macos) resolved: { // macOS has no /proc; use fcntl(F_GETPATH) to resolve the fd to a path. const rc = std.c.fcntl(fd, std.c.F.GETPATH, &buf); if (rc != 0) return error.FileNotFound; break :resolved std.mem.sliceTo(&buf, 0); } else resolved: { var proc_path_buf: [64]u8 = undefined; const proc_path = try std.fmt.bufPrint(&proc_path_buf, "/proc/self/fd/{d}", .{fd}); const n = try Io.Dir.readLinkAbsolute(cmd.std_io, proc_path, &buf); break :resolved buf[0..n]; }; cmd.current_directory = try dupeZ(cmd.arena, resolved); }
var temp_results: std.ArrayList(MinimalEntry) = .empty;
// Preallocate some memory try temp_results.ensureUnusedCapacity(cmd.arena, queue_size);
// zig skips "." and "..", so we manually add them if needed if (cmd.opts.all) { temp_results.appendAssumeCapacity(.{ .name = ".", .kind = .directory, }); temp_results.appendAssumeCapacity(.{ .name = "..", .kind = .directory, }); }
var iter = dir.iterate(); while (try iter.next(cmd.std_io)) |dirent| { if (!cmd.opts.showDotfiles() and std.mem.startsWith(u8, dirent.name, ".")) continue; if (cmd.opts.file) |file| { if (eql(file, dirent.name)) { const nameZ = try dupeZ(cmd.arena, dirent.name); try temp_results.append(cmd.arena, .{ .name = nameZ, .kind = dirent.kind, }); } continue; } const nameZ = try dupeZ(cmd.arena, dirent.name); try temp_results.append(cmd.arena, .{ .name = nameZ, .kind = dirent.kind, }); }
// sort the entries on the minimal struct. This has better memory locality since it is // much smaller than bringing in the ourio.Statx struct std.sort.pdq(MinimalEntry, temp_results.items, cmd.opts, MinimalEntry.lessThan);
var results: std.ArrayList(Entry) = .empty; try results.ensureUnusedCapacity(cmd.arena, temp_results.items.len); for (temp_results.items) |tmp| { results.appendAssumeCapacity(.{ .name = tmp.name, .kind = tmp.kind, .statx = undefined, }); } cmd.entries = results.items;
// Reserve space for the directory close and the three metadata operations. // The pinned ourio drops a task when a submission batch exceeds the ring size. cmd.entry_idx = @min(cmd.entries.len, queue_size - reserved_slots); for (cmd.entries[0..cmd.entry_idx]) |*entry| { const path = try std.fs.path.joinZ( cmd.arena, &.{ cmd.current_directory, entry.name }, );
if (entry.kind == .sym_link) { var buf: [Io.Dir.max_path_bytes]u8 = undefined;
// NOTE: Sadly, we can't do readlink via io_uring const n = try Io.Dir.cwd().readLink(cmd.std_io, path, &buf); const link = buf[0..n]; const symlink: Symlink = .{ .name = try dupeZ(cmd.arena, link) }; try cmd.symlinks.put(cmd.arena, entry.name, symlink); } _ = try io.stat(path, &entry.statx, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.stat), }); } },
.arg_lstat => unreachable, // handled by onArgLstat
.localtime => { const fd = try result.open;
// Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount // sufficiently more than that to make sure we do this in a single pass const buffer = try cmd.arena.alloc(u8, 8192); _ = try io.read(fd, buffer, .file, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.read_localtime), }); },
.read_localtime => { const n = try result.read; _ = try io.close(task.req.read.fd, .{}); const bytes = task.req.read.buffer[0..n]; var tz_reader = std.Io.Reader.fixed(bytes); const tz = try zeit.timezone.TZInfo.parse(cmd.arena, &tz_reader); cmd.tz = .{ .tzinfo = tz }; },
.passwd => { const fd = try result.open;
// TODO: stat this or do multiple reads. We'll never know a good bound unless we go // really big const buffer = try cmd.arena.alloc(u8, 8192 * 2); _ = try io.read(fd, buffer, .file, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.read_passwd), }); },
.read_passwd => { const n = try result.read; _ = try io.close(task.req.read.fd, .{}); const bytes = task.req.read.buffer[0..n];
var lines = std.mem.splitScalar(u8, bytes, '\n');
var line_count: usize = 0; while (lines.next()) |_| { line_count += 1; } try cmd.users.ensureUnusedCapacity(cmd.arena, line_count); lines.reset(); // <name>:<throwaway>:<uid><...garbage> while (lines.next()) |line| { if (line.len == 0) continue; if (std.mem.startsWith(u8, line, "#")) continue;
var iter = std.mem.splitScalar(u8, line, ':'); const name = iter.first(); _ = iter.next(); const uid = iter.next().?;
const user: User = .{ .name = name, .uid = try std.fmt.parseInt( if (builtin.os.tag == .macos) i33 else u32, uid, 10, ), };
cmd.users.appendAssumeCapacity(user); } std.sort.pdq(User, cmd.users.items, {}, User.lessThan); },
.group => { const fd = try result.open;
const buffer = try cmd.arena.alloc(u8, 8192); _ = try io.read(fd, buffer, .file, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.read_group), }); },
.read_group => { const n = try result.read; _ = try io.close(task.req.read.fd, .{}); const bytes = task.req.read.buffer[0..n];
var lines = std.mem.splitScalar(u8, bytes, '\n');
var line_count: usize = 0; while (lines.next()) |_| { line_count += 1; } try cmd.groups.ensureUnusedCapacity(cmd.arena, line_count); lines.reset(); // <name>:<throwaway>:<uid><...garbage> while (lines.next()) |line| { if (line.len == 0) continue; if (std.mem.startsWith(u8, line, "#")) continue;
var iter = std.mem.splitScalar(u8, line, ':'); const name = iter.first(); _ = iter.next(); const gid = iter.next().?;
const group: Group = .{ .name = name, .gid = try std.fmt.parseInt( if (builtin.os.tag == .macos) i33 else u32, gid, 10, ), };
cmd.groups.appendAssumeCapacity(group); } std.sort.pdq(Group, cmd.groups.items, {}, Group.lessThan); },
.stat => { _ = result.statx catch |err| { const entry: *Entry = @fieldParentPtr("statx", task.req.statx.result); const symlink = cmd.symlinks.getPtr(entry.name) orelse return err;
if (!symlink.exists) { // We already lstated this and found an error. Just zero out statx and move // along entry.statx = std.mem.zeroInit(ourio.Statx, entry.statx); return; }
symlink.exists = false;
_ = try io.lstat(task.req.statx.path, task.req.statx.result, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.stat), }); return; };
if (cmd.entry_idx >= cmd.entries.len) return;
const entry = &cmd.entries[cmd.entry_idx]; cmd.entry_idx += 1; const path = try std.fs.path.joinZ( cmd.arena, &.{ cmd.current_directory, entry.name }, );
if (entry.kind == .sym_link) { var buf: [Io.Dir.max_path_bytes]u8 = undefined;
// NOTE: Sadly, we can't do readlink via io_uring const n = try Io.Dir.cwd().readLink(cmd.std_io, path, &buf); const link = buf[0..n]; const symlink: Symlink = .{ .name = try dupeZ(cmd.arena, link) }; try cmd.symlinks.put(cmd.arena, entry.name, symlink); } _ = try io.stat(path, &entry.statx, .{ .cb = onCompletion, .ptr = cmd, .msg = @backingInt(Msg.stat), }); }, }}
fn eql(a: []const u8, b: []const u8) bool { return std.mem.eql(u8, a, b);}
fn parseArgBool(arg: []const u8) ?bool { if (arg.len == 0) return true;
if (std.ascii.eqlIgnoreCase(arg, "true")) return true; if (std.ascii.eqlIgnoreCase(arg, "false")) return false; if (std.ascii.eqlIgnoreCase(arg, "1")) return true; if (std.ascii.eqlIgnoreCase(arg, "0")) return false;
return null;}
/// getWinsize gets the window size of the output. Returns null if output is not a terminalfn getWinsize(fd: posix.fd_t) ?posix.winsize { var winsize: posix.winsize = .{ .row = 0, .col = 0, .xpixel = 0, .ypixel = 0, };
const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); switch (posix.errno(err)) { .SUCCESS => return winsize, else => return null, }}
fn optKind(a: []const u8) enum { short, long, positional } { if (std.mem.startsWith(u8, a, "--")) return .long; if (std.mem.startsWith(u8, a, "-")) return .short; return .positional;}
test "ref" { _ = natord;}
const testing = std.testing;
fn statxWithMode(mode: u16) ourio.Statx { var s: ourio.Statx = std.mem.zeroes(ourio.Statx); s.mode = mode; return s;}
test "eql" { try testing.expect(eql("abc", "abc")); try testing.expect(!eql("abc", "abd")); try testing.expect(!eql("abc", "ab")); try testing.expect(eql("", ""));}
test "parseArgBool" { try testing.expectEqual(@as(?bool, true), parseArgBool("")); try testing.expectEqual(@as(?bool, true), parseArgBool("true")); try testing.expectEqual(@as(?bool, true), parseArgBool("TRUE")); try testing.expectEqual(@as(?bool, true), parseArgBool("True")); try testing.expectEqual(@as(?bool, true), parseArgBool("1")); try testing.expectEqual(@as(?bool, false), parseArgBool("false")); try testing.expectEqual(@as(?bool, false), parseArgBool("FALSE")); try testing.expectEqual(@as(?bool, false), parseArgBool("0")); try testing.expectEqual(@as(?bool, null), parseArgBool("yes")); try testing.expectEqual(@as(?bool, null), parseArgBool("2"));}
test "optKind" { try testing.expectEqual(.long, optKind("--long")); try testing.expectEqual(.long, optKind("--all=true")); try testing.expectEqual(.long, optKind("--")); try testing.expectEqual(.short, optKind("-l")); try testing.expectEqual(.short, optKind("-la")); try testing.expectEqual(.positional, optKind("foo")); try testing.expectEqual(.positional, optKind("")); try testing.expectEqual(.positional, optKind("./foo"));}
test "isShortColumn" { // n_cols=4, n_short_cols=0 -> no columns are short try testing.expect(!isShortColumn(0, 4, 0)); try testing.expect(!isShortColumn(3, 4, 0));
// n_cols=4, n_short_cols=1 -> only the last column is short try testing.expect(!isShortColumn(0, 4, 1)); try testing.expect(!isShortColumn(2, 4, 1)); try testing.expect(isShortColumn(3, 4, 1));
// n_cols=4, n_short_cols=2 -> last two columns are short try testing.expect(!isShortColumn(0, 4, 2)); try testing.expect(!isShortColumn(1, 4, 2)); try testing.expect(isShortColumn(2, 4, 2)); try testing.expect(isShortColumn(3, 4, 2));}
test "kindFromMode" { try testing.expectEqual(Io.File.Kind.directory, kindFromMode(posix.S.IFDIR)); try testing.expectEqual(Io.File.Kind.sym_link, kindFromMode(posix.S.IFLNK)); try testing.expectEqual(Io.File.Kind.file, kindFromMode(posix.S.IFREG)); try testing.expectEqual(Io.File.Kind.block_device, kindFromMode(posix.S.IFBLK)); try testing.expectEqual(Io.File.Kind.character_device, kindFromMode(posix.S.IFCHR)); try testing.expectEqual(Io.File.Kind.named_pipe, kindFromMode(posix.S.IFIFO)); try testing.expectEqual(Io.File.Kind.unix_domain_socket, kindFromMode(posix.S.IFSOCK)); // Permission bits in the lower mode bits should not affect kind detection. try testing.expectEqual(Io.File.Kind.file, kindFromMode(posix.S.IFREG | 0o755)); try testing.expectEqual(Io.File.Kind.directory, kindFromMode(posix.S.IFDIR | 0o755));}
test "Entry.modeStr permissions" { const e: Entry = .{ .name = "f", .kind = .file, .statx = statxWithMode(posix.S.IFREG | 0o754), }; try testing.expectEqualStrings("-rwxr-xr--", &e.modeStr());}
test "Entry.modeStr kinds" { inline for (.{ .{ Io.File.Kind.directory, posix.S.IFDIR, "drwxr-xr-x" }, .{ Io.File.Kind.sym_link, posix.S.IFLNK, "lrwxr-xr-x" }, .{ Io.File.Kind.block_device, posix.S.IFBLK, "brwxr-xr-x" }, .{ Io.File.Kind.character_device, posix.S.IFCHR, "crwxr-xr-x" }, .{ Io.File.Kind.named_pipe, posix.S.IFIFO, "prwxr-xr-x" }, .{ Io.File.Kind.file, posix.S.IFREG, "-rwxr-xr-x" }, }) |case| { const e: Entry = .{ .name = "x", .kind = case[0], .statx = statxWithMode(case[1] | 0o755), }; try testing.expectEqualStrings(case[2], &e.modeStr()); }}
test "Entry.humanReadableSuffix" { const file: Entry = .{ .name = "f", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }; var e = file;
e.statx.size = 0; try testing.expectEqualStrings("B", e.humanReadableSuffix());
e.statx.size = 1023; try testing.expectEqualStrings("B", e.humanReadableSuffix());
e.statx.size = 1024; try testing.expectEqualStrings("KB", e.humanReadableSuffix());
e.statx.size = (1 << 20) - 1; try testing.expectEqualStrings("KB", e.humanReadableSuffix());
e.statx.size = 1 << 20; try testing.expectEqualStrings("MB", e.humanReadableSuffix());
e.statx.size = 1 << 30; try testing.expectEqualStrings("GB", e.humanReadableSuffix());
e.statx.size = 1 << 40; try testing.expectEqualStrings("TB", e.humanReadableSuffix());
// directories report "-" regardless of size const dir: Entry = .{ .name = "d", .kind = .directory, .statx = statxWithMode(posix.S.IFDIR) }; try testing.expectEqualStrings("-", dir.humanReadableSuffix());}
test "Entry.humanReadableSize" { var buf: [16]u8 = undefined;
const file: Entry = .{ .name = "f", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }; var e = file;
e.statx.size = 0; try testing.expectEqualStrings("0", try e.humanReadableSize(&buf));
e.statx.size = 512; try testing.expectEqualStrings("512", try e.humanReadableSize(&buf));
e.statx.size = 1024; try testing.expectEqualStrings("1.0", try e.humanReadableSize(&buf));
e.statx.size = 1536; // 1.5 KB try testing.expectEqualStrings("1.5", try e.humanReadableSize(&buf));
e.statx.size = 2 * (1 << 20); // 2 MB try testing.expectEqualStrings("2.0", try e.humanReadableSize(&buf));
// directories return an empty slice const dir: Entry = .{ .name = "d", .kind = .directory, .statx = statxWithMode(posix.S.IFDIR) }; try testing.expectEqualStrings("", try dir.humanReadableSize(&buf));}
test "Entry.isExecutable" { var e: Entry = .{ .name = "f", .kind = .file, .statx = statxWithMode(posix.S.IFREG | 0o644), }; try testing.expect(!e.isExecutable());
e.statx.mode = posix.S.IFREG | 0o744; // user-execute try testing.expect(e.isExecutable());
e.statx.mode = posix.S.IFREG | 0o654; // group-execute try testing.expect(e.isExecutable());
e.statx.mode = posix.S.IFREG | 0o645; // other-execute try testing.expect(e.isExecutable());}
test "Entry.lessThan group-directories-first" { const opts: Options = .{ .@"group-directories-first" = true }; const dir: Entry = .{ .name = "z", .kind = .directory, .statx = statxWithMode(posix.S.IFDIR) }; const file: Entry = .{ .name = "a", .kind = .file, .statx = statxWithMode(posix.S.IFREG) };
try testing.expect(Entry.lessThan(opts, dir, file)); try testing.expect(!Entry.lessThan(opts, file, dir));
const off: Options = .{ .@"group-directories-first" = false }; try testing.expect(!Entry.lessThan(off, dir, file)); try testing.expect(Entry.lessThan(off, file, dir));}
test "Entry.lessThan sort by mtime" { const opts: Options = .{ .@"group-directories-first" = false, .sort_by_mod_time = true };
var older = statxWithMode(posix.S.IFREG); older.mtime = .{ .sec = 100, .nsec = 0 }; var newer = statxWithMode(posix.S.IFREG); newer.mtime = .{ .sec = 200, .nsec = 0 };
const a: Entry = .{ .name = "old", .kind = .file, .statx = older }; const b: Entry = .{ .name = "new", .kind = .file, .statx = newer };
// newer entries come first try testing.expect(Entry.lessThan(opts, b, a)); try testing.expect(!Entry.lessThan(opts, a, b));
// when seconds match, higher nsec comes first var ns_a = statxWithMode(posix.S.IFREG); ns_a.mtime = .{ .sec = 100, .nsec = 1 }; var ns_b = statxWithMode(posix.S.IFREG); ns_b.mtime = .{ .sec = 100, .nsec = 2 }; const ea: Entry = .{ .name = "a", .kind = .file, .statx = ns_a }; const eb: Entry = .{ .name = "b", .kind = .file, .statx = ns_b }; try testing.expect(Entry.lessThan(opts, eb, ea)); try testing.expect(!Entry.lessThan(opts, ea, eb));}
test "Entry.lessThan natural-order by name" { const opts: Options = .{ .@"group-directories-first" = false }; const a: Entry = .{ .name = "file2", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }; const b: Entry = .{ .name = "file10", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }; // natural-order: file2 < file10 try testing.expect(Entry.lessThan(opts, a, b)); try testing.expect(!Entry.lessThan(opts, b, a));}
test "MinimalEntry.lessThan group-directories-first" { const opts: Options = .{ .@"group-directories-first" = true }; const dir: MinimalEntry = .{ .name = "z", .kind = .directory }; const file: MinimalEntry = .{ .name = "a", .kind = .file };
try testing.expect(MinimalEntry.lessThan(opts, dir, file)); try testing.expect(!MinimalEntry.lessThan(opts, file, dir));}
test "writeRepeatedByte" { var alloc: std.Io.Writer.Allocating = .init(testing.allocator); defer alloc.deinit();
try writeRepeatedByte(&alloc.writer, '-', 0); try testing.expectEqualStrings("", alloc.written());
try writeRepeatedByte(&alloc.writer, '-', 5); try testing.expectEqualStrings("-----", alloc.written());
// exercise the chunked path (buf is 64 bytes) alloc.deinit(); alloc = .init(testing.allocator); try writeRepeatedByte(&alloc.writer, 'x', 150); try testing.expectEqual(@as(usize, 150), alloc.written().len); for (alloc.written()) |b| try testing.expectEqual(@as(u8, 'x'), b);}
test "drawTreePrefix at root" { var alloc: std.Io.Writer.Allocating = .init(testing.allocator); defer alloc.deinit();
try drawTreePrefix(&alloc.writer, &.{}, false); try testing.expectEqualStrings("├── ", alloc.written());
alloc.deinit(); alloc = .init(testing.allocator); try drawTreePrefix(&alloc.writer, &.{}, true); try testing.expectEqualStrings("└── ", alloc.written());}
test "drawTreePrefix nested" { var alloc: std.Io.Writer.Allocating = .init(testing.allocator); defer alloc.deinit();
// Two ancestors: first not-last (vertical bar continues), second is-last (blank gutter). try drawTreePrefix(&alloc.writer, &.{ false, true }, false); try testing.expectEqualStrings("│ ├── ", alloc.written());
alloc.deinit(); alloc = .init(testing.allocator); try drawTreePrefix(&alloc.writer, &.{ true, false }, true); try testing.expectEqualStrings(" │ └── ", alloc.written());}
fn makeTestCommand(arena: std.mem.Allocator, stderr: *std.Io.Writer) Command { return .{ .arena = arena, .std_io = testing.io, .stderr = stderr, // explicit so test results don't depend on terminal detection .opts = .{ .icons = .never, .color = .never, .hyperlinks = .never, }, };}
test "printShortEntry emits just the name when formatting is off" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator); defer sink.deinit();
const cmd = makeTestCommand(arena.allocator(), &sink.writer); const entry: Entry = .{ .name = "hello.txt", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }; try printShortEntry(entry, cmd, &sink.writer); try testing.expectEqualStrings("hello.txt", sink.written());}
test "printShortOnePerLine emits one entry per line" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator); defer sink.deinit();
var cmd = makeTestCommand(arena.allocator(), &sink.writer); const entries = [_]Entry{ .{ .name = "a", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }, .{ .name = "b", .kind = .directory, .statx = statxWithMode(posix.S.IFDIR) }, .{ .name = "c", .kind = .file, .statx = statxWithMode(posix.S.IFREG) }, }; cmd.entries = @constCast(entries[0..]);
try printShortOnePerLine(cmd, &sink.writer); try testing.expectEqualStrings("a\r\nb\r\nc\r\n", sink.written());}
fn pushUser(cmd: *Command, uid: posix.uid_t, name: []const u8) !void { try cmd.users.append(cmd.arena, .{ .uid = @intCast(uid), .name = name });}
fn pushGroup(cmd: *Command, gid: posix.gid_t, name: []const u8) !void { try cmd.groups.append(cmd.arena, .{ .gid = @intCast(gid), .name = name });}
test "printLong basic layout" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator); defer sink.deinit();
var cmd = makeTestCommand(arena.allocator(), &sink.writer); cmd.tz = zeit.utc; cmd.opts.long = true; try pushUser(&cmd, 1000, "alice"); try pushGroup(&cmd, 1000, "staff");
var sx = statxWithMode(posix.S.IFREG | 0o644); sx.uid = 1000; sx.gid = 1000; sx.size = 1234; // Pick a fixed UTC mtime: 2020-06-15 12:34:56 UTC. sx.mtime = .{ .sec = 1592224496, .nsec = 0 };
const entries = [_]Entry{ .{ .name = "README.md", .kind = .file, .statx = sx }, }; cmd.entries = @constCast(entries[0..]);
try printLong(&cmd, &sink.writer); const out = sink.written();
// Mode + owner/group + size with KB suffix + date (15 Jun) — year-or-time // depends on `now`, so we don't pin it. We assert on the parts that are // deterministic for the chosen mtime. try testing.expect(std.mem.startsWith(u8, out, "-rw-r--r-- alice staff ")); try testing.expect(std.mem.indexOf(u8, out, " 1.2 KB ") != null); try testing.expect(std.mem.indexOf(u8, out, "15 Jun ") != null); try testing.expect(std.mem.endsWith(u8, out, "README.md\r\n"));}
test "printLong clamps pre-epoch mtime to year 1970" { // Regression test for the fix in 6cc3575 — zeit would panic on timestamps // before the timezone's first transition (Chrome bundles ship pre-1900 mtimes). var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator); defer sink.deinit();
var cmd = makeTestCommand(arena.allocator(), &sink.writer); cmd.tz = zeit.utc; cmd.opts.long = true; try pushUser(&cmd, 0, "root"); try pushGroup(&cmd, 0, "root");
var sx = statxWithMode(posix.S.IFREG | 0o644); sx.uid = 0; sx.gid = 0; sx.size = 10; sx.mtime = .{ .sec = -2208988800, .nsec = 0 }; // year 1900
const entries = [_]Entry{ .{ .name = "ancient.bin", .kind = .file, .statx = sx }, }; cmd.entries = @constCast(entries[0..]);
try printLong(&cmd, &sink.writer); const out = sink.written();
// pre-epoch mtime should be clamped to epoch (1 Jan 1970). try testing.expect(std.mem.indexOf(u8, out, " 1 Jan 1970 ") != null); try testing.expect(std.mem.endsWith(u8, out, "ancient.bin\r\n"));}
test "printLong aligns columns across entries" { var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator); defer sink.deinit();
var cmd = makeTestCommand(arena.allocator(), &sink.writer); cmd.tz = zeit.utc; cmd.opts.long = true; try pushUser(&cmd, 1, "a"); try pushUser(&cmd, 2, "longname"); try pushGroup(&cmd, 1, "g");
var s1 = statxWithMode(posix.S.IFREG | 0o644); s1.uid = 1; s1.gid = 1; s1.size = 1; s1.mtime = .{ .sec = 1592224496, .nsec = 0 };
var s2 = statxWithMode(posix.S.IFREG | 0o644); s2.uid = 2; s2.gid = 1; s2.size = 1; s2.mtime = .{ .sec = 1592224496, .nsec = 0 };
const entries = [_]Entry{ .{ .name = "one", .kind = .file, .statx = s1 }, .{ .name = "two", .kind = .file, .statx = s2 }, }; cmd.entries = @constCast(entries[0..]);
try printLong(&cmd, &sink.writer); const out = sink.written();
// Two lines, each with the user column padded to the width of "longname". var lines = std.mem.splitScalar(u8, out, '\n'); const line1 = lines.next().?; const line2 = lines.next().?; try testing.expect(std.mem.indexOf(u8, line1, "a ") != null); try testing.expect(std.mem.indexOf(u8, line2, "longname ") != null);}
test "directory stat batches leave room for metadata operations" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup();
const tmp_as_file: Io.File = .{ .handle = tmp.dir.handle, .flags = .{ .nonblocking = false } }; var path_buf: [Io.Dir.max_path_bytes]u8 = undefined; const path_len = try tmp_as_file.realPath(testing.io, &path_buf); const path = path_buf[0..path_len];
// Walk the batch boundary (and across multiple batches) so a dropped stat or a // dropped metadata read shows up as a failure rather than undefined metadata. const counts = [_]usize{ 0, 1, queue_size - 5, queue_size - 4, queue_size - 3, queue_size - 2, queue_size - 1, queue_size, queue_size + 1, 3 * queue_size + 1, };
var created: usize = 0; for (counts) |count| { while (created < count) : (created += 1) { var name_buf: [32]u8 = undefined; const name = try std.fmt.bufPrint(&name_buf, "file{d:0>5}", .{created}); switch (created % 7) { 1 => try tmp.dir.symLink(testing.io, "file00000", name, .{}), 2 => try tmp.dir.symLink(testing.io, "missing", name, .{}), else => { const file = try tmp.dir.createFile(testing.io, name, .{}); defer file.close(testing.io); try file.setLength(testing.io, created + 1); }, } }
var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); var stderr: std.Io.Writer.Allocating = .init(arena.allocator()); var cmd: Command = .{ .arena = arena.allocator(), .std_io = testing.io, .stderr = &stderr.writer, .opts = .{ .long = true, .icons = .never, .color = .never, .hyperlinks = .never, }, .current_directory = try dupeZ(arena.allocator(), path), }; var ring: ourio.Ring = try .init(testing.allocator, queue_size); defer ring.deinit();
// Force the directory callback to share a batch with all three metadata operations. inline for ( .{ "/etc/localtime", "/etc/passwd", "/etc/group" }, .{ Msg.localtime, Msg.passwd, Msg.group }, ) |metadata_path, msg| { _ = try ring.open(metadata_path, .{ .CLOEXEC = true }, 0, .{ .ptr = &cmd, .cb = onCompletion, .msg = @backingInt(msg), }); }
const dir = try Io.Dir.openDirAbsolute(testing.io, path, .{ .iterate = true }); // onCompletion queues the close and takes ownership of the directory handle. try onCompletion(&ring, .{ .userdata = &cmd, .msg = @backingInt(Msg.cwd), .req = .{ .open = .{ .path = cmd.current_directory, .flags = .{}, .mode = 0 } }, .result = .{ .open = dir.handle }, });
var initial_stats: usize = 0; var pending = ring.submission_q.head; while (pending) |task| : (pending = task.next) { if (task.req == .statx) initial_stats += 1; } try testing.expectEqual(initial_stats, cmd.entry_idx);
while (true) { try testing.expect(ring.submission_q.len() <= queue_size); try ring.run(.once); if (ring.backend.done() and ring.submission_q.empty()) break; }
try testing.expectEqual(count, cmd.entries.len); try testing.expectEqual(count, cmd.entry_idx); try testing.expect(cmd.tz != null); for (cmd.entries) |entry| { const index = try std.fmt.parseInt(usize, entry.name[4..], 10); const expected_size: u64 = switch (index % 7) { 1 => 1, 2 => "missing".len, else => index + 1, }; try testing.expectEqual(expected_size, entry.statx.size); if (entry.kind == .sym_link) { const link = cmd.symlinks.get(entry.name).?; try testing.expectEqual(index % 7 == 1, link.exists); } } }}