cli flags
This commit is contained in:
parent
f40aafa501
commit
7121dd9dea
3 changed files with 348 additions and 5 deletions
|
|
@ -108,7 +108,7 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm
|
|||
|
||||
- **FR-6.1**: Invoke as `tally "<expression>"` for standard mode evaluation.
|
||||
- **FR-6.2**: Flag `--programmer` or `-p` for programmer mode: `tally -p "0xFF & 0x0F"`.
|
||||
- **FR-6.3**: Flag `--bits <8|16|32|64>` to set bit width in programmer mode (default: 64).
|
||||
- **FR-6.3**: Flag `--bits <8|16|32|64|128>` to set bit width in programmer mode (default: 64). Accepted as `--bits 8` or `--bits=8`. It implies `-p`, because standard mode is fixed at 64-bit signed and the flag would mean nothing there.
|
||||
- **FR-6.4**: Flag `--base <dec|hex|oct|bin>` to control primary output format (default: show all).
|
||||
- **FR-6.5**: Subcommand `tally struct <definition_or_file>` for struct layout - accepts inline DSL string or path to a file containing the definition.
|
||||
- **FR-6.6**: Subcommand `tally cagr <start> <end> <periods>` for quick CAGR.
|
||||
|
|
@ -117,6 +117,10 @@ A calculator application with three frontends (CLI, TUI, Android) sharing a comm
|
|||
- **FR-6.9**: Output format flags: `--json` for machine-readable output, plain text default.
|
||||
- **FR-6.10**: Exit code 0 on success, non-zero on parse/evaluation errors with stderr message.
|
||||
- **FR-6.11**: Subcommand `tally amort <principal> <rate> <periods> [payment]` prints an amortization schedule as a table. Flags: `--monthly` (the rate given is an annual nominal rate charged over monthly periods, so rate/12 applies per period), `--summary` (totals only, no per-period rows), `--exact` (skip cent rounding). A table is the one financial output that cannot be an expression function, which is why it is a subcommand.
|
||||
- **FR-6.12**: Flags `--signed` (default) and `--unsigned` set how programmer-mode values are interpreted, which decides both the displayed rows and whether `>>` extends the sign. Both imply `-p`.
|
||||
- **FR-6.13**: Flag `--endian <big|little>` (also `be`/`le`, case-insensitive) sets the byte order of the hex and ASCII rows, matching the TUI's Ctrl-E toggle. Default big-endian, so the hex row reads as the number itself (FR-2.8). Implies `-p`.
|
||||
- **FR-6.14**: Every subcommand answers `-h`/`--help` with its own usage on stdout and exit code 0. Getting a subcommand's arguments wrong prints the same usage on stderr with a non-zero exit code.
|
||||
- **FR-6.15**: An unrecognized flag is an error naming `tally --help`, not part of the expression. `tally --bit 8 '1'` used to be evaluated as the expression `--bit 8 1` and reported as an unexpected token.
|
||||
|
||||
### FR-7: TUI Frontend
|
||||
|
||||
|
|
|
|||
|
|
@ -756,6 +756,30 @@ Remaining subcommands deferred until their engine modules exist.
|
|||
- NOT DONE: mouse wheel scrolling for history
|
||||
- Verify: help overlay works, mouse interactions work, looks reasonable in 80x24 terminal
|
||||
|
||||
### Task 5.11: CLI programmer-mode flags and subcommand help
|
||||
|
||||
The CLI could reach programmer mode with `-p` but not configure it, so the width,
|
||||
signedness and byte order the TUI exposes were unreachable from the command line.
|
||||
|
||||
- `--bits <8|16|32|64|128>` (FR-6.3), `--signed`/`--unsigned` (FR-6.12) and
|
||||
`--endian <big|little>` (FR-6.13). Each accepts the separated and `=` spellings
|
||||
and implies `-p`, since standard mode is fixed at 64-bit signed.
|
||||
- The accepted widths come from the `BitWidth` enum rather than a hand-written list,
|
||||
and a comptime check fails the build if the enum changes without the help text
|
||||
being updated.
|
||||
- `-h`/`--help` on every subcommand, on stdout with exit code 0 (FR-6.14). The usage
|
||||
text used to be reachable only by getting the arguments wrong.
|
||||
- An unrecognized long flag is now an error naming `tally --help` instead of being
|
||||
joined into the expression (FR-6.15): `tally --bit 8 '1'` reported "unexpected
|
||||
token", which pointed at the wrong thing.
|
||||
- Verify: 938 tests pass, including width/signedness/endianness reaching the
|
||||
formatted result and every rejected flag spelling. Checked by hand:
|
||||
`tally --bits 8 '0xFF + 1'` is 0, `--bits 8 --signed '0xFF >> 1'` is -1 while
|
||||
`--unsigned` gives 127, `--endian little '0xDEAD'` reverses the hex row.
|
||||
|
||||
NOT DONE, still open from FR-6: `--base` (FR-6.4), `--json` (FR-6.9), and the
|
||||
`struct`, `cagr` and `tvm` subcommands (FR-6.5 to FR-6.7).
|
||||
|
||||
### Task 5.10: Act on the full-codebase review [PARTIAL]
|
||||
|
||||
A four-part review of the whole codebase (numeric core, language layer, domain
|
||||
|
|
|
|||
323
src/main.zig
323
src/main.zig
|
|
@ -14,6 +14,10 @@ pub const ParsedArgs = union(enum) {
|
|||
expression: struct {
|
||||
text: []const u8,
|
||||
mode: engine.Mode,
|
||||
/// Width, signedness and byte order for programmer mode. The CLI accepts
|
||||
/// the same settings the TUI has, so a session in one can be reproduced in
|
||||
/// the other.
|
||||
config: engine.types.ProgrammerConfig = .{},
|
||||
},
|
||||
conversion: struct {
|
||||
/// Kept as text so it can be parsed exactly rather than through f64.
|
||||
|
|
@ -34,6 +38,7 @@ pub const ParsedArgs = union(enum) {
|
|||
|
||||
pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedArgs {
|
||||
var mode: engine.Mode = .standard;
|
||||
var config: engine.types.ProgrammerConfig = .{};
|
||||
var expr_parts = std.ArrayList([]const u8).empty;
|
||||
defer expr_parts.deinit(allocator);
|
||||
|
||||
|
|
@ -49,7 +54,9 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA
|
|||
return parseAmortArgs(args[1..]);
|
||||
}
|
||||
|
||||
for (args) |arg| {
|
||||
var i: usize = 0;
|
||||
while (i < args.len) : (i += 1) {
|
||||
const arg = args[i];
|
||||
if (std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--programmer")) {
|
||||
mode = .programmer;
|
||||
} else if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) {
|
||||
|
|
@ -59,7 +66,43 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA
|
|||
} };
|
||||
} else if (std.mem.eql(u8, arg, "--version")) {
|
||||
return .{ .output = .{ .text = "tally 0.1.0\n", .is_error = false } };
|
||||
} else if (std.mem.eql(u8, arg, "--signed")) {
|
||||
config.signedness = .signed;
|
||||
mode = .programmer;
|
||||
} else if (std.mem.eql(u8, arg, "--unsigned")) {
|
||||
config.signedness = .unsigned;
|
||||
mode = .programmer;
|
||||
} else {
|
||||
// Flags that take a value, accepted as either `--bits 8` or `--bits=8`.
|
||||
switch (flagValue(arg, "--bits", args, &i)) {
|
||||
.absent => {},
|
||||
.missing => return .{ .output = .{ .text = bits_usage, .is_error = true } },
|
||||
.value => |text| {
|
||||
config.bit_width = parseBitWidth(text) orelse
|
||||
return .{ .output = .{ .text = bits_usage, .is_error = true } };
|
||||
// The width only means something in programmer mode: standard
|
||||
// mode is fixed at 64-bit signed. Asking for a width is asking
|
||||
// for programmer mode, so it implies -p rather than erroring.
|
||||
mode = .programmer;
|
||||
continue;
|
||||
},
|
||||
}
|
||||
switch (flagValue(arg, "--endian", args, &i)) {
|
||||
.absent => {},
|
||||
.missing => return .{ .output = .{ .text = endian_usage, .is_error = true } },
|
||||
.value => |text| {
|
||||
config.display_endian = parseEndian(text) orelse
|
||||
return .{ .output = .{ .text = endian_usage, .is_error = true } };
|
||||
mode = .programmer;
|
||||
continue;
|
||||
},
|
||||
}
|
||||
// An unrecognised long flag is a mistake, not part of the expression.
|
||||
// `--bit 8` used to be evaluated as an expression and reported as an
|
||||
// unexpected token.
|
||||
if (std.mem.startsWith(u8, arg, "--")) {
|
||||
return .{ .output = .{ .text = unknown_flag_usage, .is_error = true } };
|
||||
}
|
||||
expr_parts.append(allocator, arg) catch {
|
||||
return .{ .output = .{ .text = "error: out of memory\n", .is_error = true } };
|
||||
};
|
||||
|
|
@ -74,9 +117,96 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA
|
|||
return .{ .output = .{ .text = "error: out of memory\n", .is_error = true } };
|
||||
};
|
||||
|
||||
return .{ .expression = .{ .text = expression, .mode = mode } };
|
||||
return .{ .expression = .{ .text = expression, .mode = mode, .config = config } };
|
||||
}
|
||||
|
||||
/// The value of a `--name value` or `--name=value` flag.
|
||||
const FlagValue = union(enum) {
|
||||
/// This argument is not that flag.
|
||||
absent,
|
||||
/// It is that flag, but no value followed it.
|
||||
missing,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
/// Read the value of a flag, consuming the following argument in the separated
|
||||
/// form. `index` advances only when the value was taken from the next argument.
|
||||
fn flagValue(arg: []const u8, name: []const u8, args: []const []const u8, index: *usize) FlagValue {
|
||||
if (!std.mem.startsWith(u8, arg, name)) return .absent;
|
||||
if (arg.len == name.len) {
|
||||
if (index.* + 1 >= args.len) return .missing;
|
||||
index.* += 1;
|
||||
return .{ .value = args[index.*] };
|
||||
}
|
||||
if (arg[name.len] == '=') {
|
||||
const text = arg[name.len + 1 ..];
|
||||
if (text.len == 0) return .missing;
|
||||
return .{ .value = text };
|
||||
}
|
||||
// A longer flag that merely starts with the same letters, e.g. `--bitsy`.
|
||||
return .absent;
|
||||
}
|
||||
|
||||
fn parseBitWidth(text: []const u8) ?engine.types.BitWidth {
|
||||
// Driven by the enum, so a width added to `BitWidth` is accepted here without
|
||||
// a second list to update.
|
||||
inline for (@typeInfo(engine.types.BitWidth).@"enum".fields) |field| {
|
||||
if (std.mem.eql(u8, text, comptime std.fmt.comptimePrint("{d}", .{field.value}))) {
|
||||
return @enumFromInt(field.value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The accepted widths, as they appear in messages: "8, 16, 32, 64, 128".
|
||||
const bit_width_list = blk: {
|
||||
var list: []const u8 = "";
|
||||
for (@typeInfo(engine.types.BitWidth).@"enum".fields, 0..) |field, i| {
|
||||
list = list ++ (if (i == 0) "" else ", ") ++ std.fmt.comptimePrint("{d}", .{field.value});
|
||||
}
|
||||
break :blk list;
|
||||
};
|
||||
|
||||
comptime {
|
||||
// `help_text` spells the same list out, because splicing it into the middle of
|
||||
// a multiline literal costs more than it saves. This makes a change to
|
||||
// `BitWidth` a compile error instead of a stale help screen.
|
||||
if (!std.mem.eql(u8, bit_width_list, "8, 16, 32, 64, 128")) {
|
||||
@compileError("BitWidth changed: update the --bits line in help_text");
|
||||
}
|
||||
}
|
||||
|
||||
fn parseEndian(text: []const u8) ?engine.types.Endianness {
|
||||
if (std.ascii.eqlIgnoreCase(text, "big") or std.ascii.eqlIgnoreCase(text, "be")) return .big;
|
||||
if (std.ascii.eqlIgnoreCase(text, "little") or std.ascii.eqlIgnoreCase(text, "le")) return .little;
|
||||
return null;
|
||||
}
|
||||
|
||||
const bits_usage = "error: --bits takes one of " ++ bit_width_list ++
|
||||
\\
|
||||
\\
|
||||
\\ tally -p --bits 8 '0xFF + 1'
|
||||
\\
|
||||
\\The width applies to programmer mode; standard mode is fixed at 64-bit signed.
|
||||
\\
|
||||
;
|
||||
|
||||
const endian_usage =
|
||||
\\error: --endian takes big or little
|
||||
\\
|
||||
\\ tally -p --endian little '0xDEADBEEF'
|
||||
\\
|
||||
\\The byte order affects the hex and ASCII rows only.
|
||||
\\
|
||||
;
|
||||
|
||||
const unknown_flag_usage =
|
||||
\\error: unknown flag
|
||||
\\
|
||||
\\Run 'tally --help' for the list of flags.
|
||||
\\
|
||||
;
|
||||
|
||||
/// Parse the arguments following the `convert` subcommand.
|
||||
///
|
||||
/// Accepts `<value> <from> <to>`, either separator word between the units
|
||||
|
|
@ -90,6 +220,9 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const []const u8) ParsedA
|
|||
/// bare expressions.
|
||||
fn parseConvertArgs(args: []const []const u8) ParsedArgs {
|
||||
const max_tokens = 8;
|
||||
if (wantsHelp(args)) {
|
||||
return .{ .output = .{ .text = convert_usage, .is_error = false } };
|
||||
}
|
||||
if (args.len < 2 or args.len > max_tokens) {
|
||||
return .{ .output = .{ .text = convert_usage, .is_error = true } };
|
||||
}
|
||||
|
|
@ -272,8 +405,19 @@ fn formatConversionUnits(
|
|||
|
||||
/// Evaluate an expression and format the result as a string.
|
||||
pub fn evaluate(allocator: std.mem.Allocator, expression: []const u8, mode: engine.Mode, buf: []u8) CliResult {
|
||||
return evaluateWith(allocator, expression, mode, .{}, buf);
|
||||
}
|
||||
|
||||
/// Evaluate with an explicit programmer configuration (width, signedness, byte
|
||||
/// order). Standard mode ignores it: it is fixed at 64-bit signed.
|
||||
pub fn evaluateWith(
|
||||
allocator: std.mem.Allocator,
|
||||
expression: []const u8,
|
||||
mode: engine.Mode,
|
||||
config: engine.types.ProgrammerConfig,
|
||||
buf: []u8,
|
||||
) CliResult {
|
||||
if (mode == .programmer) {
|
||||
const config = engine.types.ProgrammerConfig{};
|
||||
const result = engine.evalProgrammerString(allocator, expression, config) catch |err| {
|
||||
return .{ .output = errorMessage(err), .is_error = true };
|
||||
};
|
||||
|
|
@ -409,6 +553,18 @@ fn decoratedError(err: engine.CalcError) []const u8 {
|
|||
};
|
||||
}
|
||||
|
||||
/// True when a subcommand's arguments ask for its usage rather than run it.
|
||||
///
|
||||
/// Every subcommand answers `-h`/`--help` on stdout with exit status 0. The usage
|
||||
/// text was previously reachable only by getting the arguments wrong, which sent it
|
||||
/// to stderr with a non-zero status.
|
||||
fn wantsHelp(args: []const []const u8) bool {
|
||||
for (args) |arg| {
|
||||
if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const convert_usage =
|
||||
\\usage: tally convert <value> <from-unit> [to] <to-unit>
|
||||
\\
|
||||
|
|
@ -448,6 +604,10 @@ fn parseAmortArgs(args: []const []const u8) ParsedArgs {
|
|||
var monthly = false;
|
||||
var round_cents = true;
|
||||
|
||||
if (wantsHelp(args)) {
|
||||
return .{ .output = .{ .text = amort_usage, .is_error = false } };
|
||||
}
|
||||
|
||||
for (args) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--summary")) {
|
||||
summary_only = true;
|
||||
|
|
@ -616,12 +776,22 @@ const help_text =
|
|||
\\
|
||||
\\Options:
|
||||
\\ -p, --programmer Programmer mode (^ = power, xor = XOR)
|
||||
\\ --bits <N> Bit width for programmer mode: 8, 16, 32, 64, 128
|
||||
\\ (default 64). Implies -p. Standard mode is always
|
||||
\\ 64-bit signed.
|
||||
\\ --signed Treat programmer values as two's complement (default)
|
||||
\\ --unsigned Treat programmer values as unsigned. Implies -p.
|
||||
\\ --endian <order> Byte order of the hex and ASCII rows: big (default)
|
||||
\\ or little. Implies -p.
|
||||
\\ -h, --help Show this help
|
||||
\\ --version Show version
|
||||
\\
|
||||
\\Examples:
|
||||
\\ tally '2 + 3 * 4'
|
||||
\\ tally -p '0xFF and 0x0F'
|
||||
\\ tally --bits 8 '0xFF + 1' Wraps at 8 bits
|
||||
\\ tally --bits 8 --unsigned '0xFF >> 1'
|
||||
\\ tally --endian little '0xDEADBEEF'
|
||||
\\ tally 100 km to mi Unit conversion ('to' or 'in')
|
||||
\\ tally 32F in C
|
||||
\\ tally '2*3 kg to lb' The value may be an expression
|
||||
|
|
@ -641,6 +811,8 @@ const help_text =
|
|||
\\ tally convert 100 km to mi Explicit unit conversion
|
||||
\\ tally amort 200000 6 360 --monthly Amortization schedule
|
||||
\\
|
||||
\\Each subcommand takes -h for its own usage: tally amort --help
|
||||
\\
|
||||
\\Run with no arguments to start the interactive TUI.
|
||||
\\
|
||||
;
|
||||
|
|
@ -676,7 +848,7 @@ pub fn main(init: std.process.Init) u8 {
|
|||
},
|
||||
.expression => |expr| {
|
||||
var buf: [4096]u8 = undefined;
|
||||
const result = evaluate(allocator, expr.text, expr.mode, &buf);
|
||||
const result = evaluateWith(allocator, expr.text, expr.mode, expr.config, &buf);
|
||||
const file = if (result.is_error) std.Io.File.stderr() else std.Io.File.stdout();
|
||||
write(io, file, result.output);
|
||||
if (!result.is_error) write(io, std.Io.File.stdout(), "\n");
|
||||
|
|
@ -758,6 +930,149 @@ test "parseArgs: --programmer long flag" {
|
|||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --bits sets the width, in either spelling, and implies -p" {
|
||||
for ([_][]const []const u8{
|
||||
&.{ "--bits", "8", "0xFF" },
|
||||
&.{ "--bits=8", "0xFF" },
|
||||
}) |args| {
|
||||
const e = parseArgs(testing.allocator, args).expression;
|
||||
defer testing.allocator.free(e.text);
|
||||
try testing.expectEqual(engine.types.BitWidth.bits8, e.config.bit_width);
|
||||
// Asking for a width is asking for programmer mode: standard mode is fixed
|
||||
// at 64-bit signed, so the flag would mean nothing there.
|
||||
try testing.expectEqual(engine.Mode.programmer, e.mode);
|
||||
try testing.expectEqualStrings("0xFF", e.text);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: every documented bit width is accepted" {
|
||||
for ([_]struct { text: []const u8, expected: engine.types.BitWidth }{
|
||||
.{ .text = "8", .expected = .bits8 },
|
||||
.{ .text = "16", .expected = .bits16 },
|
||||
.{ .text = "32", .expected = .bits32 },
|
||||
.{ .text = "64", .expected = .bits64 },
|
||||
.{ .text = "128", .expected = .bits128 },
|
||||
}) |case| {
|
||||
try testing.expectEqual(case.expected, parseBitWidth(case.text).?);
|
||||
}
|
||||
// Anything else, including a width the engine does not have, is rejected.
|
||||
for ([_][]const u8{ "7", "0", "24", "256", "", "eight", "8x", "-8" }) |text| {
|
||||
try testing.expect(parseBitWidth(text) == null);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: --signed and --unsigned set the signedness and imply -p" {
|
||||
const signed = parseArgs(testing.allocator, &.{ "--signed", "0xFF" }).expression;
|
||||
defer testing.allocator.free(signed.text);
|
||||
try testing.expectEqual(engine.types.Signedness.signed, signed.config.signedness);
|
||||
try testing.expectEqual(engine.Mode.programmer, signed.mode);
|
||||
|
||||
const unsigned = parseArgs(testing.allocator, &.{ "--unsigned", "0xFF" }).expression;
|
||||
defer testing.allocator.free(unsigned.text);
|
||||
try testing.expectEqual(engine.types.Signedness.unsigned, unsigned.config.signedness);
|
||||
try testing.expectEqual(engine.Mode.programmer, unsigned.mode);
|
||||
}
|
||||
|
||||
test "parseArgs: --endian sets the byte order and accepts both spellings" {
|
||||
for ([_][]const []const u8{
|
||||
&.{ "--endian", "little", "0xDEADBEEF" },
|
||||
&.{ "--endian=le", "0xDEADBEEF" },
|
||||
&.{ "--endian", "LITTLE", "0xDEADBEEF" },
|
||||
}) |args| {
|
||||
const e = parseArgs(testing.allocator, args).expression;
|
||||
defer testing.allocator.free(e.text);
|
||||
try testing.expectEqual(engine.types.Endianness.little, e.config.display_endian);
|
||||
}
|
||||
try testing.expectEqual(engine.types.Endianness.big, parseEndian("big").?);
|
||||
try testing.expectEqual(engine.types.Endianness.big, parseEndian("BE").?);
|
||||
try testing.expect(parseEndian("middle") == null);
|
||||
try testing.expect(parseEndian("") == null);
|
||||
}
|
||||
|
||||
test "parseArgs: the flags combine, and order does not matter" {
|
||||
const e = parseArgs(testing.allocator, &.{ "--unsigned", "--bits", "16", "--endian", "little", "0xFF", "+", "1" }).expression;
|
||||
defer testing.allocator.free(e.text);
|
||||
try testing.expectEqual(engine.types.BitWidth.bits16, e.config.bit_width);
|
||||
try testing.expectEqual(engine.types.Signedness.unsigned, e.config.signedness);
|
||||
try testing.expectEqual(engine.types.Endianness.little, e.config.display_endian);
|
||||
try testing.expectEqualStrings("0xFF + 1", e.text);
|
||||
}
|
||||
|
||||
test "parseArgs: a bad or incomplete flag value is reported, not evaluated" {
|
||||
for ([_][]const []const u8{
|
||||
&.{ "--bits", "7", "1" },
|
||||
&.{ "--bits=7", "1" },
|
||||
&.{"--bits"},
|
||||
&.{"--bits="},
|
||||
&.{ "--endian", "sideways", "1" },
|
||||
&.{"--endian"},
|
||||
// A typo used to be joined into the expression and reported as an
|
||||
// unexpected token, which pointed at the wrong thing.
|
||||
&.{ "--bit", "8", "1" },
|
||||
&.{ "--programer", "1" },
|
||||
// `--bitsy` starts with `--bits` but is not it, so it is an unknown flag
|
||||
// rather than a width of "y".
|
||||
&.{ "--bitsy", "1" },
|
||||
}) |args| {
|
||||
try testing.expect(parseArgs(testing.allocator, args).output.is_error);
|
||||
}
|
||||
}
|
||||
|
||||
test "parseArgs: subcommands answer --help on stdout" {
|
||||
// Reachable without getting the arguments wrong, and not an error.
|
||||
for ([_][]const []const u8{
|
||||
&.{ "convert", "--help" },
|
||||
&.{ "convert", "-h" },
|
||||
&.{ "amort", "--help" },
|
||||
&.{ "amort", "-h" },
|
||||
&.{ "amortize", "-h" },
|
||||
}) |args| {
|
||||
const out = parseArgs(testing.allocator, args).output;
|
||||
try testing.expect(!out.is_error);
|
||||
try testing.expect(std.mem.startsWith(u8, out.text, "usage: tally "));
|
||||
}
|
||||
|
||||
// The same text still reports an error when the arguments are wrong.
|
||||
try testing.expect(parseArgs(testing.allocator, &.{"convert"}).output.is_error);
|
||||
}
|
||||
|
||||
test "the programmer config reaches the result" {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// 8-bit: 0xFF + 1 wraps to 0.
|
||||
const narrow = evaluateWith(alloc, "0xFF + 1", .programmer, .{ .bit_width = .bits8 }, &buf);
|
||||
try testing.expect(!narrow.is_error);
|
||||
try testing.expect(std.mem.indexOf(u8, narrow.output, "dec(unsigned): 0\n") != null);
|
||||
|
||||
// Signedness decides whether >> extends the sign.
|
||||
const signed = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
||||
.bit_width = .bits8,
|
||||
.signedness = .signed,
|
||||
}, &buf);
|
||||
try testing.expect(std.mem.indexOf(u8, signed.output, "dec(signed): -1") != null);
|
||||
|
||||
const unsigned = evaluateWith(alloc, "0xFF >> 1", .programmer, .{
|
||||
.bit_width = .bits8,
|
||||
.signedness = .unsigned,
|
||||
}, &buf);
|
||||
try testing.expect(std.mem.indexOf(u8, unsigned.output, "dec(unsigned): 127") != null);
|
||||
|
||||
// Byte order reverses the hex row and nothing else.
|
||||
const little = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
||||
.bit_width = .bits16,
|
||||
.display_endian = .little,
|
||||
}, &buf);
|
||||
try testing.expect(std.mem.indexOf(u8, little.output, "hex: AD DE") != null);
|
||||
const big = evaluateWith(alloc, "0xDEAD", .programmer, .{
|
||||
.bit_width = .bits16,
|
||||
.display_endian = .big,
|
||||
}, &buf);
|
||||
try testing.expect(std.mem.indexOf(u8, big.output, "hex: DE AD") != null);
|
||||
}
|
||||
|
||||
test "parseArgs: --help" {
|
||||
const parsed = parseArgs(testing.allocator, &.{"--help"});
|
||||
switch (parsed) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue