Skip to content

Commit

Permalink
Release 2.x - initial changes (#104)
Browse files Browse the repository at this point in the history
* Refactor (#96)

* Move onError tp api/cli.d

* Improve ANSI styling handling and making AnsiStylingArgument boolean-like

* Make Parser private

* Remove hooks

* Remove subcommands

* Remove argumentparser

* Update readme

* Add unit tests

* Rename Config.helpStyle to Config.styling (#97)

* Add unit test (#98)

* Refactor (#99)

* Small cleanup

* Make Config a template parameter

* Add unit test

* Styling in error messages (#100)

* Add errorMessagePrefix to Style

* Rename Style.namedArgumentName => Style.argumentName

* Styling in error messages

* Add check for argument name (#101)

* Rename Config.namedArgChar to namedArgPrefix (#102)

* Add checks for positional arguments (#103)

* Refactor (#105)

* Add ArgumentInfo.memberSymbol

* Small refactoring

* Move Restriction and RestrictionGroup to internal.restriction

* Remove symbol parameter

* remove partial apply

* Small refactoring

* Small refactoring

* Add unit test

* Pin LDC to 1.34.0 (#108)

* Add '--verbose' to builds

* Refactoring (#127)

* Required positional arguments must come before optional ones

* Optional positional arguments are not allowed with default subcommand

* Rewrite parser (#128)

* Refactor

* Split ArgumentInfo.names to shortNames and longNames

* Add namedArguments and positionalArguments to Arguments

* Rename Arguments.arguments to info

* Refactor Arguments API

* Remove ArgumentInfo.ignoreInDefaultCommand

* Rewrite parser

* unit tests

* Update readme (#121)

* Update readme

* Update the examples as well

* Apply suggested changes to readme

* Declare `Style.Default` with an alternative syntax (#130)

* Turn `main` and `mainComplete` into regular templates (#132)

They don't need advanced features that template mixins provide. (Regular
templates are mixable, too.)
https://dlang.org/spec/template-mixin.html

* Make Styling API `nothrow pure @safe` (#133)

* Reduce allocations in Styling API (#134)

* Reduce allocations in Styling API

* Remove the overload of `TextStyle.opCall` that takes a sink

We should make `StyledText` a range instead.

* Do not depend on `std.regex` (#131)

* Do not depend on `std.regex`

This saves 1.5 MB in the binary, which is desirable since not every
program that uses `argparse` may want to use regexes - or that
particular implementation of regexes.

`argparse.ansi.getUnstyledText` became eager, but the library wasn't
exploiting its laziness anyway.

* Make `getUnstyledText` lazy and `@nogc`

* Use constants instead of hardcoded characters

* Rework the auxiliary function

Co-Authored-By: Andrey Zherikov <[email protected]>

* Remove one mutable variable

* Add a small comment

---------

Co-authored-by: Andrey Zherikov <[email protected]>
Co-authored-by: Andrey Zherikov <[email protected]>

* Improve compilation time and memory (#124)

* Use regular parameters instead of template parameters where possible

This helps in reducing both compilation time and (build-time) memory
requirement.

* Deduplicate `HelpArgumentUDA.parse`

2.3 GB -> 1.7 GB.

* Deduplicate `Complete.CompleteCmd`

1.7 GB -> 1.6 GB.

* Compile the completer on demand

1.6 GB -> 0.8 GB.

* Simplify `CLI!(...).complete`

* Deduplicate `CounterParsingFunction`

* Make some of the config's fields statically known by the parser

So that the compiler can prune dead branches of code.

* Remove `assignChar` from parser's template parameters

* Try to simplify min-max-handling logic in `getMemberArgumentUDA`

* Add a unit test for `getMemberArgumentUDA`

* Move `getArgumentUDA` to `argparse.internal.arguments`

Renamed into `finalize` in the process. It only fills `ArgumentInfo`
so it doesn't have to know about UDAs at all.

* Import non-std modules once per file

---------

Co-authored-by: Nickolay Bukreyev <[email protected]>
Co-authored-by: Andrey Zherikov <[email protected]>
  • Loading branch information
3 people authored Dec 11, 2023
1 parent d789306 commit 1685e39
Show file tree
Hide file tree
Showing 27 changed files with 2,509 additions and 1,588 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
dc: [dmd-latest, ldc-latest]
dc: [dmd-latest, ldc-1.34.0]

runs-on: ${{ matrix.os }}
steps:
Expand All @@ -27,25 +27,25 @@ jobs:
compiler: ${{ matrix.dc }}

- name: Run tests
run: dub test --build=unittest-cov
run: dub test --build=unittest-cov --verbose

- name: Run tests (dip1000)
env:
DFLAGS: -dip1000
run: dub test --build=unittest-cov
run: dub test --build=unittest-cov --verbose

- name: Run tests (in)
env:
DFLAGS: -preview=in
run: dub test --build=unittest-cov
run: dub test --build=unittest-cov --verbose

- name: Run tests (dip1000 & in)
env:
DFLAGS: -dip1000 -preview=in
run: dub test --build=unittest-cov
run: dub test --build=unittest-cov --verbose

- name: Build examples
run: dub --root examples build --parallel
run: dub --root examples build --parallel --verbose

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v2
425 changes: 210 additions & 215 deletions README.md

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions examples/getting_started/advanced/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct Advanced
@NamedArgument
Enum choice;

// Custom types can also be used with custon parsing function
// Custom types can also be used with custom parsing function
struct CustomType {
double d;
}
Expand All @@ -47,8 +47,8 @@ auto config()
{
Config cfg;

cfg.helpStyle.programName = blue.onYellow;
cfg.helpStyle.namedArgumentName = bold.italic.cyan.onRed;
cfg.styling.programName = blue.onYellow;
cfg.styling.argumentName = bold.italic.cyan.onRed;

return cfg;
}
Expand All @@ -68,9 +68,8 @@ mixin CLI!(config(), Advanced).main!((args, unparsed)
writeln("Unparsed args: ", unparsed);

// use actual styling mode to print output
auto style = Advanced.color == Config.StylingMode.on ? red.onWhite : noStyle;
writeln(style("Styling mode: "), Advanced.color);
assert(Advanced.color == Config.StylingMode.on || Advanced.color == Config.StylingMode.off);
auto style = Advanced.color ? red.onWhite : noStyle;
writeln(style("Styling mode: "), Advanced.color ? "on" : "off");

return 0;
});
18 changes: 9 additions & 9 deletions examples/getting_started/basic/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,39 @@ import argparse;
struct Basic
{
// Basic data types are supported:
// --name argument
// '--name' argument
string name;

// --number argument
// '--number' argument
int number;

// --boolean argument
// '--boolean' argument
bool boolean;

// Argument can have default value if it's not specified in command line
// --unused argument
// '--unused' argument
string unused = "some default value";


// Enums are also supported
enum Enum { unset, foo, boo }
// --choice argument
// '--choice' argument
Enum choice;

// Use array to store multiple values
// --array argument
// '--array' argument
int[] array;

// Callback with no args (flag)
// --callback argument
// '--callback' argument
void callback() {}

// Callback with single value
// --callback1 argument
// '--callback1' argument
void callback1(string value) { assert(value == "cb-value"); }

// Callback with zero or more values
// --callback2 argument
// '--callback2' argument
void callback2(string[] value) { assert(value == ["cb-v1","cb-v2"]); }
}

Expand Down
179 changes: 144 additions & 35 deletions source/argparse/ansi.d
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
module argparse.ansi;

import std.regex: ctRegex;

// The string that starts an ANSI command sequence.
private enum prefix = "\033[";

Expand All @@ -14,9 +12,6 @@ private enum suffix = "m";
// The sequence used to reset all styling.
private enum reset = prefix ~ suffix;

// Regex to match ANSI sequence
private enum sequenceRegex = ctRegex!(`\x1b\[(\d*(;\d*)*)?m`);

// Code offset between foreground and background
private enum colorBgOffset = 10;

Expand Down Expand Up @@ -52,38 +47,47 @@ private enum Color

package struct TextStyle
{
private ubyte[] style;
private string style = prefix;

private this(ubyte[] st)
{
style = st;
}
private this(ubyte st)
private this(const(ubyte)[] st...) scope inout nothrow pure @safe
{
if(st != 0)
style = [st];
import std.algorithm.iteration: joiner, map;
import std.array: appender;
import std.conv: toChars;
import std.utf: byCodeUnit;

auto a = appender(prefix);
a ~= st.map!(_ => uint(_).toChars).joiner(separator.byCodeUnit);
style = a[];
}

private auto opBinary(string op)(ubyte other) if(op == "~")
private ref opOpAssign(string op : "~")(ubyte other)
{
return other != 0 ? TextStyle(style ~ other) : this;
import std.array: appender;
import std.conv: toChars;

if(other != 0)
{
auto a = appender(style);
if(style.length != prefix.length)
a ~= separator;
a ~= uint(other).toChars;
style = a[];
}
return this;
}

public auto opCall(string str) const
{
import std.conv: text, to;
import std.algorithm: joiner, map;
import std.range: chain;

if(style.length == 0 || str.length == 0)
if(str.length == 0 || style.length == prefix.length)
return str;

return text(prefix, style.map!(to!string).joiner(separator), suffix, str, reset);
return style ~ suffix ~ str ~ reset;
}
}

unittest
nothrow pure @safe unittest
{
assert(TextStyle.init("foo") == "foo");
assert(TextStyle([])("foo") == "foo");
assert(TextStyle([Font.bold])("foo") == "\033[1mfoo\033[m");
assert(TextStyle([Font.bold, Font.italic])("foo") == "\033[1;3mfoo\033[m");
Expand All @@ -97,7 +101,7 @@ package struct StyledText

string text;

string toString() const
string toString() return scope const nothrow pure @safe
{
return style(text);
}
Expand All @@ -107,38 +111,49 @@ package struct StyledText
{
return toString() ~ rhs;
}

// lhs ~ this
string opBinaryRight(string op : "~")(string lhs) const
{
return lhs ~ toString();
}
}

unittest
nothrow pure @safe unittest
{
auto s = TextStyle([Font.bold]);
assert(StyledText(s, "foo").toString() == s("foo"));

const ubyte[1] data = [Font.bold];
scope c = const TextStyle(data);
assert((const StyledText(c, "foo")).toString() == c("foo"));

immutable foo = StyledText(s, "foo");
assert(foo ~ "bar" == s("foo") ~ "bar");
assert("bar" ~ foo == "bar" ~ s("foo"));
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package template StyleImpl(ubyte styleCode)
{
public auto StyleImpl()
immutable style = TextStyle(styleCode);

public TextStyle StyleImpl()
{
return TextStyle(styleCode);
return style;
}
public auto StyleImpl(TextStyle otherStyle)
{
return otherStyle ~ styleCode;
return otherStyle ~= styleCode;
}
public auto StyleImpl(string text)
{
return StyledText(TextStyle(styleCode), text);
return StyledText(style, text);
}
public auto StyleImpl(TextStyle otherStyle, string text)
{
return StyledText(otherStyle ~ styleCode, text);
return StyledText(otherStyle ~= styleCode, text);
}
}

Expand Down Expand Up @@ -183,7 +198,7 @@ alias onLightCyan = StyleImpl!(colorBgOffset + Color.lightCyan);
alias onWhite = StyleImpl!(colorBgOffset + Color.white);


unittest
nothrow pure @safe unittest
{
assert(bold == TextStyle([Font.bold]));
assert(bold.italic == TextStyle([Font.bold, Font.italic]));
Expand All @@ -195,13 +210,107 @@ unittest
assert(bold.italic.red.onWhite("foo").toString() == "\033[1;3;31;107mfoo\033[m");
}

nothrow pure @safe @nogc unittest
{
auto style = bold;
style = italic; // Should be able to reassign.
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

package auto getUnstyledText(string text)
/**
Split a string into two parts: `result[0]` (head) is the first textual chunk (i.e., with no command sequences) that
occurs in `text`; `result[1]` (tail) is everything that follows it, with one leading command sequence stripped.
*/
private inout(char)[][2] findNextTextChunk(return scope inout(char)[] text) nothrow pure @safe @nogc
{
import std.ascii: isDigit;
import std.string: indexOf;

static assert(separator.length == 1);
static assert(suffix.length == 1);

size_t idx = 0;

while(true)
{
immutable seqIdx = text.indexOf(prefix, idx);

if(seqIdx < 0)
return [text, null];

idx = seqIdx + prefix.length;
while(idx < text.length && (text[idx] == separator[0] || isDigit(text[idx])))
idx++;

if(idx < text.length && text[idx] == suffix[0])
{
idx++;
if(seqIdx > 0) // If the chunk is not empty
return [text[0 .. seqIdx], text[idx .. $]];

// Chunk is empty so we skip command sequence and continue
text = text[idx .. $];
idx = 0;
}
}
}

public auto getUnstyledText(C : char)(C[] text)
{
import std.regex: splitter;
struct Unstyler
{
private C[] head, tail;

@property bool empty() const { return head.length == 0; }

@property inout(C)[] front() inout { return head; }

void popFront()
{
auto a = findNextTextChunk(tail);
head = a[0];
tail = a[1];
}

@property auto save() inout { return this; }
}

auto a = findNextTextChunk(text);
return Unstyler(a[0], a[1]);
}

nothrow pure @safe @nogc unittest
{
import std.range.primitives: ElementType, isForwardRange;

alias R = typeof(getUnstyledText(""));
assert(isForwardRange!R);
assert(is(ElementType!R == string));
}

nothrow pure @safe @nogc unittest
{
bool eq(T)(T actual, const(char[])[] expected...) // This allows `expected` to be `@nogc` even without `-dip1000`
{
import std.algorithm.comparison: equal;

return equal(actual, expected);
}

assert(eq(getUnstyledText("")));
assert(eq(getUnstyledText("\x1b[m")));
assert(eq(getUnstyledText("a\x1b[m"), "a"));
assert(eq(getUnstyledText("a\x1b[0;1m\x1b[9mm\x1b[m\x1b["), "a", "m", "\x1b["));
assert(eq(getUnstyledText("a\x1b[0:abc\x1b[m"), "a\x1b[0:abc"));

char[2] m = "\x1b[";
const char[2] c = "\x1b[";
immutable char[2] i = "\x1b[";

return text.splitter(sequenceRegex);
assert(eq(getUnstyledText(m), "\x1b["));
assert(eq(getUnstyledText(c), "\x1b["));
assert(eq(getUnstyledText(i), "\x1b["));
}

package size_t getUnstyledTextLength(string text)
Expand All @@ -213,7 +322,7 @@ package size_t getUnstyledTextLength(string text)

package size_t getUnstyledTextLength(StyledText text)
{
return getUnstyledTextLength(text.toString());
return getUnstyledTextLength(text.text);
}

unittest
Expand Down
Loading

0 comments on commit 1685e39

Please sign in to comment.