Skip to content

Commit

Permalink
Refactor parser, help writer, arg matches and tokenizer
Browse files Browse the repository at this point in the history
A lot of implementation are refactored but among others parser is the main focus
of this change.

- Clean up the `Parser.zig` by moving `ShortOption` into its own file
  `ShortOptionIterator.zig` file.
- Separate the parser related error and printing implementation to
  separate file `parse_error.zig`.
- Add a bit nicer way to store and print the error context.
- Improve error names.
- Introduce new `ParseResult` as a main type returned by the parser and
  let `ArgMatches` wrapped it.
- Document the implementation of parser.

Help writer changes:
- Rename `help.zig` to `HelpMessageWriter.zig`.
- Clean up the implementation.

Tokenizer changes:
- Document the API properly.
- Use more nicer variable names.

Signed-off-by: prajwalch <[email protected]>
  • Loading branch information
prajwalch committed Sep 4, 2024
1 parent cb43fa0 commit 1288c8c
Show file tree
Hide file tree
Showing 15 changed files with 1,575 additions and 1,248 deletions.
105 changes: 49 additions & 56 deletions src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ const App = @This();

const std = @import("std");
const builtin = @import("builtin");
const help = @import("help.zig");
const Allocator = std.mem.Allocator;
const ArgMatches = @import("ArgMatches.zig");
const Command = @import("Command.zig");
const Parser = @import("Parser.zig");
const ArgMatches = @import("arg_matches.zig").ArgMatches;
const Tokenizer = @import("tokenizer.zig").Tokenizer;
const HelpMessageWriter = @import("HelpMessageWriter.zig");
const Parser = @import("parser/Parser.zig");
const ParseResult = @import("./parser/ParseResult.zig");
const parse_error = @import("./parser/parse_error.zig");
const YazapError = @import("error.zig").YazapError;

const Allocator = std.mem.Allocator;

/// Top level allocator for the entire library.
allocator: Allocator,
/// Root command of the app.
command: Command,
subcommand_help: ?help.Help = null,
/// Core structure containing parse result.
///
/// It is not intended for direct access, use `ArgMatches` instead.
parse_result: ?ParseResult = null,
/// Public container to query parse result.
///
/// It cab be access directly but for the most part `parseFrom` or `parseProcess`
/// returns it.
arg_matches: ?ArgMatches = null,
/// Raw buffer containing command line arguments.
process_args: ?[]const [:0]u8 = null,

/// Creates a new instance of `App`.
Expand All @@ -41,13 +51,15 @@ pub fn init(allocator: Allocator, name: []const u8, description: ?[]const u8) Ap
/// defer app.deinit();
/// ```
pub fn deinit(self: *App) void {
if (self.arg_matches) |*matches| matches.deinit();
if (self.process_args) |pargs| std.process.argsFree(self.allocator, pargs);
self.command.deinit();

if (self.subcommand_help) |subcmd_help| {
subcmd_help.parents.?.deinit();
if (self.parse_result) |*parse_result| {
parse_result.deinit();
}
if (self.process_args) |args| {
std.process.argsFree(self.allocator, args);
}
self.command.deinit();
self.parse_result = null;
self.arg_matches = null;
}

/// Creates a new `Command` with given name and optional description.
Expand Down Expand Up @@ -114,14 +126,23 @@ pub fn parseProcess(self: *App) YazapError!(*const ArgMatches) {
/// const matches = try app.parseFrom(&.{ "arg1", "--some-option" "subcmd" });
/// ```
pub fn parseFrom(self: *App, argv: []const [:0]const u8) YazapError!(*const ArgMatches) {
var parser = Parser.init(self.allocator, Tokenizer.init(argv), self.rootCommand());
self.arg_matches = parser.parse() catch |e| {
var parser = Parser.init(self.allocator, argv, self.rootCommand());

const parse_result = parser.parse() catch |err| {
// Don't clutter the test result with error messages.
if (!builtin.is_test) {
try parser.err.log(e);
try parse_error.print(parser.error_context);
}
return e;
return err;
};
try self.handleHelpOption();
if (parse_result.getCommandContainingHelpFlag()) |command| {
var help_writer = HelpMessageWriter.init(command);
try help_writer.write();
self.deinit();
std.process.exit(0);
}
self.parse_result = parse_result;
self.arg_matches = ArgMatches{ .parse_result = &self.parse_result.? };
return &self.arg_matches.?;
}

Expand All @@ -148,13 +169,11 @@ pub fn parseFrom(self: *App, argv: []const [:0]const u8) YazapError!(*const ArgM
/// return;
/// }
/// ```
pub fn displayHelp(self: *App) !void {
var cmd_help = help.Help.init(
self.allocator,
self.rootCommand(),
self.rootCommand().name,
) catch unreachable;
return cmd_help.writeAll(std.io.getStdErr().writer());
pub fn displayHelp(self: *App) YazapError!void {
if (self.parse_result) |parse_result| {
var help_writer = HelpMessageWriter.init(parse_result.getCommand());
try help_writer.write();
}
}

/// Displays the usage message of specified subcommand on the command line.
Expand All @@ -181,39 +200,13 @@ pub fn displayHelp(self: *App) !void {
/// if (matches.subcommandMatches("subcmd")) |subcmd_matches| {
/// if (!subcmd_matches.containsArgs()) {
/// try app.displaySubcommandHelp();
/// return;
/// }
/// }
/// ```
pub fn displaySubcommandHelp(self: *App) !void {
if (self.subcommand_help) |*h| return h.writeAll(std.io.getStdErr().writer());
}

fn handleHelpOption(self: *App) !void {
if (help.findSubcommand(self.rootCommand(), &self.arg_matches.?)) |subcmd| {
self.subcommand_help = try help.Help.init(
self.allocator,
self.rootCommand(),
subcmd,
);
}
try self.displayHelpAndExitIfFound();
}
pub fn displaySubcommandHelp(self: *App) YazapError!void {
const parse_result = self.parse_result orelse return;

fn displayHelpAndExitIfFound(self: *App) !void {
var arg_matches = self.arg_matches.?;
var help_displayed = false;

if (arg_matches.containsArg("help")) {
try self.displayHelp();
help_displayed = true;
} else {
try self.displaySubcommandHelp();
help_displayed = (self.subcommand_help != null);
}

if (help_displayed) {
self.deinit();
std.process.exit(0);
if (parse_result.getActiveSubcommand()) |subcmd| {
var help_writer = HelpMessageWriter.init(subcmd);
try help_writer.write();
}
}
163 changes: 163 additions & 0 deletions src/ArgMatches.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! A structure for querying the parse result.
const ArgMatches = @This();

const std = @import("std");
const ParseResult = @import("./parser/ParseResult.zig");

/// Core structure containing parse result.
///
/// It is not intended to use directly instead methods provided by this
/// structure should be use.
parse_result: *const ParseResult,

/// Checks whether any arguments were present on the command line.
///
/// ## Examples
///
/// ```zig
/// var app = App.init(allocator, "myapp", "My app description");
/// defer app.deinit();
///
/// var root = app.rootCommand();
/// try root.addArg(Arg.booleanOption("verbose", 'v', "Enable verbose output"));
/// try root.addSubcommand(app.createCommand("init-exe", "Initilize project"));
///
/// const matches = try app.parseProcess();
///
/// if (!matches.containsArgs()) {
/// try app.displayHelp();
/// return;
/// }
/// ```
pub fn containsArgs(self: *const ArgMatches) bool {
// zig fmt: off
return (self.parse_result.getArgs().count() >= 1
or self.parse_result.getSubcommandParseResult() != null);
// zig fmt: on
}

/// Checks whether an option, positional argument or subcommand with the
/// specified name was present on the command line.
///
/// ## Examples
///
/// ```zig
/// var app = App.init(allocator, "myapp", "My app description");
/// defer app.deinit();
///
/// var root = app.rootCommand();
/// try root.addArg(Arg.booleanOption("verbose", 'v', "Enable verbose output"));
///
/// // Define a subcommand
/// var build_cmd = app.createCommand("build", "Build the project");
/// try build_cmd.addArg(Arg.booleanOption("release", 'r', "Build in release mode"));
/// try root.addSubcommand(build_cmd);
///
/// const matches = try app.parseProcess();
///
/// if (matches.containsArg("verbose")) {
/// // Handle verbose operation
/// }
///
/// if (matches.containsArg("build")) {
/// const build_cmd_matches = matches.subcommandMatches("build").?;
///
/// if (build_cmd_matches.containsArg("release")) {
/// // Build for release mode
/// }
/// }
///
/// ```
pub fn containsArg(self: *const ArgMatches, arg: []const u8) bool {
if (self.parse_result.getArgs().contains(arg)) {
return true;
} else if (self.parse_result.getSubcommandParseResult()) |subcmd_result| {
return std.mem.eql(u8, subcmd_result.getCommand().deref().name, arg);
}
return false;
}

/// Returns the value of an option or positional argument if it was present
/// on the command line; otherwise, returns `null`.
///
/// ## Examples
///
/// ```zig
/// var app = App.init(allocator, "myapp", "My app description");
/// defer app.deinit();
///
/// var root = app.rootCommand();
/// try root.addArg(Arg.singleValueOption("config", 'c', "Config file"));
///
/// const matches = try app.parseProcess();
///
/// if (matches.getSingleValue("config")) |config_file| {
/// std.debug.print("Config file name: {s}", .{config_file});
/// }
/// ```
pub fn getSingleValue(self: *const ArgMatches, arg: []const u8) ?[]const u8 {
if (self.parse_result.getArgs().get(arg)) |value| {
if (value.isSingle()) return value.single;
}
return null;
}

/// Returns the values of an option if it was present on the command line;
/// otherwise, returns `null`.
///
/// ## Examples
///
/// ```zig
/// var app = App.init(allocator, "myapp", "My app description");
/// defer app.deinit();
///
/// var root = app.rootCommand();
/// try root.addArg(Arg.multiValuesOption("nums", 'n', "Numbers to add", 2));
///
/// const matches = try app.parseProcess();
///
/// if (matches.getMultiValues("nums")) |numbers| {
/// std.debug.print("Add {s} + {s}", .{ numbers[0], numbers[1] });
/// }
/// ```
pub fn getMultiValues(self: *const ArgMatches, arg: []const u8) ?[][]const u8 {
if (self.parse_result.getArgs().get(arg)) |value| {
if (value.isMany()) return value.many.items[0..];
}
return null;
}

/// Returns the `ArgMatches` for a specific subcommand if it was present on
/// on the command line; otherwise, returns `null`.
///
/// ## Examples
///
/// ```zig
/// var app = App.init(allocator, "myapp", "My app description");
/// defer app.deinit();
///
/// var root = app.rootCommand();
///
/// var build_cmd = app.createCommand("build", "Build the project");
/// try build_cmd.addArg(Arg.booleanOption("release", 'r', "Build in release mode"));
/// try build_cmd.addArg(Arg.singleValueOption("target", 't', "Build for given target"));
/// try root.addSubcommand(build_cmd);
///
/// const matches = try app.parseProcess();
///
/// if (matches.subcommandMatches("build")) |build_cmd_matches| {
/// if (build_cmd_matches.containsArg("release")) {
/// const target = build_cmd_matches.getSingleValue("target") orelse "default";
/// // Build for release mode to given target
/// }
/// }
///
/// ```
pub fn subcommandMatches(self: *const ArgMatches, subcmd: []const u8) ?ArgMatches {
if (self.parse_result.getSubcommandParseResult()) |subcmd_result| {
if (std.mem.eql(u8, subcmd_result.getCommand().deref().name, subcmd)) {
return ArgMatches{ .parse_result = subcmd_result };
}
}
return null;
}
Loading

0 comments on commit 1288c8c

Please sign in to comment.