diff --git a/ncdu.pod b/ncdu.pod index 290b5c225c8ff2f6d15832fe77ac30300324bd5f..9dce23ad43b5d5d6023cab413f4fede1c4cf6fa8 100644 --- a/ncdu.pod +++ b/ncdu.pod @@ -283,8 +283,8 @@ starting with C<#> are ignored. Example configuration file: # Disable file deletion --disable-delete - # Sort by apparent size by default - --sort apparent-size + # Exclude .git directories + --exclude .git =head1 KEYS diff --git a/src/main.zig b/src/main.zig index 27fd29245d63bc9a7aeaeb06020b264abc81624e..8c5cc4516db0b3ae9e8d8c35d53eaf649b035483 100644 --- a/src/main.zig +++ b/src/main.zig @@ -79,154 +79,84 @@ pub const config = struct { pub var state: enum { scan, browse, refresh, shell, delete } = .scan; // Simple generic argument parser, supports getopt_long() style arguments. -// T can be any type that has a 'fn next(T) ?[:0]const u8' method, e.g.: -// var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); -fn Args(T: anytype) type { - return struct { - it: T, - short: ?[:0]const u8 = null, // Remainder after a short option, e.g. -x<stuff> (which may be either more short options or an argument) - last: ?[]const u8 = null, - last_arg: ?[:0]const u8 = null, // In the case of --option=<arg> - shortbuf: [2]u8 = undefined, - argsep: bool = false, - - const Self = @This(); - const Option = struct { - opt: bool, - val: []const u8, - - fn is(self: @This(), cmp: []const u8) bool { - return self.opt and std.mem.eql(u8, self.val, cmp); - } - }; - - fn init(it: T) Self { - return Self{ .it = it }; - } - - fn shortopt(self: *Self, s: [:0]const u8) Option { - self.shortbuf[0] = '-'; - self.shortbuf[1] = s[0]; - self.short = if (s.len > 1) s[1.. :0] else null; - self.last = &self.shortbuf; - return .{ .opt = true, .val = &self.shortbuf }; - } - - /// Return the next option or positional argument. - /// 'opt' indicates whether it's an option or positional argument, - /// 'val' will be either -x, --something or the argument. - pub fn next(self: *Self) ?Option { - if (self.last_arg != null) ui.die("Option '{s}' does not expect an argument.\n", .{ self.last.? }); - if (self.short) |s| return self.shortopt(s); - const val = self.it.next() orelse return null; - if (self.argsep or val.len == 0 or val[0] != '-') return Option{ .opt = false, .val = val }; - if (val.len == 1) ui.die("Invalid option '-'.\n", .{}); - if (val.len == 2 and val[1] == '-') { - self.argsep = true; - return self.next(); - } - if (val[1] == '-') { - if (std.mem.indexOfScalar(u8, val, '=')) |sep| { - if (sep == 2) ui.die("Invalid option '{s}'.\n", .{val}); - self.last_arg = val[sep+1.. :0]; - self.last = val[0..sep]; - return Option{ .opt = true, .val = self.last.? }; - } - self.last = val; - return Option{ .opt = true, .val = val }; - } - return self.shortopt(val[1..:0]); - } - - /// Returns the argument given to the last returned option. Dies with an error if no argument is provided. - pub fn arg(self: *Self) [:0]const u8 { - if (self.short) |a| { - defer self.short = null; - return a; - } - if (self.last_arg) |a| { - defer self.last_arg = null; - return a; - } - if (self.it.next()) |o| return o; - ui.die("Option '{s}' requires an argument.\n", .{ self.last.? }); +const Args = struct { + lst: []const [:0]const u8, + short: ?[:0]const u8 = null, // Remainder after a short option, e.g. -x<stuff> (which may be either more short options or an argument) + last: ?[]const u8 = null, + last_arg: ?[:0]const u8 = null, // In the case of --option=<arg> + shortbuf: [2]u8 = undefined, + argsep: bool = false, + + const Self = @This(); + const Option = struct { + opt: bool, + val: []const u8, + + fn is(self: @This(), cmp: []const u8) bool { + return self.opt and std.mem.eql(u8, self.val, cmp); } }; -} -const ArgsFile = struct { - f: std.fs.File, - path: []const u8, - buf: std.io.BufferedReader(4096, std.fs.File.Reader), - hasarg: bool = false, - ch: u8 = 0, - - fn open(path: [:0]const u8) ?ArgsFile { - const f = std.fs.cwd().openFileZ(path, .{}) catch |e| switch (e) { - error.FileNotFound => return null, - else => ui.die("Error opening {s}: {s}\n", .{ path, ui.errorString(e) }), - }; - var self = ArgsFile{ - .f = f, - .path = path, - .buf = std.io.bufferedReader(f.reader()), - }; - self.con(); - return self; + fn init(lst: []const [:0]const u8) Self { + return Self{ .lst = lst }; } - fn con(self: *ArgsFile) void { - self.ch = self.buf.reader().readByte() catch |e| switch (e) { - error.EndOfStream => 0, - else => ui.die("Error reading from {s}: {s}\n", .{ self.path, ui.errorString(e) }), - }; + fn pop(self: *Self) ?[:0]const u8 { + if (self.lst.len == 0) return null; + defer self.lst = self.lst[1..]; + return self.lst[0]; } - // Return value /should/ be freed, but the rest of the argument parsing - // code won't bother with that. Leaking arguments isn't a big deal. - fn next(self: *ArgsFile) ?[:0]const u8 { - while (true) { - while (true) { - switch (self.ch) { - 0 => return null, - '\n', ' ', '\t', '\r' => {}, - else => break, - } - self.con(); - } - if (self.ch == '#') { - while (true) { - self.con(); - if (self.ch == 0) return null; - if (self.ch == '\n') break; - } - } else break; + fn shortopt(self: *Self, s: [:0]const u8) Option { + self.shortbuf[0] = '-'; + self.shortbuf[1] = s[0]; + self.short = if (s.len > 1) s[1.. :0] else null; + self.last = &self.shortbuf; + return .{ .opt = true, .val = &self.shortbuf }; + } + + /// Return the next option or positional argument. + /// 'opt' indicates whether it's an option or positional argument, + /// 'val' will be either -x, --something or the argument. + pub fn next(self: *Self) ?Option { + if (self.last_arg != null) ui.die("Option '{s}' does not expect an argument.\n", .{ self.last.? }); + if (self.short) |s| return self.shortopt(s); + const val = self.pop() orelse return null; + if (self.argsep or val.len == 0 or val[0] != '-') return Option{ .opt = false, .val = val }; + if (val.len == 1) ui.die("Invalid option '-'.\n", .{}); + if (val.len == 2 and val[1] == '-') { + self.argsep = true; + return self.next(); } - var val = std.ArrayList(u8).init(allocator); - if (self.hasarg) { - while (self.ch != '\n' and self.ch != 0) { - val.append(self.ch) catch unreachable; - self.con(); - } - self.hasarg = false; - } else { - while (true) { - switch (self.ch) { - '=', ' ', '\t', '\r' => { self.hasarg = true; break; }, - '\n' => break, - 0 => return null, - else => val.append(self.ch) catch unreachable, - } - self.con(); + if (val[1] == '-') { + if (std.mem.indexOfScalar(u8, val, '=')) |sep| { + if (sep == 2) ui.die("Invalid option '{s}'.\n", .{val}); + self.last_arg = val[sep+1.. :0]; + self.last = val[0..sep]; + return Option{ .opt = true, .val = self.last.? }; } + self.last = val; + return Option{ .opt = true, .val = val }; } - return util.arrayListBufZ(&val); + return self.shortopt(val[1..:0]); + } + + /// Returns the argument given to the last returned option. Dies with an error if no argument is provided. + pub fn arg(self: *Self) [:0]const u8 { + if (self.short) |a| { + defer self.short = null; + return a; + } + if (self.last_arg) |a| { + defer self.last_arg = null; + return a; + } + if (self.pop()) |o| return o; + ui.die("Option '{s}' requires an argument.\n", .{ self.last.? }); } }; -// TODO: Rewrite this (and Args(), I guess) to be non-generic, it rather bloats -// the binary this way. -fn argConfig(args: anytype, opt: anytype) bool { +fn argConfig(args: *Args, opt: Args.Option) bool { if (opt.is("-q") or opt.is("--slow-ui-updates")) config.update_delay = 2*std.time.ns_per_s else if (opt.is("--fast-ui-updates")) config.update_delay = 100*std.time.ns_per_ms else if (opt.is("-x") or opt.is("--one-file-system")) config.same_fs = true @@ -294,7 +224,7 @@ fn argConfig(args: anytype, opt: anytype) bool { else if (opt.is("--no-si")) config.si = false else if (opt.is("-L") or opt.is("--follow-symlinks")) config.follow_symlinks = true else if (opt.is("--no-follow-symlinks")) config.follow_symlinks = false - else if (opt.is("--exclude")) config.exclude_patterns.append(args.arg()) catch unreachable + else if (opt.is("--exclude")) config.exclude_patterns.append(allocator.dupeZ(u8, args.arg()) catch unreachable) catch unreachable else if (opt.is("-X") or opt.is("--exclude-from")) { const arg = args.arg(); readExcludeFile(arg) catch |e| ui.die("Error reading excludes from {s}: {s}.\n", .{ arg, ui.errorString(e) }); @@ -317,11 +247,36 @@ fn argConfig(args: anytype, opt: anytype) bool { } fn tryReadArgsFile(path: [:0]const u8) void { - var args = Args(ArgsFile).init(ArgsFile.open(path) orelse return); + var f = std.fs.cwd().openFileZ(path, .{}) catch |e| switch (e) { + error.FileNotFound => return, + else => ui.die("Error opening {s}: {s}\n", .{ path, ui.errorString(e) }), + }; + defer f.close(); + + var arglist = std.ArrayList([:0]const u8).init(allocator); + var rd = std.io.bufferedReader(f.reader()).reader(); + var linebuf: [4096]u8 = undefined; + + while ( + rd.readUntilDelimiterOrEof(&linebuf, '\n') + catch |e| ui.die("Error reading from {s}: {s}\n", .{ path, ui.errorString(e) }) + ) |line_| { + var line = std.mem.trim(u8, line_, &std.ascii.spaces); + if (line.len == 0 or line[0] == '#') continue; + if (std.mem.indexOfAny(u8, line, " \t=")) |i| { + arglist.append(allocator.dupeZ(u8, line[0..i]) catch unreachable) catch unreachable; + line = std.mem.trimLeft(u8, line[i+1..], &std.ascii.spaces); + } + arglist.append(allocator.dupeZ(u8, line) catch unreachable) catch unreachable; + } + + var args = Args.init(arglist.items); while (args.next()) |opt| { if (!argConfig(&args, opt)) - ui.die("Uncrecognized option in config file '{s}': {s}.\n", .{path, opt.val}); + ui.die("Unrecognized option in config file '{s}': {s}.\n", .{path, opt.val}); } + for (arglist.items) |i| allocator.free(i); + arglist.deinit(); } fn version() noreturn { @@ -446,26 +401,30 @@ pub fn main() void { tryReadArgsFile(path); } - var args = Args(std.process.ArgIteratorPosix).init(std.process.ArgIteratorPosix.init()); var scan_dir: ?[]const u8 = null; var import_file: ?[:0]const u8 = null; var export_file: ?[:0]const u8 = null; - _ = args.next(); // program name - while (args.next()) |opt| { - if (!opt.opt) { - // XXX: ncdu 1.x doesn't error, it just silently ignores all but the last argument. - if (scan_dir != null) ui.die("Multiple directories given, see ncdu -h for help.\n", .{}); - scan_dir = opt.val; - continue; + { + var arglist = std.process.argsAlloc(allocator) catch unreachable; + defer std.process.argsFree(allocator, arglist); + var args = Args.init(arglist); + _ = args.next(); // program name + while (args.next()) |opt| { + if (!opt.opt) { + // XXX: ncdu 1.x doesn't error, it just silently ignores all but the last argument. + if (scan_dir != null) ui.die("Multiple directories given, see ncdu -h for help.\n", .{}); + scan_dir = opt.val; + continue; + } + if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help() + else if (opt.is("-v") or opt.is("-V") or opt.is("--version")) version() + else if (opt.is("-o") and export_file != null) ui.die("The -o flag can only be given once.\n", .{}) + else if (opt.is("-o")) export_file = allocator.dupeZ(u8, args.arg()) catch unreachable + else if (opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{}) + else if (opt.is("-f")) import_file = allocator.dupeZ(u8, args.arg()) catch unreachable + else if (argConfig(&args, opt)) {} + else ui.die("Unrecognized option '{s}'.\n", .{opt.val}); } - if (opt.is("-h") or opt.is("-?") or opt.is("--help")) help() - else if (opt.is("-v") or opt.is("-V") or opt.is("--version")) version() - else if (opt.is("-o") and export_file != null) ui.die("The -o flag can only be given once.\n", .{}) - else if (opt.is("-o")) export_file = args.arg() - else if (opt.is("-f") and import_file != null) ui.die("The -f flag can only be given once.\n", .{}) - else if (opt.is("-f")) import_file = args.arg() - else if (argConfig(&args, opt)) {} - else ui.die("Unrecognized option '{s}'.\n", .{opt.val}); } if (std.builtin.os.tag != .linux and config.exclude_kernfs) @@ -568,19 +527,9 @@ pub fn handleEvent(block: bool, force_draw: bool) void { test "argument parser" { - const L = struct { - lst: []const [:0]const u8, - idx: usize = 0, - fn next(s: *@This()) ?[:0]const u8 { - if (s.idx == s.lst.len) return null; - defer s.idx += 1; - return s.lst[s.idx]; - } - }; const lst = [_][:0]const u8{ "a", "-abcd=e", "--opt1=arg1", "--opt2", "arg2", "-x", "foo", "", "--", "--arg", "", "-", }; - const l = L{ .lst = &lst }; const T = struct { - a: Args(L), + a: Args, fn opt(self: *@This(), isopt: bool, val: []const u8) !void { const o = self.a.next().?; try std.testing.expectEqual(isopt, o.opt); @@ -591,7 +540,7 @@ test "argument parser" { try std.testing.expectEqualStrings(val, self.a.arg()); } }; - var t = T{ .a = Args(L).init(l) }; + var t = T{ .a = Args.init(&lst) }; try t.opt(false, "a"); try t.opt(true, "-a"); try t.opt(true, "-b");