From b07b493c4861abda3b4b9243d602a462bab80094 Mon Sep 17 00:00:00 2001 From: rini Date: Sat, 28 Dec 2024 22:11:12 -0300 Subject: [PATCH 1/9] z --- README.md | 24 +++++---------- src/main.zig | 10 ++++++ src/parser.zig | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 src/main.zig create mode 100644 src/parser.zig diff --git a/README.md b/README.md index 64b04f5..12c587b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Of course, you can use maid to run its own tasks! Build the executable ```sh -cabal build maid +zig build-exe -OReleaseSafe --strip --name maid -Mroot=src/main.zig ``` ```` @@ -76,7 +76,8 @@ Of course, you can use maid to run its own tasks! Build the executable ```sh -cabal build maid +zig build-exe -OReleaseFast -fstrip src/main.zig --name maid +rm maid.o ``` ### install @@ -84,26 +85,17 @@ cabal build maid Install project onto `$dstdir` ```sh -install -Dm755 "$(cabal list-bin maid)" "$dstdir/bin/maid" -install -Dm644 LICENSE -t "$dstdir/share/licenses/maid" -install -Dm644 extras/completion/zsh "$dstdir/share/zsh/site-functions/_maid" +install -Dm755 maid "$dstdir/bin/maid" +install -Dm644 LICENSE -t "$dstdir/share/licenses/maid" +install -Dm644 extras/completion/zsh "$dstdir/share/zsh/site-functions/_maid" install -Dm644 extras/completion/fish "$dstdir/share/fish/vendor_completions.d/maid.fish" install -Dm644 extras/completion/bash "$dstdir/share/bash-completion/completions/maid" -ln -s maid "$dstdir/bin/made" ``` ### version Display the current version -This is used in build scripts - -```hs -import Data.Maybe -import qualified Data.Text as T -import qualified Data.Text.IO as I - -main = I.readFile "maid-build.cabal" - >>= (I.putStrLn . T.strip) - . (head . mapMaybe (T.stripPrefix $ T.pack "version:") . T.lines) +```sh +git describe --abbrev=0 ``` diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..0a6512d --- /dev/null +++ b/src/main.zig @@ -0,0 +1,10 @@ +const std = @import("std"); +const Parser = @import("parser.zig"); + +pub fn main() !void { + const alloc = std.heap.page_allocator; + const stdin = try std.fs.cwd().openFile("README.md", .{}); + + var parser = try Parser.init(alloc); + parser.parse(stdin.reader()); +} diff --git a/src/parser.zig b/src/parser.zig new file mode 100644 index 0000000..3192990 --- /dev/null +++ b/src/parser.zig @@ -0,0 +1,83 @@ +buffer: std.ArrayList(u8), +headingNr: usize = 0, + +pub fn init(alloc: std.mem.Allocator) !Self { + const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); + return .{ .buffer = buffer }; +} + +pub fn parse(self: *Self, file: anytype) void { + self.nextBlock(file) catch return; + + self.headingNr = heading(self.buffer.items); + if (self.headingNr != 0) + return @call(.always_tail, parseTasklist, .{ self, file }) + else + return @call(.always_tail, parse, .{ self, file }); +} + +fn parseTasklist(self: *Self, file: anytype) void { + self.nextBlock(file) catch return; + + if (std.mem.eql(u8, self.buffer.items, "")) + return @call(.always_tail, parseTasks, .{ self, file }) + else if (isBlank(self.buffer.items)) + return @call(.always_tail, parseTasklist, .{ self, file }) + else + return @call(.always_tail, parse, .{ self, file }); +} + +fn parseTasks(self: *Self, file: anytype) void { + self.nextBlock(file) catch return; + + std.debug.print("{}\n", .{self.headingNr}); +} + +fn nextBlock(self: *Self, file: anytype) !void { + const line = self.buffer.items; + const count = fence(line); + if (count > 0) { + try self.nextLine(file); + + while (true) : (try self.nextLine(file)) { + if (fence(line) >= count) break; + } + } else { + try self.nextLine(file); + } +} + +fn nextLine(self: *Self, file: anytype) !void { + self.buffer.clearRetainingCapacity(); + + file.streamUntilDelimiter(self.buffer.writer(), '\n', null) catch |err| { + if (self.buffer.items.len == 0) return err; + }; +} + +fn isBlank(text: []u8) bool { + for (text) |c| if (c != ' ' and c != '\t') return false; + + return true; +} + +fn fence(text: []u8) usize { + if (text.len < 3) + return 0; + if (text[0] == '`' or text[0] == '~') { + for (text[1..], 1..) |c, i| { + if (c != text[0]) return if (i < 3) 0 else i; + } + } + return 0; +} + +fn heading(text: []u8) usize { + for (text, 0..6) |c, i| { + if (c != '#') return i; + } + return 0; +} + +const std = @import("std"); +const Self = @This(); From 20c56ce0e6ccc6653c40ad651e2671c6d64d6399 Mon Sep 17 00:00:00 2001 From: rini Date: Sun, 29 Dec 2024 17:14:58 -0300 Subject: [PATCH 2/9] waow Co-authored-by: 87 <87flowers@noreply.codeberg.org> --- src/Parser.zig | 124 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 16 +++++-- src/parser.zig | 107 +++++++++++++++++++++++++++++------------- 3 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 src/Parser.zig diff --git a/src/Parser.zig b/src/Parser.zig new file mode 100644 index 0000000..e28099c --- /dev/null +++ b/src/Parser.zig @@ -0,0 +1,124 @@ +buffer: std.ArrayList(u8), +headingNr: usize = 0, +fenceNr: usize = 0, +lineNr: usize = 0, +blank: bool = true, + +pub fn init(alloc: mem.Allocator) !Self { + const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); + return .{ .buffer = buffer }; +} + +pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Task) { + var tasksHeading: usize = 0; + var state: enum { tasklist, magic, task, desc, code } = .tasklist; + + var tasks = std.ArrayList(Task).init(alloc); + var task: Task = undefined; + + while (true) { + self.nextBlock(file) catch break; + + switch (state) { + .tasklist => { + if (self.headingNr != 0) continue; + + state = .magic; + tasksHeading = self.headingNr; + }, + .magic => { + if (self.blank) continue; + + state = if (isMagic(self.buffer.items)) .task else .tasklist; + }, + .task => { + if (self.blank or self.headingNr == 0) continue; + + if (self.headingNr <= tasksHeading) break; + + state = .desc; + task.name = try ascii.allocLowerString( + alloc, + mem.trimLeft(u8, self.buffer.items[self.headingNr..], " \t"), + ); + }, + .desc => { + if (self.blank) continue; + + state = .task; + try tasks.append(task); + task = undefined; + }, + .code => {}, + } + } + + return tasks; +} + +fn nextBlock(self: *Self, file: anytype) !void { + try self.nextLine(file); + + if (self.fenceNr > 0) { + while (true) { + try self.nextLine(file); + if (fence(self.buffer.items) >= self.fenceNr) break; + } + try self.nextLine(file); + } else if (!self.blank and self.headingNr == 0) { + while (true) { + try self.nextLine(file); + if (isBlank(self.buffer.items)) break; + } + } + + self.headingNr = heading(self.buffer.items); + self.fenceNr = fence(self.buffer.items); + self.blank = isBlank(self.buffer.items); +} + +fn nextLine(self: *Self, file: anytype) !void { + self.buffer.clearRetainingCapacity(); + + file.streamUntilDelimiter(self.buffer.writer(), '\n', null) catch |err| { + if (self.buffer.items.len == 0) return err; + }; + + self.lineNr += 1; +} + +fn isMagic(text: []u8) bool { + return mem.eql(u8, text, ""); +} + +fn isBlank(text: []u8) bool { + for (text) |c| if (c != ' ' and c != '\t') return false; + + return true; +} + +fn fence(text: []u8) usize { + if (text.len < 3) + return 0; + if (text[0] == '`' or text[0] == '~') { + for (text[1..], 1..) |c, i| { + if (c != text[0]) return if (i < 3) 0 else i; + } + return text.len; + } + return 0; +} + +fn heading(text: []u8) usize { + for (text, 0..) |c, i| { + if (i > 6) return 0; + if (c != '#') return i; + } + return 0; +} + +const std = @import("std"); +const ascii = std.ascii; +const mem = std.mem; +const Self = @This(); +const Task = @import("main.zig").Task; diff --git a/src/main.zig b/src/main.zig index 0a6512d..1cb58fc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,10 +1,20 @@ -const std = @import("std"); -const Parser = @import("parser.zig"); +pub const Task = struct { + name: []u8, + description: []u8, + code: struct { lang: []u8, text: []u8 }, +}; pub fn main() !void { const alloc = std.heap.page_allocator; const stdin = try std.fs.cwd().openFile("README.md", .{}); var parser = try Parser.init(alloc); - parser.parse(stdin.reader()); + const tasks = try parser.parse(alloc, stdin.reader()); + + for (tasks.items) |task| { + std.debug.print("{s}\n", .{task.name}); + } } + +const std = @import("std"); +const Parser = @import("parser.zig"); diff --git a/src/parser.zig b/src/parser.zig index 3192990..e28099c 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -1,50 +1,80 @@ buffer: std.ArrayList(u8), headingNr: usize = 0, +fenceNr: usize = 0, +lineNr: usize = 0, +blank: bool = true, -pub fn init(alloc: std.mem.Allocator) !Self { +pub fn init(alloc: mem.Allocator) !Self { const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); return .{ .buffer = buffer }; } -pub fn parse(self: *Self, file: anytype) void { - self.nextBlock(file) catch return; - - self.headingNr = heading(self.buffer.items); - if (self.headingNr != 0) - return @call(.always_tail, parseTasklist, .{ self, file }) - else - return @call(.always_tail, parse, .{ self, file }); -} - -fn parseTasklist(self: *Self, file: anytype) void { - self.nextBlock(file) catch return; - - if (std.mem.eql(u8, self.buffer.items, "")) - return @call(.always_tail, parseTasks, .{ self, file }) - else if (isBlank(self.buffer.items)) - return @call(.always_tail, parseTasklist, .{ self, file }) - else - return @call(.always_tail, parse, .{ self, file }); -} - -fn parseTasks(self: *Self, file: anytype) void { - self.nextBlock(file) catch return; +pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Task) { + var tasksHeading: usize = 0; + var state: enum { tasklist, magic, task, desc, code } = .tasklist; + + var tasks = std.ArrayList(Task).init(alloc); + var task: Task = undefined; + + while (true) { + self.nextBlock(file) catch break; + + switch (state) { + .tasklist => { + if (self.headingNr != 0) continue; + + state = .magic; + tasksHeading = self.headingNr; + }, + .magic => { + if (self.blank) continue; + + state = if (isMagic(self.buffer.items)) .task else .tasklist; + }, + .task => { + if (self.blank or self.headingNr == 0) continue; + + if (self.headingNr <= tasksHeading) break; + + state = .desc; + task.name = try ascii.allocLowerString( + alloc, + mem.trimLeft(u8, self.buffer.items[self.headingNr..], " \t"), + ); + }, + .desc => { + if (self.blank) continue; + + state = .task; + try tasks.append(task); + task = undefined; + }, + .code => {}, + } + } - std.debug.print("{}\n", .{self.headingNr}); + return tasks; } fn nextBlock(self: *Self, file: anytype) !void { - const line = self.buffer.items; - const count = fence(line); - if (count > 0) { - try self.nextLine(file); + try self.nextLine(file); - while (true) : (try self.nextLine(file)) { - if (fence(line) >= count) break; + if (self.fenceNr > 0) { + while (true) { + try self.nextLine(file); + if (fence(self.buffer.items) >= self.fenceNr) break; } - } else { try self.nextLine(file); + } else if (!self.blank and self.headingNr == 0) { + while (true) { + try self.nextLine(file); + if (isBlank(self.buffer.items)) break; + } } + + self.headingNr = heading(self.buffer.items); + self.fenceNr = fence(self.buffer.items); + self.blank = isBlank(self.buffer.items); } fn nextLine(self: *Self, file: anytype) !void { @@ -53,6 +83,12 @@ fn nextLine(self: *Self, file: anytype) !void { file.streamUntilDelimiter(self.buffer.writer(), '\n', null) catch |err| { if (self.buffer.items.len == 0) return err; }; + + self.lineNr += 1; +} + +fn isMagic(text: []u8) bool { + return mem.eql(u8, text, ""); } fn isBlank(text: []u8) bool { @@ -68,16 +104,21 @@ fn fence(text: []u8) usize { for (text[1..], 1..) |c, i| { if (c != text[0]) return if (i < 3) 0 else i; } + return text.len; } return 0; } fn heading(text: []u8) usize { - for (text, 0..6) |c, i| { + for (text, 0..) |c, i| { + if (i > 6) return 0; if (c != '#') return i; } return 0; } const std = @import("std"); +const ascii = std.ascii; +const mem = std.mem; const Self = @This(); +const Task = @import("main.zig").Task; From 5813b8ddbf340a6c8a854477207c631bc464894a Mon Sep 17 00:00:00 2001 From: rini Date: Sun, 29 Dec 2024 19:36:09 -0300 Subject: [PATCH 3/9] ok --- src/Parser.zig | 68 ++++++++++++++++++++------- src/main.zig | 9 +++- src/parser.zig | 124 ------------------------------------------------- 3 files changed, 57 insertions(+), 144 deletions(-) delete mode 100644 src/parser.zig diff --git a/src/Parser.zig b/src/Parser.zig index e28099c..8aae79a 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -4,14 +4,14 @@ fenceNr: usize = 0, lineNr: usize = 0, blank: bool = true, -pub fn init(alloc: mem.Allocator) !Self { +pub fn init(alloc: std.mem.Allocator) !Self { const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); return .{ .buffer = buffer }; } -pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Task) { +pub fn parse(self: *Self, alloc: std.mem.Allocator, file: anytype) !std.ArrayList(Task) { var tasksHeading: usize = 0; - var state: enum { tasklist, magic, task, desc, code } = .tasklist; + var state: enum(u8) { tasklist, magic, task, desc, code } = .tasklist; var tasks = std.ArrayList(Task).init(alloc); var task: Task = undefined; @@ -19,9 +19,11 @@ pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Ta while (true) { self.nextBlock(file) catch break; - switch (state) { + std.debug.print("{d} {:3} | {s}\n", .{ @intFromEnum(state), self.lineNr, self.buffer.items }); + + sw: switch (state) { .tasklist => { - if (self.headingNr != 0) continue; + if (self.headingNr == 0) continue; state = .magic; tasksHeading = self.headingNr; @@ -32,30 +34,64 @@ pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Ta state = if (isMagic(self.buffer.items)) .task else .tasklist; }, .task => { - if (self.blank or self.headingNr == 0) continue; - + if (self.headingNr == 0) continue; if (self.headingNr <= tasksHeading) break; state = .desc; - task.name = try ascii.allocLowerString( + task.name = try std.ascii.allocLowerString( alloc, - mem.trimLeft(u8, self.buffer.items[self.headingNr..], " \t"), + std.mem.trim(u8, self.buffer.items[self.headingNr..], " \t"), ); }, .desc => { + if (self.headingNr > 0) continue :sw .task; if (self.blank) continue; + state = .code; + if (self.fenceNr == 0) { + var desc = try std.ArrayList(u8).initCapacity(alloc, self.buffer.items.len); + while (!isBlank(self.buffer.items)) { + try desc.appendSlice(self.buffer.items); + try desc.append('\n'); + try self.nextLine(file); + } + self.identify(); + task.description = try desc.toOwnedSlice(); + } else { + task.description = try alloc.dupe(u8, "[No description]"); + } + continue :sw state; + }, + .code => { + if (self.headingNr > 0) continue :sw .task; + if (self.blank or self.fenceNr == 0) continue; + state = .task; + task.code.lang = try alloc.dupe(u8, self.buffer.items[self.fenceNr..]); + { + var code = try std.ArrayList(u8).initCapacity(alloc, self.buffer.items.len); + while (fence(self.buffer.items) < self.fenceNr) { + try code.appendSlice(self.buffer.items); + try code.append('\n'); + try self.nextLine(file); + } + self.identify(); + task.code.text = try code.toOwnedSlice(); + } try tasks.append(task); - task = undefined; }, - .code => {}, } } return tasks; } +fn identify(self: *Self) void { + self.headingNr = heading(self.buffer.items); + self.fenceNr = fence(self.buffer.items); + self.blank = isBlank(self.buffer.items); +} + fn nextBlock(self: *Self, file: anytype) !void { try self.nextLine(file); @@ -67,14 +103,12 @@ fn nextBlock(self: *Self, file: anytype) !void { try self.nextLine(file); } else if (!self.blank and self.headingNr == 0) { while (true) { - try self.nextLine(file); if (isBlank(self.buffer.items)) break; + try self.nextLine(file); } } - self.headingNr = heading(self.buffer.items); - self.fenceNr = fence(self.buffer.items); - self.blank = isBlank(self.buffer.items); + self.identify(); } fn nextLine(self: *Self, file: anytype) !void { @@ -88,7 +122,7 @@ fn nextLine(self: *Self, file: anytype) !void { } fn isMagic(text: []u8) bool { - return mem.eql(u8, text, ""); + return std.mem.eql(u8, text, ""); } fn isBlank(text: []u8) bool { @@ -118,7 +152,5 @@ fn heading(text: []u8) usize { } const std = @import("std"); -const ascii = std.ascii; -const mem = std.mem; const Self = @This(); const Task = @import("main.zig").Task; diff --git a/src/main.zig b/src/main.zig index 1cb58fc..7837f4b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,9 +12,14 @@ pub fn main() !void { const tasks = try parser.parse(alloc, stdin.reader()); for (tasks.items) |task| { - std.debug.print("{s}\n", .{task.name}); + std.debug.print("task: {s}\n", .{task.name}); + std.debug.print("{s}\n", .{task.description}); + std.debug.print("\n", .{}); + std.debug.print("code({s}):\n", .{task.code.lang}); + std.debug.print("{s}\n", .{task.code.text}); + std.debug.print("\n", .{}); } } const std = @import("std"); -const Parser = @import("parser.zig"); +const Parser = @import("Parser.zig"); diff --git a/src/parser.zig b/src/parser.zig deleted file mode 100644 index e28099c..0000000 --- a/src/parser.zig +++ /dev/null @@ -1,124 +0,0 @@ -buffer: std.ArrayList(u8), -headingNr: usize = 0, -fenceNr: usize = 0, -lineNr: usize = 0, -blank: bool = true, - -pub fn init(alloc: mem.Allocator) !Self { - const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); - return .{ .buffer = buffer }; -} - -pub fn parse(self: *Self, alloc: mem.Allocator, file: anytype) !std.ArrayList(Task) { - var tasksHeading: usize = 0; - var state: enum { tasklist, magic, task, desc, code } = .tasklist; - - var tasks = std.ArrayList(Task).init(alloc); - var task: Task = undefined; - - while (true) { - self.nextBlock(file) catch break; - - switch (state) { - .tasklist => { - if (self.headingNr != 0) continue; - - state = .magic; - tasksHeading = self.headingNr; - }, - .magic => { - if (self.blank) continue; - - state = if (isMagic(self.buffer.items)) .task else .tasklist; - }, - .task => { - if (self.blank or self.headingNr == 0) continue; - - if (self.headingNr <= tasksHeading) break; - - state = .desc; - task.name = try ascii.allocLowerString( - alloc, - mem.trimLeft(u8, self.buffer.items[self.headingNr..], " \t"), - ); - }, - .desc => { - if (self.blank) continue; - - state = .task; - try tasks.append(task); - task = undefined; - }, - .code => {}, - } - } - - return tasks; -} - -fn nextBlock(self: *Self, file: anytype) !void { - try self.nextLine(file); - - if (self.fenceNr > 0) { - while (true) { - try self.nextLine(file); - if (fence(self.buffer.items) >= self.fenceNr) break; - } - try self.nextLine(file); - } else if (!self.blank and self.headingNr == 0) { - while (true) { - try self.nextLine(file); - if (isBlank(self.buffer.items)) break; - } - } - - self.headingNr = heading(self.buffer.items); - self.fenceNr = fence(self.buffer.items); - self.blank = isBlank(self.buffer.items); -} - -fn nextLine(self: *Self, file: anytype) !void { - self.buffer.clearRetainingCapacity(); - - file.streamUntilDelimiter(self.buffer.writer(), '\n', null) catch |err| { - if (self.buffer.items.len == 0) return err; - }; - - self.lineNr += 1; -} - -fn isMagic(text: []u8) bool { - return mem.eql(u8, text, ""); -} - -fn isBlank(text: []u8) bool { - for (text) |c| if (c != ' ' and c != '\t') return false; - - return true; -} - -fn fence(text: []u8) usize { - if (text.len < 3) - return 0; - if (text[0] == '`' or text[0] == '~') { - for (text[1..], 1..) |c, i| { - if (c != text[0]) return if (i < 3) 0 else i; - } - return text.len; - } - return 0; -} - -fn heading(text: []u8) usize { - for (text, 0..) |c, i| { - if (i > 6) return 0; - if (c != '#') return i; - } - return 0; -} - -const std = @import("std"); -const ascii = std.ascii; -const mem = std.mem; -const Self = @This(); -const Task = @import("main.zig").Task; From 007b0eb995b9479f138b040d986d1c47392ffbc3 Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 30 Dec 2024 17:04:43 -0300 Subject: [PATCH 4/9] a --- src/Parser.zig | 172 +++++++++++++++++++++++-------------------------- src/Task.zig | 21 ++++++ src/main.zig | 14 +--- 3 files changed, 104 insertions(+), 103 deletions(-) create mode 100644 src/Task.zig diff --git a/src/Parser.zig b/src/Parser.zig index 8aae79a..116fd78 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -1,128 +1,117 @@ -buffer: std.ArrayList(u8), +line: std.ArrayList(u8), +block: std.ArrayList(u8), + headingNr: usize = 0, fenceNr: usize = 0, lineNr: usize = 0, -blank: bool = true, +lang: ?Task.Lang = null, pub fn init(alloc: std.mem.Allocator) !Self { - const buffer = try std.ArrayList(u8).initCapacity(alloc, 32); - return .{ .buffer = buffer }; + const line = try std.ArrayList(u8).initCapacity(alloc, 32); + const block = try std.ArrayList(u8).initCapacity(alloc, 128); + + return .{ .line = line, .block = block }; } pub fn parse(self: *Self, alloc: std.mem.Allocator, file: anytype) !std.ArrayList(Task) { var tasksHeading: usize = 0; - var state: enum(u8) { tasklist, magic, task, desc, code } = .tasklist; + var state: enum { tasklist, magic, task, desc, code } = .tasklist; var tasks = std.ArrayList(Task).init(alloc); var task: Task = undefined; - while (true) { - self.nextBlock(file) catch break; - - std.debug.print("{d} {:3} | {s}\n", .{ @intFromEnum(state), self.lineNr, self.buffer.items }); - - sw: switch (state) { - .tasklist => { - if (self.headingNr == 0) continue; - - state = .magic; - tasksHeading = self.headingNr; - }, - .magic => { - if (self.blank) continue; - - state = if (isMagic(self.buffer.items)) .task else .tasklist; - }, - .task => { - if (self.headingNr == 0) continue; - if (self.headingNr <= tasksHeading) break; - - state = .desc; - task.name = try std.ascii.allocLowerString( - alloc, - std.mem.trim(u8, self.buffer.items[self.headingNr..], " \t"), - ); - }, - .desc => { - if (self.headingNr > 0) continue :sw .task; - if (self.blank) continue; - - state = .code; - if (self.fenceNr == 0) { - var desc = try std.ArrayList(u8).initCapacity(alloc, self.buffer.items.len); - while (!isBlank(self.buffer.items)) { - try desc.appendSlice(self.buffer.items); - try desc.append('\n'); - try self.nextLine(file); - } - self.identify(); - task.description = try desc.toOwnedSlice(); - } else { - task.description = try alloc.dupe(u8, "[No description]"); - } + while (self.nextBlock(file) catch null) |_| sw: switch (state) { + .tasklist => { + if (self.headingNr == 0) continue; + + state = .magic; + tasksHeading = self.headingNr; + }, + .magic => { + state = if (isMagic(self.block.items)) .task else .tasklist; + }, + .task => { + if (self.headingNr == 0) continue; + if (self.headingNr <= tasksHeading) break; + + state = .desc; + task.name = try std.ascii.allocLowerString(alloc, trim(u8, self.block.items[self.headingNr..], " \t")); + }, + .desc => { + if (self.headingNr > 0) continue :sw .task; + + state = .code; + if (self.fenceNr == 0) { + task.desc = try alloc.dupe(u8, trim(u8, self.block.items, " \t")); + } else { + task.desc = try alloc.dupe(u8, "[No description]"); continue :sw state; - }, - .code => { - if (self.headingNr > 0) continue :sw .task; - if (self.blank or self.fenceNr == 0) continue; - - state = .task; - task.code.lang = try alloc.dupe(u8, self.buffer.items[self.fenceNr..]); - { - var code = try std.ArrayList(u8).initCapacity(alloc, self.buffer.items.len); - while (fence(self.buffer.items) < self.fenceNr) { - try code.appendSlice(self.buffer.items); - try code.append('\n'); - try self.nextLine(file); - } - self.identify(); - task.code.text = try code.toOwnedSlice(); - } - try tasks.append(task); - }, - } - } + } + }, + .code => { + if (self.headingNr > 0) continue :sw .task; + if (self.fenceNr == 0) continue; + + state = .task; + task.code.lang = self.lang.?; + task.code.text = try alloc.dupe(u8, trim(u8, self.block.items, "\n")); + self.nextBlock(file) catch {}; + try tasks.append(task); + continue :sw state; + }, + }; return tasks; } -fn identify(self: *Self) void { - self.headingNr = heading(self.buffer.items); - self.fenceNr = fence(self.buffer.items); - self.blank = isBlank(self.buffer.items); -} - fn nextBlock(self: *Self, file: anytype) !void { - try self.nextLine(file); + self.block.clearRetainingCapacity(); - if (self.fenceNr > 0) { - while (true) { + while (true) { + try self.nextLine(file); + if (!isBlank(self.line.items)) break; + } + + self.headingNr = heading(self.line.items); + self.fenceNr = fence(self.line.items); + + if (self.headingNr > 0) { + try self.block.appendSlice(self.line.items); + } else if (self.fenceNr > 0) { + self.lang = Task.Lang.fromId(trim(u8, self.line.items[self.fenceNr..], " \t")); + + while (true) : ({ + try self.block.append('\n'); + try self.block.appendSlice(self.line.items); + }) { try self.nextLine(file); - if (fence(self.buffer.items) >= self.fenceNr) break; + if (fence(self.line.items) >= self.fenceNr) break; } - try self.nextLine(file); - } else if (!self.blank and self.headingNr == 0) { - while (true) { - if (isBlank(self.buffer.items)) break; + } else { + try self.block.appendSlice(self.line.items); + + while (true) : ({ + try self.block.append(' '); + try self.block.appendSlice(self.line.items); + }) { try self.nextLine(file); + if (isBlank(self.line.items)) break; } } - - self.identify(); } fn nextLine(self: *Self, file: anytype) !void { - self.buffer.clearRetainingCapacity(); + self.line.clearRetainingCapacity(); - file.streamUntilDelimiter(self.buffer.writer(), '\n', null) catch |err| { - if (self.buffer.items.len == 0) return err; + file.streamUntilDelimiter(self.line.writer(), '\n', null) catch |err| { + if (self.line.items.len == 0) return err; }; self.lineNr += 1; } fn isMagic(text: []u8) bool { - return std.mem.eql(u8, text, ""); + return std.mem.startsWith(u8, text, ""); } fn isBlank(text: []u8) bool { @@ -152,5 +141,6 @@ fn heading(text: []u8) usize { } const std = @import("std"); +const trim = std.mem.trim; const Self = @This(); -const Task = @import("main.zig").Task; +const Task = @import("Task.zig"); diff --git a/src/Task.zig b/src/Task.zig new file mode 100644 index 0000000..80a1860 --- /dev/null +++ b/src/Task.zig @@ -0,0 +1,21 @@ +name: []u8, +desc: []u8, +code: struct { lang: Lang, text: []u8 }, + +pub const Lang = enum { + shell, + haskell, + javascript, + + pub fn fromId(id: []const u8) ?Lang { + return langIds.get(id); + } +}; + +const langIds = std.StaticStringMap(Lang).initComptime(.{ + .{ "sh", .shell }, .{ "bash", .shell }, + .{ "hs", .haskell }, .{ "haskell", .haskell }, + .{ "js", .javascript }, .{ "javascript", .javascript }, +}); + +const std = @import("std"); diff --git a/src/main.zig b/src/main.zig index 7837f4b..6ea0d70 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,9 +1,3 @@ -pub const Task = struct { - name: []u8, - description: []u8, - code: struct { lang: []u8, text: []u8 }, -}; - pub fn main() !void { const alloc = std.heap.page_allocator; const stdin = try std.fs.cwd().openFile("README.md", .{}); @@ -12,12 +6,8 @@ pub fn main() !void { const tasks = try parser.parse(alloc, stdin.reader()); for (tasks.items) |task| { - std.debug.print("task: {s}\n", .{task.name}); - std.debug.print("{s}\n", .{task.description}); - std.debug.print("\n", .{}); - std.debug.print("code({s}):\n", .{task.code.lang}); - std.debug.print("{s}\n", .{task.code.text}); - std.debug.print("\n", .{}); + std.debug.print("{s}\n", .{task.name}); + std.debug.print(" {s}\n\n", .{task.desc}); } } From ffac1d2085ed021084392d5836c7c5df485a07fe Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 30 Dec 2024 20:25:48 -0300 Subject: [PATCH 5/9] close --- .github/workflows/publish.yml | 15 +++-------- README.md | 5 ++-- src/Parser.zig | 47 +++++++++++++++-------------------- src/Style.zig | 46 ++++++++++++++++++++++++++++++++++ src/main.zig | 41 ++++++++++++++++++++++++++---- 5 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 src/Style.zig diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b285d2d..a67ee76 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,19 +36,12 @@ jobs: choco install upx - name: Build run: | - cabal update - cabal configure \ - --enable-optimization=2 \ - --enable-static --enable-executable-static \ - --enable-executable-stripping - cabal build - echo "binpath=$(cabal list-bin maid)" >> $GITHUB_ENV + zig build-exe src/main.zig --name maid - name: Publish if: runner.os == 'Linux' run: | - pkg=maid-$("$binpath" -q version) - dstdir=$pkg "$binpath" install - strip "$pkg/bin/maid" + pkg=maid-$(./maid -q version) + ./maid install --dstdir=$pkg tar --remove-files -cf "$pkg.tar.gz" "$pkg" gh release upload "$GITHUB_REF_NAME" "$pkg.tar.gz" @@ -57,7 +50,7 @@ jobs: - name: Publish if: runner.os == 'Windows' run: | - exe=maid-$("$binpath" -q version).exe + exe=maid-$(./maid.exe -q version).exe cp "$binpath" "$exe" strip "$exe" && upx "$exe" gh release upload "$GITHUB_REF_NAME" "$exe" diff --git a/README.md b/README.md index 12c587b..a557ff3 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Of course, you can use maid to run its own tasks! Build the executable ```sh -zig build-exe -OReleaseSafe --strip --name maid -Mroot=src/main.zig +zig build-exe src/main.zig --name maid ``` ```` @@ -76,8 +76,7 @@ Of course, you can use maid to run its own tasks! Build the executable ```sh -zig build-exe -OReleaseFast -fstrip src/main.zig --name maid -rm maid.o +zig build-exe src/main.zig --name maid ``` ### install diff --git a/src/Parser.zig b/src/Parser.zig index 116fd78..895f268 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -1,3 +1,4 @@ +alloc: std.mem.Allocator, line: std.ArrayList(u8), block: std.ArrayList(u8), @@ -6,20 +7,19 @@ fenceNr: usize = 0, lineNr: usize = 0, lang: ?Task.Lang = null, -pub fn init(alloc: std.mem.Allocator) !Self { +pub fn init(alloc: std.mem.Allocator) !Parser { const line = try std.ArrayList(u8).initCapacity(alloc, 32); const block = try std.ArrayList(u8).initCapacity(alloc, 128); - return .{ .line = line, .block = block }; + return .{ .alloc = alloc, .line = line, .block = block }; } -pub fn parse(self: *Self, alloc: std.mem.Allocator, file: anytype) !std.ArrayList(Task) { +pub fn parseMarkdown(self: *Parser, file: anytype, tasks: *std.ArrayList(Task)) !void { + var task: Task = undefined; + var tasksHeading: usize = 0; var state: enum { tasklist, magic, task, desc, code } = .tasklist; - var tasks = std.ArrayList(Task).init(alloc); - var task: Task = undefined; - while (self.nextBlock(file) catch null) |_| sw: switch (state) { .tasklist => { if (self.headingNr == 0) continue; @@ -35,18 +35,17 @@ pub fn parse(self: *Self, alloc: std.mem.Allocator, file: anytype) !std.ArrayLis if (self.headingNr <= tasksHeading) break; state = .desc; - task.name = try std.ascii.allocLowerString(alloc, trim(u8, self.block.items[self.headingNr..], " \t")); + task.name = try std.ascii.allocLowerString(self.alloc, self.block.items); }, .desc => { if (self.headingNr > 0) continue :sw .task; state = .code; - if (self.fenceNr == 0) { - task.desc = try alloc.dupe(u8, trim(u8, self.block.items, " \t")); - } else { - task.desc = try alloc.dupe(u8, "[No description]"); - continue :sw state; - } + task.desc = try self.alloc.dupe(u8, if (self.fenceNr == 0) + trim(u8, self.block.items, " \t") + else + "[No description]"); + continue :sw state; }, .code => { if (self.headingNr > 0) continue :sw .task; @@ -54,17 +53,13 @@ pub fn parse(self: *Self, alloc: std.mem.Allocator, file: anytype) !std.ArrayLis state = .task; task.code.lang = self.lang.?; - task.code.text = try alloc.dupe(u8, trim(u8, self.block.items, "\n")); - self.nextBlock(file) catch {}; + task.code.text = try self.alloc.dupe(u8, self.block.items); try tasks.append(task); - continue :sw state; }, }; - - return tasks; } -fn nextBlock(self: *Self, file: anytype) !void { +fn nextBlock(self: *Parser, file: anytype) !void { self.block.clearRetainingCapacity(); while (true) { @@ -76,7 +71,7 @@ fn nextBlock(self: *Self, file: anytype) !void { self.fenceNr = fence(self.line.items); if (self.headingNr > 0) { - try self.block.appendSlice(self.line.items); + try self.block.appendSlice(trim(u8, self.line.items[self.headingNr..], " \t")); } else if (self.fenceNr > 0) { self.lang = Task.Lang.fromId(trim(u8, self.line.items[self.fenceNr..], " \t")); @@ -100,14 +95,12 @@ fn nextBlock(self: *Self, file: anytype) !void { } } -fn nextLine(self: *Self, file: anytype) !void { +fn nextLine(self: *Parser, file: anytype) !void { self.line.clearRetainingCapacity(); - - file.streamUntilDelimiter(self.line.writer(), '\n', null) catch |err| { - if (self.line.items.len == 0) return err; - }; - self.lineNr += 1; + + return file.streamUntilDelimiter(self.line.writer(), '\n', null) catch |err| + if (self.line.items.len == 0) err; } fn isMagic(text: []u8) bool { @@ -142,5 +135,5 @@ fn heading(text: []u8) usize { const std = @import("std"); const trim = std.mem.trim; -const Self = @This(); +const Parser = @This(); const Task = @import("Task.zig"); diff --git a/src/Style.zig b/src/Style.zig new file mode 100644 index 0000000..da8c43f --- /dev/null +++ b/src/Style.zig @@ -0,0 +1,46 @@ +primary: Color, +secondary: Color, +reset: Color, +err: Color, + +pub fn default() Self { + return if (std.process.hasEnvVarConstant("NO_COLOR") or + !std.io.getStdOut().getOrEnableAnsiEscapeSupport()) + .{ + .primary = .none, + .secondary = .none, + .reset = .none, + .err = .none, + } + else + .{ + .primary = Color.make("95;1"), + .secondary = Color.make("0;1"), + .reset = Color.make(""), + .err = Color.make("31;1"), + }; +} + +pub const Color = union(enum) { + esc: []const u8, + none, + + fn make(esc: []const u8) Color { + return .{ .esc = esc }; + } + + pub fn format( + self: Color, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + switch (self) { + .esc => |esc| try std.fmt.format(writer, "\x1b[{s}m", .{esc}), + else => {}, + } + } +}; + +const std = @import("std"); +const Self = @This(); diff --git a/src/main.zig b/src/main.zig index 6ea0d70..9f2db36 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,15 +1,46 @@ pub fn main() !void { + const out = std.io.getStdOut().writer(); const alloc = std.heap.page_allocator; - const stdin = try std.fs.cwd().openFile("README.md", .{}); + const tasks = try findTaskfile(alloc); + const style = Style.default(); + + try std.fmt.format(out, "{}Tasks in {s}\n\n", .{ style.primary, tasks.path }); + + for (tasks.list) |task| { + try std.fmt.format(out, "{} {s}\n", .{ style.secondary, task.name }); + try std.fmt.format(out, "{} {s}\n\n", .{ style.reset, task.desc }); + } +} + +const Taskfile = struct { + path: []const u8, + list: []Task, +}; + +fn findTaskfile(alloc: std.mem.Allocator) !Taskfile { var parser = try Parser.init(alloc); - const tasks = try parser.parse(alloc, stdin.reader()); + var tasks = std.ArrayList(Task).init(alloc); + var dir = std.fs.cwd(); + + for (0..16) |_| { + for ([_]([]const u8){ "README.md", "CONTRIBUTING.md" }) |name| { + const file = try dir.openFile(name, .{}); + try parser.parseMarkdown(file.reader(), &tasks); - for (tasks.items) |task| { - std.debug.print("{s}\n", .{task.name}); - std.debug.print(" {s}\n\n", .{task.desc}); + if (tasks.items.len > 0) return .{ + .path = name, + .list = try tasks.toOwnedSlice(), + }; + } + + dir = try dir.openDir("..", .{}); } + + return error.NoTaskfile; } const std = @import("std"); const Parser = @import("Parser.zig"); +const Style = @import("Style.zig"); +const Task = @import("Task.zig"); From 7530a9e1704a2553bf389f7a4d3871ffe3fc6cdb Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 30 Dec 2024 20:28:51 -0300 Subject: [PATCH 6/9] workflow --- .github/workflows/publish.yml | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a67ee76..b0dc62e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,33 +26,29 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: haskell-actions/setup@v2 + - uses: mlugg/setup-zig@v3 with: - ghc-version: "9.4.8" - cabal-version: "3.10.3.0" - - name: Install dependencies - if: runner.os == 'Windows' - run: | - choco install upx + version: 0.14.0-dev.2563+af5e73172 + - name: Build - run: | - zig build-exe src/main.zig --name maid + run: zig build-exe src/main.zig --name maid + - name: Publish if: runner.os == 'Linux' run: | - pkg=maid-$(./maid -q version) - ./maid install --dstdir=$pkg - tar --remove-files -cf "$pkg.tar.gz" "$pkg" + pkg=maid-$(./maid -q version).tar.gz + ./maid install --dstdir=package + tar --remove-files -cf "$pkg" package - gh release upload "$GITHUB_REF_NAME" "$pkg.tar.gz" + gh release upload "$GITHUB_REF_NAME" "$pkg" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish if: runner.os == 'Windows' run: | exe=maid-$(./maid.exe -q version).exe - cp "$binpath" "$exe" - strip "$exe" && upx "$exe" + cp maid.exe "$exe" gh release upload "$GITHUB_REF_NAME" "$exe" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d1bb76d4b3772f16469633cd3e6bd3e5080c276b Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 30 Dec 2024 20:35:57 -0300 Subject: [PATCH 7/9] wkflworw --- .github/workflows/test.yml | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8e1113..86f7aee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,31 +24,9 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: haskell-actions/setup@v2 + - uses: mlugg/setup-zig@v3 with: - ghc-version: "9.4.8" - cabal-version: "3.10.3.0" - - name: Install dependencies - if: runner.os == 'Windows' - run: | - choco install upx + version: 0.14.0-dev.2563+af5e73172 + - name: Build - run: | - cabal update - cabal configure \ - --enable-optimization=2 \ - --enable-static --enable-executable-static \ - --enable-executable-stripping - cabal build - echo "binpath=$(cabal list-bin maid)" >> $GITHUB_ENV - - name: Check binary size - run: | - cp "$binpath" maid - strip maid -o maid-s - upx maid-s -o maid-u - echo "No strip, no compression" - du -hs maid - echo "Stripped, no compression" - du -hs maid-s - echo "Stripped, compressed" - du -hs maid-u + run: zig build-exe src/main.zig --name maid From 4c7ea5e605d09a7b2da036198eb588bc4651a0dc Mon Sep 17 00:00:00 2001 From: rini Date: Mon, 30 Dec 2024 20:36:45 -0300 Subject: [PATCH 8/9] wowwowflow --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b0dc62e..2029d34 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: mlugg/setup-zig@v3 + - uses: mlugg/setup-zig@v1 with: version: 0.14.0-dev.2563+af5e73172 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86f7aee..7f26e38 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: mlugg/setup-zig@v3 + - uses: mlugg/setup-zig@v1 with: version: 0.14.0-dev.2563+af5e73172 From e69671c4f6dd6ba5c0dbd1a997d7d22ca19fa6c9 Mon Sep 17 00:00:00 2001 From: rini Date: Thu, 2 Jan 2025 11:33:48 -0300 Subject: [PATCH 9/9] im tirde --- src/Parser.zig | 12 ++- src/getopt.zig | 255 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 142 +++++++++++++++++++++++---- 3 files changed, 387 insertions(+), 22 deletions(-) create mode 100644 src/getopt.zig diff --git a/src/Parser.zig b/src/Parser.zig index 895f268..ef169f3 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -14,7 +14,13 @@ pub fn init(alloc: std.mem.Allocator) !Parser { return .{ .alloc = alloc, .line = line, .block = block }; } -pub fn parseMarkdown(self: *Parser, file: anytype, tasks: *std.ArrayList(Task)) !void { +pub fn deinit(self: *Parser) void { + self.line.deinit(); + self.block.deinit(); +} + +pub fn parseMarkdown(self: *Parser, file: anytype) !std.ArrayList(Task) { + var tasks = std.ArrayList(Task).init(self.alloc); var task: Task = undefined; var tasksHeading: usize = 0; @@ -52,11 +58,13 @@ pub fn parseMarkdown(self: *Parser, file: anytype, tasks: *std.ArrayList(Task)) if (self.fenceNr == 0) continue; state = .task; - task.code.lang = self.lang.?; + task.code.lang = self.lang orelse return error.UnknownLanguage; task.code.text = try self.alloc.dupe(u8, self.block.items); try tasks.append(task); }, }; + + return tasks; } fn nextBlock(self: *Parser, file: anytype) !void { diff --git a/src/getopt.zig b/src/getopt.zig new file mode 100644 index 0000000..082ba3c --- /dev/null +++ b/src/getopt.zig @@ -0,0 +1,255 @@ +//! getopt(3)-style command-line parsing +//! +//! https://git.transistor.house/rini/getopt.zig +//! +//! COPYRIGHT +//! This file is provided in the same terms as Zig itself. +//! +//! https://github.com/ziglang/zig/blob/master/LICENSE + +pub var no_permute = false; + +pub fn parse(comptime T: type, args: anytype, opts: anytype) Parser(T, @TypeOf(args), opts) { + return .{ .args = args }; +} + +pub fn parseAlloc( + comptime T: type, + alloc: std.mem.Allocator, + args: anytype, + opts: anytype, +) ![]T { + const buffer = try alloc.alloc(T, args.len); + var parser = parse(T, args, opts); + + var i: usize = 0; + while (try parser.next()) |opt| : (i += 1) + buffer[i] = opt; + + return try alloc.realloc(buffer, i); +} + +const ArgDescr = enum { noArg, optArg, reqArg }; + +const OptDescr = struct { + short: ?u8, + long: ?[]const u8, + field: []const u8, + arg: ArgDescr, +}; + +pub const ParseError = error{ + RequiredArgument, + UnknownOption, + UnexpectedArgument, +}; + +pub const ErrorMessage = struct { + err: ParseError, + opt: []const u8, + + pub fn format( + self: ErrorMessage, + _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + const prefix = if (self.opt.len > 1) "--" else "-"; + const args = .{ prefix, self.opt }; + + try switch (self.err) { + error.RequiredArgument => std.fmt.format(writer, "Option {s}{s} requires an argument\n", args), + error.UnknownOption => std.fmt.format(writer, "Unknown option {s}{s}\n", args), + error.UnexpectedArgument => std.fmt.format(writer, "Option {s}{s} doesn't allow arguments\n", args), + }; + } +}; + +fn Parser(comptime T: type, comptime Args: type, comptime opts: anytype) type { + comptime var descrs: [@divExact(opts.len, 3)]OptDescr = undefined; + comptime { + var i = 0; + while (i < opts.len) : (i += 3) { + const short = switch (opts[i + 0].len) { + 0 => null, + 1 => opts[i + 0][0], + else => unreachable, + }; + const long = switch (opts[i + 1].len) { + 0 => null, + 1 => unreachable, + else => opts[i + 1], + }; + const field = @tagName(opts[i + 2]); + const arg = switch (@FieldType(T, field)) { + ?[]const u8 => .optArg, + []const u8 => .reqArg, + void => .noArg, + else => unreachable, + }; + + descrs[@divExact(i, 3)] = + .{ .short = short, .long = long, .arg = arg, .field = field }; + } + } + + return struct { + const Self = @This(); + + args: Args, + index: usize = 0, + opt_index: usize = 0, + + errored_opt: []const u8 = &.{}, + + pub fn nextArg(self: *Self) ?[]const u8 { + if (self.index < self.args.len) { + defer self.index += 1; + return self.args[self.index]; + } + return null; + } + + pub fn next(self: *Self) ParseError!?T { + var index_shift: usize = 1; + defer self.index += index_shift; + + if (self.opt_index > 0 and self.opt_index + 1 >= self.args[self.index].len) { + self.opt_index = 0; + self.index += 1; + } + + if (self.index >= self.args.len) return null; + + if (self.args[self.index].len < 2 or self.args[self.index][0] != '-') { + index_shift = 0; + return null; + + // non-option; TODO: rotate arguments? ? ?? + } + + if (self.opt_index > 0 or self.args[self.index][1] != '-') { + self.opt_index += 1; + + // short option + const opt = self.args[self.index][self.opt_index]; + const arg = self.args[self.index][self.opt_index + 1 ..]; + + inline for (descrs) |desc| if (desc.short) |short| { + if (opt == short) switch (desc.arg) { + .reqArg => { + self.opt_index = 0; + if (arg.len > 0) { + return @unionInit(T, desc.field, arg); + } else if (self.index + 1 < self.args.len) { + self.index += 1; + return @unionInit(T, desc.field, self.args[self.index]); + } else { + self.errored_opt = &.{opt}; + return ParseError.RequiredArgument; + } + }, + .optArg => { + self.opt_index = 0; + return @unionInit(T, desc.field, if (arg.len > 0) arg else null); + }, + .noArg => { + index_shift = 0; + return @unionInit(T, desc.field, {}); + }, + }; + }; + + self.errored_opt = &.{opt}; + return ParseError.UnknownOption; + } + + if (self.args[self.index].len == 2) return null; // end of options + + // long option + var opt_ = self.args[self.index]; + var opt = opt_[2..opt_.len]; // might be sentinel array + var arg: ?[]const u8 = null; + + if (std.mem.indexOfScalar(u8, opt, '=')) |eq| { + arg = opt[eq + 1 ..]; + opt = opt[0..eq]; + } + + inline for (descrs) |desc| if (desc.long) |long| { + if (std.mem.eql(u8, opt, long)) switch (desc.arg) { + .reqArg => { + if (arg) |a| { + return @unionInit(T, desc.field, a); + } else if (self.index + 1 < self.args.len) { + self.index += 1; + return @unionInit(T, desc.field, self.args[self.index]); + } else { + self.errored_opt = opt; + return ParseError.RequiredArgument; + } + }, + .optArg => return @unionInit(T, desc.field, arg), + .noArg => { + if (arg) |_| { + self.errored_opt = opt; + return ParseError.UnexpectedArgument; + } else { + return @unionInit(T, desc.field, {}); + } + }, + }; + }; + + self.errored_opt = opt; + return ParseError.UnknownOption; + } + + pub fn errorMessage(self: *Self, err: ParseError) ErrorMessage { + return .{ .err = err, .opt = self.errored_opt }; + } + }; +} + +inline fn ArgType(T: type) ArgDescr { + return switch (T) { + ?[]const u8 => .optArg, + []const u8 => .reqArg, + void => .noArg, + else => unreachable, + }; +} + +test "getopt" { + const Opts = union(enum) { + help, + version, + output: []const u8, + }; + + const args = [_][]const u8{ + "-omeow.a", "-o", "meow.b", "--output=meow.c", "--output", "meow.d", "foo", + "--help", "-vvv", + }; + var opts = parse(Opts, args, .{ + "h", "help", .help, + "v", "version", .version, + "o", "output", .output, + }); + + try expectEqualDeep(opts.next(), Opts{ .output = "meow.a" }); + try expectEqualDeep(opts.next(), Opts{ .output = "meow.b" }); + try expectEqualDeep(opts.next(), Opts{ .output = "meow.c" }); + try expectEqualDeep(opts.next(), Opts{ .output = "meow.d" }); + try expectEqualDeep(opts.next(), null); + try expectEqualDeep(opts.nextArg(), "foo"); + try expectEqualDeep(opts.next(), Opts.help); + try expectEqualDeep(opts.next(), Opts.version); + try expectEqualDeep(opts.next(), Opts.version); + try expectEqualDeep(opts.next(), Opts.version); + try expectEqualDeep(opts.next(), null); + try expectEqualDeep(opts.nextArg(), null); +} + +const std = @import("std"); +const expectEqualDeep = std.testing.expectEqualDeep; diff --git a/src/main.zig b/src/main.zig index 9f2db36..f6c519b 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,15 +1,84 @@ +const options = + \\ -h --help Display this message + \\ -l --list List tasks concisely + \\ -n --dry-run Don't run anything, only display commands + \\ -q --quiet Don't display anything + \\ -f FILE --taskfile=FILE Use tasks in FILE +; + +const Opts = union(enum) { + help, + list, + dryRun, + quiet, + taskfile: []const u8, +}; + +var style: Style = undefined; + +inline fn fatal(fmt: []const u8, args: anytype) noreturn { + std.debug.print("{}error: {}", .{ style.err, style.secondary }); + std.debug.print(fmt, args); + std.debug.print("\n", .{}); + std.process.exit(1); +} + pub fn main() !void { - const out = std.io.getStdOut().writer(); - const alloc = std.heap.page_allocator; + style = Style.default(); + + const args = try std.process.argsAlloc(alloc); + defer std.process.argsFree(alloc, args); - const tasks = try findTaskfile(alloc); - const style = Style.default(); + var doList = false; + var dryRun = false; + var quiet = false; + var taskName: ?[]const u8 = null; + var tasksArg: ?Taskfile = null; + var opts = getopt.parse(Opts, args, .{ + "h", "help", .help, + "l", "list", .list, + "n", "dry-run", .dryRun, + "q", "quiet", .quiet, + "f", "taskfile", .taskfile, + }); - try std.fmt.format(out, "{}Tasks in {s}\n\n", .{ style.primary, tasks.path }); + _ = opts.nextArg(); - for (tasks.list) |task| { - try std.fmt.format(out, "{} {s}\n", .{ style.secondary, task.name }); - try std.fmt.format(out, "{} {s}\n\n", .{ style.reset, task.desc }); + while (true) { + while (opts.next() catch |err| { + fatal("{}", .{opts.errorMessage(err)}); + }) |opt| switch (opt) { + .help => { + std.debug.print("{}Usage: {}maid [options] [task]\n\n", .{ style.primary, style.secondary }); + std.debug.print("{}Options:{}\n", .{ style.primary, style.reset }); + std.debug.print("{s}\n", .{options}); + return; + }, + .list => doList = true, + .dryRun => dryRun = true, + .quiet => quiet = true, + .taskfile => |f| tasksArg = try readTaskfile(std.fs.cwd(), f), + }; + + if (opts.nextArg()) |arg| { + if (taskName != null or doList) fatal("Unexpected argument {s}", .{arg}); + + taskName = arg; + } else { + const tasks = tasksArg orelse try findTaskfile(); + + if (doList) return listTasks(tasks); + if (taskName) |a| return runTask(tasks, a); + + std.debug.print("{}Tasks in {s}\n\n", .{ style.primary, tasks.path }); + + for (tasks.list) |task| { + std.debug.print("{} {s}\n", .{ style.secondary, task.name }); + std.debug.print("{} {s}\n\n", .{ style.reset, task.desc }); + } + + return; + } } } @@ -18,29 +87,62 @@ const Taskfile = struct { list: []Task, }; -fn findTaskfile(alloc: std.mem.Allocator) !Taskfile { +fn runTask(tasks: Taskfile, name: []const u8) !void { + const task = for (tasks.list) |task| { + if (std.mem.eql(u8, name, task.name)) + break task; + } else fatal("No such task: {s}", .{name}); + + std.debug.print("{}maid {s}{}", .{ style.secondary, name, style.reset }); + + switch (task.code.lang) { + .shell => {}, + .haskell => {}, + .javascript => {}, + } +} + +fn listTasks(tasks: Taskfile) !void { + const out = std.io.getStdOut().writer(); + + for (tasks.list) |task| + try out.print("{s} {s}\n", .{ task.name, task.desc }); +} + +fn readTaskfile(dir: std.fs.Dir, path: []const u8) !?Taskfile { + const file = try dir.openFile(path, .{}); + var parser = try Parser.init(alloc); - var tasks = std.ArrayList(Task).init(alloc); + defer parser.deinit(); + + var tasks = try parser.parseMarkdown(file.reader()); + defer tasks.deinit(); + + if (tasks.items.len == 0) return null; + + return .{ + .path = path, + .list = try tasks.toOwnedSlice(), + }; +} + +fn findTaskfile() !Taskfile { var dir = std.fs.cwd(); for (0..16) |_| { - for ([_]([]const u8){ "README.md", "CONTRIBUTING.md" }) |name| { - const file = try dir.openFile(name, .{}); - try parser.parseMarkdown(file.reader(), &tasks); - - if (tasks.items.len > 0) return .{ - .path = name, - .list = try tasks.toOwnedSlice(), - }; - } + for ([_][]const u8{ "README.md", "CONTRIBUTING.md" }) |name| + return try readTaskfile(dir, name) orelse continue; dir = try dir.openDir("..", .{}); } - return error.NoTaskfile; + fatal("No taskfile", .{}); } +const alloc = std.heap.page_allocator; + const std = @import("std"); +const getopt = @import("getopt.zig"); const Parser = @import("Parser.zig"); const Style = @import("Style.zig"); const Task = @import("Task.zig");