Revise self-hosted command line interface
Commands are now separated more precisely from one another. Arguments are parsed mostly using a custom argument parser instead of manually. This should be on parity feature-wise with the previous main.zig but adds a few extra code-paths as well that were not yet implemented. Subcommands are much more prominent and consistent. The first argument is always a sub-command and then all following arguments refer to that command. Different commands display there own usage messages and options based on what they can do instead of a one-for-all usage message that was only applicable for the build commands previously. The `cc` command is added and is intended for driving a c compiler. See #490. This is currently a wrapper over the system cc and assumes that it exists, but it should suffice as a starting point.
This commit is contained in:
parent
281c17f6ae
commit
803f0a295b
284
src-self-hosted/arg.zig
Normal file
284
src-self-hosted/arg.zig
Normal file
@ -0,0 +1,284 @@
|
||||
const std = @import("std");
|
||||
const debug = std.debug;
|
||||
const mem = std.mem;
|
||||
|
||||
const Allocator = mem.Allocator;
|
||||
const ArrayList = std.ArrayList;
|
||||
const HashMap = std.HashMap;
|
||||
|
||||
fn trimStart(slice: []const u8, ch: u8) []const u8 {
|
||||
var i: usize = 0;
|
||||
for (slice) |b| {
|
||||
if (b != '-') break;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return slice[i..];
|
||||
}
|
||||
|
||||
fn argInAllowedSet(maybe_set: ?[]const []const u8, arg: []const u8) bool {
|
||||
if (maybe_set) |set| {
|
||||
for (set) |possible| {
|
||||
if (mem.eql(u8, arg, possible)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Modifies the current argument index during iteration
|
||||
fn readFlagArguments(allocator: &Allocator, args: []const []const u8, required: usize,
|
||||
allowed_set: ?[]const []const u8, index: &usize) !FlagArg {
|
||||
|
||||
switch (required) {
|
||||
0 => return FlagArg { .None = undefined }, // TODO: Required to force non-tag but value?
|
||||
1 => {
|
||||
if (*index + 1 >= args.len) {
|
||||
return error.MissingFlagArguments;
|
||||
}
|
||||
|
||||
*index += 1;
|
||||
const arg = args[*index];
|
||||
|
||||
if (!argInAllowedSet(allowed_set, arg)) {
|
||||
return error.ArgumentNotInAllowedSet;
|
||||
}
|
||||
|
||||
return FlagArg { .Single = arg };
|
||||
},
|
||||
else => |needed| {
|
||||
var extra = ArrayList([]const u8).init(allocator);
|
||||
errdefer extra.deinit();
|
||||
|
||||
var j: usize = 0;
|
||||
while (j < needed) : (j += 1) {
|
||||
if (*index + 1 >= args.len) {
|
||||
return error.MissingFlagArguments;
|
||||
}
|
||||
|
||||
*index += 1;
|
||||
const arg = args[*index];
|
||||
|
||||
if (!argInAllowedSet(allowed_set, arg)) {
|
||||
return error.ArgumentNotInAllowedSet;
|
||||
}
|
||||
|
||||
try extra.append(arg);
|
||||
}
|
||||
|
||||
return FlagArg { .Many = extra };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const HashMapFlags = HashMap([]const u8, FlagArg, std.hash.Fnv1a_32.hash, mem.eql_slice_u8);
|
||||
|
||||
// A store for querying found flags and positional arguments.
|
||||
pub const Args = struct {
|
||||
flags: HashMapFlags,
|
||||
positionals: ArrayList([]const u8),
|
||||
|
||||
pub fn parse(allocator: &Allocator, comptime spec: []const Flag, args: []const []const u8) !Args {
|
||||
var parsed = Args {
|
||||
.flags = HashMapFlags.init(allocator),
|
||||
.positionals = ArrayList([]const u8).init(allocator),
|
||||
};
|
||||
|
||||
var i: usize = 0;
|
||||
next: while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg.len != 0 and arg[0] == '-') {
|
||||
// TODO: hashmap, although the linear scan is okay for small argument sets as is
|
||||
for (spec) |flag| {
|
||||
if (mem.eql(u8, arg, flag.name)) {
|
||||
const flag_name_trimmed = trimStart(flag.name, '-');
|
||||
const flag_args = readFlagArguments(allocator, args, flag.required, flag.allowed_set, &i) catch |err| {
|
||||
switch (err) {
|
||||
error.ArgumentNotInAllowedSet => {
|
||||
std.debug.warn("argument '{}' is invalid for flag '{}'\n", args[i], arg);
|
||||
std.debug.warn("allowed options are ");
|
||||
for (??flag.allowed_set) |possible| {
|
||||
std.debug.warn("'{}' ", possible);
|
||||
}
|
||||
std.debug.warn("\n");
|
||||
},
|
||||
error.MissingFlagArguments => {
|
||||
std.debug.warn("missing argument for flag: {}\n", arg);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
|
||||
if (flag.mergable) {
|
||||
var prev =
|
||||
if (parsed.flags.get(flag_name_trimmed)) |entry|
|
||||
entry.value.Many
|
||||
else
|
||||
ArrayList([]const u8).init(allocator);
|
||||
|
||||
// MergeN creation disallows 0 length flag entry (doesn't make sense)
|
||||
switch (flag_args) {
|
||||
FlagArg.None => unreachable,
|
||||
FlagArg.Single => |inner| try prev.append(inner),
|
||||
FlagArg.Many => |inner| try prev.appendSlice(inner.toSliceConst()),
|
||||
}
|
||||
|
||||
_ = try parsed.flags.put(flag_name_trimmed, FlagArg { .Many = prev });
|
||||
} else {
|
||||
_ = try parsed.flags.put(flag_name_trimmed, flag_args);
|
||||
}
|
||||
|
||||
continue :next;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Better errors with context, global error state and return is sufficient.
|
||||
std.debug.warn("could not match flag: {}\n", arg);
|
||||
return error.UnknownFlag;
|
||||
} else {
|
||||
try parsed.positionals.append(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
pub fn deinit(self: &Args) void {
|
||||
self.flags.deinit();
|
||||
self.positionals.deinit();
|
||||
}
|
||||
|
||||
// e.g. --help
|
||||
pub fn present(self: &Args, name: []const u8) bool {
|
||||
return self.flags.contains(name);
|
||||
}
|
||||
|
||||
// e.g. --name value
|
||||
pub fn single(self: &Args, name: []const u8) ?[]const u8 {
|
||||
if (self.flags.get(name)) |entry| {
|
||||
switch (entry.value) {
|
||||
FlagArg.Single => |inner| { return inner; },
|
||||
else => @panic("attempted to retrieve flag with wrong type"),
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// e.g. --names value1 value2 value3
|
||||
pub fn many(self: &Args, name: []const u8) ?[]const []const u8 {
|
||||
if (self.flags.get(name)) |entry| {
|
||||
switch (entry.value) {
|
||||
FlagArg.Many => |inner| { return inner.toSliceConst(); },
|
||||
else => @panic("attempted to retrieve flag with wrong type"),
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Arguments for a flag. e.g. arg1, arg2 in `--command arg1 arg2`.
|
||||
const FlagArg = union(enum) {
|
||||
None,
|
||||
Single: []const u8,
|
||||
Many: ArrayList([]const u8),
|
||||
};
|
||||
|
||||
// Specification for how a flag should be parsed.
|
||||
pub const Flag = struct {
|
||||
name: []const u8,
|
||||
required: usize,
|
||||
mergable: bool,
|
||||
allowed_set: ?[]const []const u8,
|
||||
|
||||
pub fn Bool(comptime name: []const u8) Flag {
|
||||
return ArgN(name, 0);
|
||||
}
|
||||
|
||||
pub fn Arg1(comptime name: []const u8) Flag {
|
||||
return ArgN(name, 1);
|
||||
}
|
||||
|
||||
pub fn ArgN(comptime name: []const u8, comptime n: usize) Flag {
|
||||
return Flag {
|
||||
.name = name,
|
||||
.required = n,
|
||||
.mergable = false,
|
||||
.allowed_set = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ArgMergeN(comptime name: []const u8, comptime n: usize) Flag {
|
||||
if (n == 0) {
|
||||
@compileError("n must be greater than 0");
|
||||
}
|
||||
|
||||
return Flag {
|
||||
.name = name,
|
||||
.required = n,
|
||||
.mergable = true,
|
||||
.allowed_set = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn Option(comptime name: []const u8, comptime set: []const []const u8) Flag {
|
||||
return Flag {
|
||||
.name = name,
|
||||
.required = 1,
|
||||
.mergable = false,
|
||||
.allowed_set = set,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
test "parse arguments" {
|
||||
const spec1 = comptime []const Flag {
|
||||
Flag.Bool("--help"),
|
||||
Flag.Bool("--init"),
|
||||
Flag.Arg1("--build-file"),
|
||||
Flag.Option("--color", []const []const u8 { "on", "off", "auto" }),
|
||||
Flag.ArgN("--pkg-begin", 2),
|
||||
Flag.ArgMergeN("--object", 1),
|
||||
Flag.ArgN("--library", 1),
|
||||
};
|
||||
|
||||
const cliargs = []const []const u8 {
|
||||
"build",
|
||||
"--help",
|
||||
"pos1",
|
||||
"--build-file", "build.zig",
|
||||
"--object", "obj1",
|
||||
"--object", "obj2",
|
||||
"--library", "lib1",
|
||||
"--library", "lib2",
|
||||
"--color", "on",
|
||||
"pos2",
|
||||
};
|
||||
|
||||
var args = try Args.parse(std.debug.global_allocator, spec1, cliargs);
|
||||
|
||||
debug.assert(args.present("help"));
|
||||
debug.assert(!args.present("help2"));
|
||||
debug.assert(!args.present("init"));
|
||||
|
||||
debug.assert(mem.eql(u8, ??args.single("build-file"), "build.zig"));
|
||||
debug.assert(mem.eql(u8, ??args.single("color"), "on"));
|
||||
|
||||
const objects = ??args.many("object");
|
||||
debug.assert(mem.eql(u8, objects[0], "obj1"));
|
||||
debug.assert(mem.eql(u8, objects[1], "obj2"));
|
||||
|
||||
debug.assert(mem.eql(u8, ??args.single("library"), "lib2"));
|
||||
|
||||
const pos = args.positionals.toSliceConst();
|
||||
debug.assert(mem.eql(u8, pos[0], "build"));
|
||||
debug.assert(mem.eql(u8, pos[1], "pos1"));
|
||||
debug.assert(mem.eql(u8, pos[2], "pos2"));
|
||||
}
|
71
src-self-hosted/introspect.zig
Normal file
71
src-self-hosted/introspect.zig
Normal file
@ -0,0 +1,71 @@
|
||||
// Introspection and determination of system libraries needed by zig.
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const os = std.os;
|
||||
|
||||
const warn = std.debug.warn;
|
||||
|
||||
/// Caller must free result
|
||||
pub fn testZigInstallPrefix(allocator: &mem.Allocator, test_path: []const u8) ![]u8 {
|
||||
const test_zig_dir = try os.path.join(allocator, test_path, "lib", "zig");
|
||||
errdefer allocator.free(test_zig_dir);
|
||||
|
||||
const test_index_file = try os.path.join(allocator, test_zig_dir, "std", "index.zig");
|
||||
defer allocator.free(test_index_file);
|
||||
|
||||
var file = try os.File.openRead(allocator, test_index_file);
|
||||
file.close();
|
||||
|
||||
return test_zig_dir;
|
||||
}
|
||||
|
||||
/// Caller must free result
|
||||
pub fn findZigLibDir(allocator: &mem.Allocator) ![]u8 {
|
||||
const self_exe_path = try os.selfExeDirPath(allocator);
|
||||
defer allocator.free(self_exe_path);
|
||||
|
||||
var cur_path: []const u8 = self_exe_path;
|
||||
while (true) {
|
||||
const test_dir = os.path.dirname(cur_path);
|
||||
|
||||
if (mem.eql(u8, test_dir, cur_path)) {
|
||||
break;
|
||||
}
|
||||
|
||||
return testZigInstallPrefix(allocator, test_dir) catch |err| {
|
||||
cur_path = test_dir;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO look in hard coded installation path from configuration
|
||||
//if (ZIG_INSTALL_PREFIX != nullptr) {
|
||||
// if (test_zig_install_prefix(buf_create_from_str(ZIG_INSTALL_PREFIX), out_path)) {
|
||||
// return 0;
|
||||
// }
|
||||
//}
|
||||
|
||||
return error.FileNotFound;
|
||||
}
|
||||
|
||||
pub fn resolveZigLibDir(allocator: &mem.Allocator, zig_install_prefix_arg: ?[]const u8) ![]u8 {
|
||||
if (zig_install_prefix_arg) |zig_install_prefix| {
|
||||
return testZigInstallPrefix(allocator, zig_install_prefix) catch |err| {
|
||||
warn("No Zig installation found at prefix {}: {}\n", zig_install_prefix_arg, @errorName(err));
|
||||
return error.ZigInstallationNotFound;
|
||||
};
|
||||
} else {
|
||||
return findZigLibDir(allocator) catch |err| {
|
||||
warn(
|
||||
\\Unable to find zig lib directory: {}.
|
||||
\\Reinstall Zig or use --zig-install-prefix.
|
||||
\\
|
||||
,
|
||||
@errorName(err)
|
||||
);
|
||||
|
||||
return error.ZigLibDirNotFound;
|
||||
};
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -109,6 +109,29 @@ pub const Module = struct {
|
||||
LlvmIr,
|
||||
};
|
||||
|
||||
pub const CliPkg = struct {
|
||||
name: []const u8,
|
||||
path: []const u8,
|
||||
children: ArrayList(&CliPkg),
|
||||
parent: ?&CliPkg,
|
||||
|
||||
pub fn init(allocator: &mem.Allocator, name: []const u8, path: []const u8, parent: ?&CliPkg) !&CliPkg {
|
||||
var pkg = try allocator.create(CliPkg);
|
||||
pkg.name = name;
|
||||
pkg.path = path;
|
||||
pkg.children = ArrayList(&CliPkg).init(allocator);
|
||||
pkg.parent = parent;
|
||||
return pkg;
|
||||
}
|
||||
|
||||
pub fn deinit(self: &CliPkg) void {
|
||||
for (self.children.toSliceConst()) |child| {
|
||||
child.deinit();
|
||||
}
|
||||
self.children.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn create(allocator: &mem.Allocator, name: []const u8, root_src_path: ?[]const u8, target: &const Target,
|
||||
kind: Kind, build_mode: builtin.Mode, zig_lib_dir: []const u8, cache_dir: []const u8) !&Module
|
||||
{
|
||||
|
@ -85,6 +85,14 @@ pub const File = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn exists(allocator: &mem.Allocator, path: []const u8) bool {
|
||||
if (openRead(allocator, path)) |*file| {
|
||||
file.close();
|
||||
return true;
|
||||
} else |_| {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Upon success, the stream is in an uninitialized state. To continue using it,
|
||||
/// you must use the open() function.
|
||||
|
Loading…
x
Reference in New Issue
Block a user