Skip to content

Commit

Permalink
Add 'RequiredTogether' and 'MutuallyExclusive' (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrey-zherikov authored Dec 16, 2021
1 parent 4fae406 commit b7ee509
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 15 deletions.
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,58 @@ assert(parseCLIKnownArgs!T(args).get == T("A"));
assert(args == ["-c", "C"]);
```

## Argument dependencies

### Mutually exclusive arguments

Mutually exclusive arguments (i.e. those that can't be used together) can be declared using `MutuallyExclusive()` UDA:

```d
struct T
{
@MutuallyExclusive()
{
string a;
string b;
}
}
// Either or no argument is allowed
assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0);
assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0);
assert(parseCLIArgs!T([], (T t) {}) == 0);
// Both arguments are not allowed
assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0);
```

**Note that parenthesis are required in this UDA to work correctly.**

### Mutually required arguments

Mutually required arguments (i.e. those that require other arguments) can be declared using `RequiredTogether()` UDA:

```d
struct T
{
@RequiredTogether()
{
string a;
string b;
}
}
// Both or no argument is allowed
assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0);
assert(parseCLIArgs!T([], (T t) {}) == 0);
// Only one argument is not allowed
assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0);
assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0);
```

**Note that parenthesis are required in this UDA to work correctly.**

## Help generation

### Command
Expand Down
190 changes: 175 additions & 15 deletions source/argparse.d
Original file line number Diff line number Diff line change
Expand Up @@ -432,8 +432,43 @@ unittest
assert(g.description == "description");
}


private struct RestrictionGroup
{
string location;

enum Type { together, exclusive }
Type type;

private size_t[] arguments;
}

auto RequiredTogether(string file=__FILE__, uint line = __LINE__)()
{
import std.conv: to;
return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.together);
}

auto MutuallyExclusive(string file=__FILE__, uint line = __LINE__)()
{
import std.conv: to;
return RestrictionGroup(file~":"~line.to!string, RestrictionGroup.Type.exclusive);
}

unittest
{
auto t = RequiredTogether();
assert(t.location.length > 0);
assert(t.type == RestrictionGroup.Type.together);

auto e = MutuallyExclusive();
assert(e.location.length > 0);
assert(e.type == RestrictionGroup.Type.exclusive);
}


private alias ParseFunction(RECEIVER) = bool delegate(in Config config, string argName, ref RECEIVER receiver, string[] rawValues);
private alias Restriction = bool delegate(in Config config, in bool[ulong] cliArgs);
private alias Restriction = bool delegate(in Config config, in bool[size_t] cliArgs);

// Have to do this magic because closures are not supported in CFTE
// DMD v2.098.0 prints "Error: closures are not yet supported in CTFE"
Expand Down Expand Up @@ -461,17 +496,70 @@ auto partiallyApply(alias fun,C...)(C context)

private struct Restrictions
{
static Restriction RequiredArg(string errorMessage)(size_t index)
static Restriction RequiredArg(ArgumentInfo info)(size_t index)
{
return partiallyApply!((size_t index, in Config config, in bool[ulong] cliArgs)
return partiallyApply!((size_t index, in Config config, in bool[size_t] cliArgs)
{
if(index in cliArgs)
return true;

config.onError(errorMessage);
config.onError("The following argument is required: ", info.names[0].getArgumentName(config));
return false;
})(index);
}

static bool RequiredTogether(in Config config,
in bool[size_t] cliArgs,
in size_t[] restrictionArgs,
in ArgumentInfo[] allArgs)
{
size_t foundIndex = size_t.max;
size_t missedIndex = size_t.max;

foreach(index; restrictionArgs)
{
if(index in cliArgs)
{
if(foundIndex == size_t.max)
foundIndex = index;
}
else if(missedIndex == size_t.max)
missedIndex = index;

if(foundIndex != size_t.max && missedIndex != size_t.max)
{
config.onError("Missed argument '", allArgs[missedIndex].names[0].getArgumentName(config),
"' - it is required by argument '", allArgs[foundIndex].names[0].getArgumentName(config),"'");
return false;
}
}

return true;
}

static bool MutuallyExclusive(in Config config,
in bool[size_t] cliArgs,
in size_t[] restrictionArgs,
in ArgumentInfo[] allArgs)
{
size_t foundIndex = size_t.max;

foreach(index; restrictionArgs)
if(index in cliArgs)
{
if(foundIndex == size_t.max)
foundIndex = index;
else
{
config.onError("Argument '", allArgs[foundIndex].names[0].getArgumentName(config),
"' is not allowed with argument '", allArgs[index].names[0].getArgumentName(config),"'");
return false;
}

}

return true;
}
}

private struct Arguments(RECEIVER)
Expand Down Expand Up @@ -503,6 +591,7 @@ private struct Arguments(RECEIVER)
size_t[string] groupsByName;

Restriction[] restrictions;
RestrictionGroup[] restrictionGroups;

@property ref Group requiredGroup() { return groups[requiredGroupIndex]; }
@property ref const(Group) requiredGroup() const { return groups[requiredGroupIndex]; }
Expand All @@ -526,28 +615,28 @@ private struct Arguments(RECEIVER)
groups = [ Group("Required arguments"), Group("Optional arguments") ];
}

private void addArgument(ArgumentInfo info, Group group)(ParseFunction!RECEIVER parse)
private void addArgument(ArgumentInfo info, RestrictionGroup[] restrictions, Group group)(ParseFunction!RECEIVER parse)
{
auto index = (group.name in groupsByName);
if(index !is null)
addArgument!info(parse, groups[*index]);
addArgument!(info, restrictions)(parse, groups[*index]);
else
{
groupsByName[group.name] = groups.length;
groups ~= group;
addArgument!info(parse, groups[$-1]);
addArgument!(info, restrictions)(parse, groups[$-1]);
}
}

private void addArgument(ArgumentInfo info)(ParseFunction!RECEIVER parse)
private void addArgument(ArgumentInfo info, RestrictionGroup[] restrictions = [])(ParseFunction!RECEIVER parse)
{
static if(info.required)
addArgument!info(parse, requiredGroup);
addArgument!(info, restrictions)(parse, requiredGroup);
else
addArgument!info(parse, optionalGroup);
addArgument!(info, restrictions)(parse, optionalGroup);
}

private void addArgument(ArgumentInfo info)(ParseFunction!RECEIVER parse, ref Group group)
private void addArgument(ArgumentInfo info, RestrictionGroup[] argRestrictions = [])(ParseFunction!RECEIVER parse, ref Group group)
{
static assert(info.names.length > 0);

Expand All @@ -572,16 +661,46 @@ private struct Arguments(RECEIVER)
group.arguments ~= index;

static if(info.required)
restrictions ~= Restrictions.RequiredArg!(info.names[0])(index);
restrictions ~= Restrictions.RequiredArg!info(index);

static foreach(restriction; argRestrictions)
addRestriction!(info, restriction)(index);
}

private void addRestriction(ArgumentInfo info, RestrictionGroup restriction)(size_t argIndex)
{
auto groupIndex = (restriction.location in groupsByName);
auto index = groupIndex !is null
? *groupIndex
: {
auto index = groupsByName[restriction.location] = restrictionGroups.length;
restrictionGroups ~= restriction;
return index;
}();

private bool checkRestrictions(in bool[ulong] cliArgs, in Config config) const
restrictionGroups[index].arguments ~= argIndex;
}


private bool checkRestrictions(in bool[size_t] cliArgs, in Config config) const
{
foreach(restriction; restrictions)
if(!restriction(config, cliArgs))
return false;

foreach(restriction; restrictionGroups)
final switch(restriction.type)
{
case RestrictionGroup.Type.together:
if(!Restrictions.RequiredTogether(config, cliArgs, restriction.arguments, arguments))
return false;
break;
case RestrictionGroup.Type.exclusive:
if(!Restrictions.MutuallyExclusive(config, cliArgs, restriction.arguments, arguments))
return false;
break;
}

return true;
}

Expand Down Expand Up @@ -663,10 +782,17 @@ private void addArgument(alias symbol, RECEIVER)(ref Arguments!RECEIVER args)

enum info = uda.info.setDefaults!(typeof(member), symbol);

enum restrictions = {
RestrictionGroup[] restrictions;
static foreach(gr; getUDAs!(member, RestrictionGroup))
restrictions ~= gr;
return restrictions;
}();

static if(getUDAs!(member, Group).length > 0)
args.addArgument!(info, getUDAs!(member, Group)[0])(ParsingFunction!(symbol, uda, info, RECEIVER));
args.addArgument!(info, restrictions, getUDAs!(member, Group)[0])(ParsingFunction!(symbol, uda, info, RECEIVER));
else
args.addArgument!info(ParsingFunction!(symbol, uda, info, RECEIVER));
args.addArgument!(info, restrictions)(ParsingFunction!(symbol, uda, info, RECEIVER));
}

private auto createArguments(RECEIVER)(bool caseSensitive)
Expand Down Expand Up @@ -3371,4 +3497,38 @@ unittest
}

assert(parseCLIArgs!T([], (T t) { assert(false); }) != 0);
}

unittest
{
@Command("MYPROG")
struct T
{
@MutuallyExclusive()
{
string a;
string b;
}
}
assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) { assert(false); }) != 0);
assert(parseCLIArgs!T(["-a","a"], (T t) {}) == 0);
assert(parseCLIArgs!T(["-b","b"], (T t) {}) == 0);
assert(parseCLIArgs!T([], (T t) {}) == 0);
}

unittest
{
@Command("MYPROG")
struct T
{
@RequiredTogether()
{
string a;
string b;
}
}
assert(parseCLIArgs!T(["-a","a","-b","b"], (T t) {}) == 0);
assert(parseCLIArgs!T(["-a","a"], (T t) { assert(false); }) != 0);
assert(parseCLIArgs!T(["-b","b"], (T t) { assert(false); }) != 0);
assert(parseCLIArgs!T([], (T t) {}) == 0);
}

0 comments on commit b7ee509

Please sign in to comment.