this repo has no description

add time/year and actually use -a/--all

+182 -25
+1 -1
build.zig
··· 19 19 exe_mod.addImport("zeit", zeit_mod); 20 20 21 21 const exe = b.addExecutable(.{ 22 - .name = "lsr", 22 + .name = "fls", 23 23 .root_module = exe_mod, 24 24 }); 25 25
+2 -2
build.zig.zon
··· 6 6 // 7 7 // It is redundant to include "zig" in this name because it is already 8 8 // within the Zig package namespace. 9 - .name = .lsr, 9 + .name = .fls, 10 10 11 11 // This is a [Semantic Version](https://semver.org/). 12 12 // In a future version of Zig it will be used for package deduplication. ··· 24 24 // original project's identity. Thus it is recommended to leave the comment 25 25 // on the following line intact, so that it shows up in code reviews that 26 26 // modify the field. 27 - .fingerprint = 0x495d173af9afcd7, // Changing this has security and trust implications. 27 + .fingerprint = 0xb35f6aad49b2a75c, // Changing this has security and trust implications. 28 28 29 29 // Tracks the earliest Zig version that the package considers to be a 30 30 // supported use case.
+179 -22
src/main.zig
··· 1 1 const std = @import("std"); 2 2 const builtin = @import("builtin"); 3 3 const ourio = @import("ourio"); 4 + const zeit = @import("zeit"); 4 5 5 - const linux = std.os.linux; 6 6 const posix = std.posix; 7 7 8 8 const Options = struct { 9 9 all: bool = false, 10 + long: bool = false, 11 + directory: [:0]const u8 = ".", 10 12 }; 11 13 12 - fn eql(a: []const u8, b: []const u8) bool { 13 - return std.mem.eql(u8, a, b); 14 - } 15 - 16 - fn optKind(a: []const u8) enum { short, long, positional } { 17 - if (std.mem.startsWith(u8, a, "--")) return .long; 18 - if (std.mem.startsWith(u8, a, "-")) return .short; 19 - return .positional; 20 - } 21 - 22 14 pub fn main() !void { 23 15 var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 24 16 const gpa, const is_debug = gpa: { ··· 37 29 var cmd: Command = .{ .arena = arena.allocator() }; 38 30 39 31 var args = std.process.args(); 32 + // skip binary 33 + _ = args.next(); 40 34 while (args.next()) |arg| { 41 35 switch (optKind(arg)) { 42 36 .short => { ··· 44 38 for (str) |b| { 45 39 switch (b) { 46 40 'a' => cmd.opts.all = true, 41 + 'l' => cmd.opts.long = true, 47 42 else => { 48 43 const w = std.io.getStdErr().writer(); 49 44 try w.print("Invalid opt: '{c}'", .{b}); ··· 56 51 const opt = arg[2..]; 57 52 if (eql(opt, "all")) 58 53 cmd.opts.all = true 54 + else if (eql(opt, "long")) 55 + cmd.opts.long = true 59 56 else { 60 57 const w = std.io.getStdErr().writer(); 61 58 try w.print("Invalid opt: '{s}'", .{opt}); 62 59 std.process.exit(1); 63 60 } 64 61 }, 65 - .positional => {}, 62 + .positional => { 63 + cmd.opts.directory = arg; 64 + }, 66 65 } 67 66 } 68 67 69 68 var ring: ourio.Ring = try .init(arena.allocator(), 64); 70 69 defer ring.deinit(); 71 70 72 - _ = try ring.open(".", .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 71 + _ = try ring.open(cmd.opts.directory, .{ .DIRECTORY = true, .CLOEXEC = true }, 0, .{ 73 72 .ptr = &cmd, 74 73 .cb = onCompletion, 75 74 .msg = @intFromEnum(Msg.cwd), 76 75 }); 77 76 78 - if (cmd.opts.all) { 77 + if (cmd.opts.long) { 79 78 // We need to also open /etc/localtime and /etc/passwd 80 79 _ = try ring.open("/etc/localtime", .{ .CLOEXEC = true }, 0, .{ 81 80 .ptr = &cmd, ··· 85 84 _ = try ring.open("/etc/passwd", .{ .CLOEXEC = true }, 0, .{ 86 85 .ptr = &cmd, 87 86 .cb = onCompletion, 88 - .msg = @intFromEnum(Msg.localtime), 87 + .msg = @intFromEnum(Msg.passwd), 89 88 }); 90 89 } 91 90 92 91 try ring.run(.until_done); 93 92 93 + std.sort.insertion(Entry, cmd.entries, cmd.opts, Entry.lessThan); 94 + 94 95 const stdout = std.io.getStdOut(); 95 96 var bw = std.io.bufferedWriter(stdout.writer()); 96 - for (cmd.entries) |entry| { 97 - try bw.writer().print("{s}\r\n", .{entry.name}); 97 + if (cmd.opts.long) { 98 + const tz = cmd.tz.?; 99 + const now = zeit.instant(.{}) catch unreachable; 100 + const one_year_ago = try now.subtract(.{ .days = 365 }); 101 + for (cmd.entries) |entry| { 102 + const user = cmd.getUser(entry.statx.uid).?; 103 + const ts = @as(i128, entry.statx.mtime.sec) * std.time.ns_per_s; 104 + const inst: zeit.Instant = .{ .timestamp = ts, .timezone = &tz }; 105 + const time = inst.time(); 106 + 107 + if (ts > one_year_ago.timestamp) { 108 + try bw.writer().print("{s} {s} {d: >2} {s} {d: >2}:{d:0>2} {s}\r\n", .{ 109 + &entry.modeStr(), 110 + user.name, 111 + time.day, 112 + time.month.shortName(), 113 + time.hour, 114 + time.minute, 115 + entry.name, 116 + }); 117 + } else { 118 + try bw.writer().print("{s} {s} {d: >2} {s} {d: >5} {s}\r\n", .{ 119 + &entry.modeStr(), 120 + user.name, 121 + time.day, 122 + time.month.shortName(), 123 + time.year, 124 + entry.name, 125 + }); 126 + } 127 + } 128 + } else { 129 + for (cmd.entries) |entry| { 130 + try bw.writer().print("{s}\r\n", .{entry.name}); 131 + } 98 132 } 99 - 100 133 try bw.flush(); 101 134 } 102 135 ··· 104 137 arena: std.mem.Allocator, 105 138 opts: Options = .{}, 106 139 entries: []Entry = &.{}, 140 + 141 + tz: ?zeit.TimeZone = null, 142 + users: std.ArrayListUnmanaged(User) = .empty, 143 + 144 + fn getUser(self: Command, uid: posix.uid_t) ?User { 145 + for (self.users.items) |user| { 146 + if (user.uid == uid) return user; 147 + } 148 + return null; 149 + } 107 150 }; 108 151 109 152 const Msg = enum(u16) { ··· 111 154 localtime, 112 155 passwd, 113 156 stat, 157 + 158 + read_localtime, 159 + read_passwd, 160 + }; 161 + 162 + const User = struct { 163 + uid: posix.uid_t, 164 + name: []const u8, 165 + 166 + fn lessThan(_: void, lhs: User, rhs: User) bool { 167 + return lhs.uid < rhs.uid; 168 + } 114 169 }; 115 170 116 171 const Entry = struct { ··· 118 173 kind: std.fs.File.Kind, 119 174 statx: ourio.Statx, 120 175 121 - fn lessThan(_: void, lhs: Entry, rhs: Entry) bool { 176 + fn lessThan(_: Options, lhs: Entry, rhs: Entry) bool { 122 177 return std.ascii.orderIgnoreCase(lhs.name, rhs.name).compare(.lt); 123 178 } 179 + 180 + fn modeStr(self: Entry) [10]u8 { 181 + var mode = [_]u8{'-'} ** 10; 182 + switch (self.kind) { 183 + .directory => mode[0] = 'd', 184 + else => {}, 185 + } 186 + 187 + if (self.statx.mode & posix.S.IRUSR != 0) mode[1] = 'r'; 188 + if (self.statx.mode & posix.S.IWUSR != 0) mode[2] = 'w'; 189 + if (self.statx.mode & posix.S.IXUSR != 0) mode[3] = 'x'; 190 + 191 + if (self.statx.mode & posix.S.IRGRP != 0) mode[4] = 'r'; 192 + if (self.statx.mode & posix.S.IWGRP != 0) mode[5] = 'w'; 193 + if (self.statx.mode & posix.S.IXGRP != 0) mode[6] = 'x'; 194 + 195 + if (self.statx.mode & posix.S.IROTH != 0) mode[7] = 'r'; 196 + if (self.statx.mode & posix.S.IWOTH != 0) mode[8] = 'w'; 197 + if (self.statx.mode & posix.S.IXOTH != 0) mode[9] = 'x'; 198 + return mode; 199 + } 124 200 }; 125 201 126 202 fn onCompletion(io: *ourio.Ring, task: ourio.Task) anyerror!void { ··· 131 207 switch (msg) { 132 208 .cwd => { 133 209 const fd = try result.open; 210 + // we are async, no need to defer! 211 + _ = try io.close(fd, .{}); 134 212 const dir: std.fs.Dir = .{ .fd = fd }; 135 213 136 214 var results: std.ArrayListUnmanaged(Entry) = .empty; 137 215 138 216 var iter = dir.iterate(); 139 217 while (try iter.next()) |dirent| { 218 + if (!cmd.opts.all and std.mem.startsWith(u8, dirent.name, ".")) continue; 140 219 const nameZ = try cmd.arena.dupeZ(u8, dirent.name); 141 220 try results.append(cmd.arena, .{ 142 221 .name = nameZ, ··· 145 224 }); 146 225 } 147 226 cmd.entries = results.items; 148 - // best effort close 149 - _ = try io.close(fd, .{}); 150 227 151 228 for (cmd.entries) |*entry| { 152 - _ = try io.stat(entry.name, &entry.statx, .{ 229 + const path = try std.fs.path.joinZ( 230 + cmd.arena, 231 + &.{ cmd.opts.directory, entry.name }, 232 + ); 233 + _ = try io.stat(path, &entry.statx, .{ 153 234 .cb = onCompletion, 154 235 .ptr = cmd, 155 236 .msg = @intFromEnum(Msg.stat), ··· 157 238 } 158 239 }, 159 240 241 + .localtime => { 242 + const fd = try result.open; 243 + 244 + // Largest TZ file on my system is Asia/Hebron at 4791 bytes. We allocate an amount 245 + // sufficiently more than that to make sure we do this in a single pass 246 + const buffer = try cmd.arena.alloc(u8, 8192); 247 + _ = try io.read(fd, buffer, .{ 248 + .cb = onCompletion, 249 + .ptr = cmd, 250 + .msg = @intFromEnum(Msg.read_localtime), 251 + }); 252 + }, 253 + 254 + .read_localtime => { 255 + const n = try result.read; 256 + _ = try io.close(task.req.read.fd, .{}); 257 + const bytes = task.req.read.buffer[0..n]; 258 + var fbs = std.io.fixedBufferStream(bytes); 259 + const tz = try zeit.timezone.TZInfo.parse(cmd.arena, fbs.reader()); 260 + cmd.tz = .{ .tzinfo = tz }; 261 + }, 262 + 263 + .passwd => { 264 + const fd = try result.open; 265 + 266 + // TODO: stat this or do multiple reads. We'll never know a good bound unless we go 267 + // really big 268 + const buffer = try cmd.arena.alloc(u8, 8192 * 2); 269 + _ = try io.read(fd, buffer, .{ 270 + .cb = onCompletion, 271 + .ptr = cmd, 272 + .msg = @intFromEnum(Msg.read_passwd), 273 + }); 274 + }, 275 + 276 + .read_passwd => { 277 + const n = try result.read; 278 + _ = try io.close(task.req.read.fd, .{}); 279 + const bytes = task.req.read.buffer[0..n]; 280 + 281 + var lines = std.mem.splitScalar(u8, bytes, '\n'); 282 + 283 + var line_count: usize = 0; 284 + while (lines.next()) |_| { 285 + line_count += 1; 286 + } 287 + try cmd.users.ensureUnusedCapacity(cmd.arena, line_count); 288 + lines.reset(); 289 + // <name>:<throwaway>:<uid><...garbage> 290 + while (lines.next()) |line| { 291 + if (line.len == 0) continue; 292 + var iter = std.mem.splitScalar(u8, line, ':'); 293 + const name = iter.first(); 294 + _ = iter.next(); 295 + const uid = iter.next().?; 296 + 297 + const user: User = .{ 298 + .name = name, 299 + .uid = try std.fmt.parseInt(u32, uid, 10), 300 + }; 301 + 302 + cmd.users.appendAssumeCapacity(user); 303 + } 304 + std.mem.sort(User, cmd.users.items, {}, User.lessThan); 305 + }, 306 + 160 307 else => {}, 161 308 } 162 309 } 310 + 311 + fn eql(a: []const u8, b: []const u8) bool { 312 + return std.mem.eql(u8, a, b); 313 + } 314 + 315 + fn optKind(a: []const u8) enum { short, long, positional } { 316 + if (std.mem.startsWith(u8, a, "--")) return .long; 317 + if (std.mem.startsWith(u8, a, "-")) return .short; 318 + return .positional; 319 + }