Skip to content

Commit

Permalink
Allow alias args for non option like arguments but validate them to b…
Browse files Browse the repository at this point in the history
…e a proper match
  • Loading branch information
Arnesfield committed Mar 13, 2024
1 parent 2a5df16 commit bc9c8eb
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 83 deletions.
7 changes: 4 additions & 3 deletions src/node/alias.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Options } from '../types/core.types';
import { isAlias, isOption } from '../utils/arg.utils';
import { isAlias } from '../utils/arg.utils';
import { splitAlias } from './split-alias';

export class Alias {
Expand All @@ -17,8 +17,9 @@ export class Alias {
for (let alias in this.aliasMap) {
alias = alias.trim();
const args = alias && isAlias(alias) ? this.getAliasArgs(alias) : [];
// skip command aliases since we don't need to split them
if (args.length > 0 && isOption(args[0])) {
// // skip command aliases since we don't need to split them
// && isOption(args[0])
if (args.length > 0) {
// remove prefix only when saving
this.aliases.push(alias.slice(1));
}
Expand Down
17 changes: 12 additions & 5 deletions src/node/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,19 @@ export class Node {
this.children.push(node);
}

parse(arg: string): Options | null {
parse(arg: string, strict = false): Options | null {
// make sure parse result is a valid object
const options = this._parse(arg);
return typeof options === 'object' && !Array.isArray(options)
? options
: null;
const value =
typeof options === 'object' && !Array.isArray(options) ? options : null;
if (strict && !value) {
throw new ArgsTreeError({
cause: ArgsTreeError.UNRECOGNIZED_ARGUMENT_ERROR,
options: this.options,
message: `Unrecognized option or command: ${arg}`
});
}
return value;
}

range(diff = 0): NodeRange {
Expand Down Expand Up @@ -128,7 +135,7 @@ export class Node {
throw new ArgsTreeError({
cause: ArgsTreeError.UNRECOGNIZED_ALIAS_ERROR,
options: this.options,
message: `Unrecognized ${label}: ${list}.`
message: `Unrecognized ${label}: ${list}`
});
}
return this;
Expand Down
19 changes: 12 additions & 7 deletions src/parser/parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Node } from '../node/node';
import { Parsed } from './parser.types';
import { Parsed, ParsedType } from './parser.types';
import { preparse } from './preparse';

export class Parser {
Expand All @@ -12,10 +12,12 @@ export class Parser {

private save(item: Parsed): Parsed[] {
// option or command
// `raw` means `is arg a value`
const { arg, raw } = item;
const { arg, type } = item;
const parsed: Parsed[] = [];
const options = raw ? null : this.parent.parse(arg);
const isValue = type === ParsedType.Value;
const options = isValue
? null
: this.parent.parse(arg, type === ParsedType.Match);
// is an option
if (options != null) {
// validate existing child then make new child
Expand All @@ -33,7 +35,7 @@ export class Parser {

// not an option
// if this arg is an alias, expand into multiple args
const split = raw ? null : this.parent.alias.split(arg);
const split = isValue ? null : this.parent.alias.split(arg);
if (split && split.total > 0) {
// treat left over from split as argument if it's not an alias like option
if (split.arg != null) {
Expand All @@ -43,12 +45,15 @@ export class Parser {
// treat first as is (alias) while the rest as values
for (const aliasArgs of split.args) {
aliasArgs.forEach((arg, index) => {
parsed.push({ arg, raw: index > 0 });
parsed.push({
arg,
type: index > 0 ? ParsedType.Value : ParsedType.Match
});
});
}
}
// for value, always save to child if it exists (most likely it exists)
else if (raw && this.child) {
else if (isValue && this.child) {
this.child.push(arg);
// validate only if it does not satisfy max
if (!this.child.range().satisfies.max) {
Expand Down
10 changes: 7 additions & 3 deletions src/parser/parser.types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
export enum ParsedType {
Value,
Match
}

export interface Parsed {
/**
* The argument string.
*/
arg: string;
/**
* Determines whether the {@linkcode arg} is treated
* as a value (`true`) or an option/command/non-strict value (`false`).
* Determines whether the {@linkcode arg} is treated as a value or an option/command.
*
* If {@linkcode arg} is treated as a value,
* it is saved as an argument for the child node.
*/
raw?: boolean;
type?: ParsedType;
}
7 changes: 5 additions & 2 deletions src/parser/preparse.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isAlias, isOption } from '../utils/arg.utils';
import { Parsed } from './parser.types';
import { Parsed, ParsedType } from './parser.types';

export function preparse(arg: string): Parsed[] {
const parsed: Parsed[] = [];
Expand All @@ -13,7 +13,10 @@ export function preparse(arg: string): Parsed[] {
const value = arg.slice(equalIndex + 1);
// for some reason, enclosing quotes are not included
// with the value part, so there's no need to handle them
parsed.push({ arg: alias }, { arg: value, raw: true });
parsed.push(
{ type: ParsedType.Match, arg: alias },
{ type: ParsedType.Value, arg: value }
);
} else {
// maybe value or command, ignore equal sign
// treat as an arg without splitting `=` if ever it exists
Expand Down
162 changes: 99 additions & 63 deletions test/argstree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,79 +63,115 @@ describe('argstree', () => {
expectError({
args: ['-t'],
cause: ArgsTreeError.UNRECOGNIZED_ARGUMENT_ERROR,
options: {
alias: { '-t': ['not', '--test'] },
args: { '--test': {} }
}
options: { alias: { '-t': ['foo', '--test'] }, args: { '--test': {} } }
});
});

// errors
it('should treat the rest of the values of an alias argument as values', () => {
let tree = argstree(['-t'], {
alias: { '-t': ['--test', 'foo', 'bar'] },
args: { '--test': {}, foo: {}, bar: {} }
});
expect(tree.children).be.have.length(1);
expect(tree.descendants).be.have.length(1);
expect(tree.children[0]).to.have.property('id').that.equals('--test');
expect(tree.children[0])
.be.have.property('args')
.that.is.an('array')
.that.deep.equals(['foo', 'bar']);

describe('error', () => {
it('should throw an error for invalid options', () => {
expectError({
cause: ArgsTreeError.INVALID_OPTIONS_ERROR,
options: { min: 1, max: 0 }
});
tree = argstree(['-t', 'f', 'b'], {
alias: {
'-t': ['--test', 'foo', 'bar'],
f: ['foo', '--test', 'foo', 'bar'],
b: 'bar'
},
args: { '--test': {}, foo: {}, bar: {} }
});
expect(tree.children).be.have.length(3);
expect(tree.descendants).be.have.length(3);

const options = { args: { test: { min: 2, max: 1 } } } satisfies Options;
expectError({
args: ['test'],
cause: ArgsTreeError.INVALID_OPTIONS_ERROR,
options,
equal: options.args.test
});
expect(tree.children[0]).to.have.property('id').that.equals('--test');
expect(tree.children[0])
.be.have.property('args')
.that.is.an('array')
.that.deep.equals(['foo', 'bar']);

expect(tree.children[1]).to.have.property('id').that.equals('foo');
expect(tree.children[1])
.be.have.property('args')
.that.is.an('array')
.that.deep.equals(['--test', 'foo', 'bar']);

expect(tree.children[2]).to.have.property('id').that.equals('bar');
expect(tree.children[2])
.be.have.property('args')
.that.is.an('array')
.with.length(0);
});

it('should throw an error for invalid options', () => {
expectError({
cause: ArgsTreeError.INVALID_OPTIONS_ERROR,
options: { min: 1, max: 0 }
});

it('should throw an error for invalid range', () => {
expect(() => argstree([], { max: 1 })).to.not.throw(ArgsTreeError);
expect(() => argstree([], { args: { test: { max: 1 } } })).to.not.throw(
ArgsTreeError
);
expectError({
cause: ArgsTreeError.INVALID_RANGE_ERROR,
options: { min: 1 }
});
const options = { args: { test: { min: 2, max: 1 } } } satisfies Options;
expectError({
args: ['test'],
cause: ArgsTreeError.INVALID_OPTIONS_ERROR,
options,
equal: options.args.test
});
});

const options = { args: { test: { min: 1, max: 2 } } } satisfies Options;
expectError({
args: ['test'],
cause: ArgsTreeError.INVALID_RANGE_ERROR,
options,
equal: options.args.test
});
it('should throw an error for invalid range', () => {
expect(() => argstree([], { max: 1 })).to.not.throw(ArgsTreeError);
expect(() => argstree([], { args: { test: { max: 1 } } })).to.not.throw(
ArgsTreeError
);
expectError({
cause: ArgsTreeError.INVALID_RANGE_ERROR,
options: { min: 1 }
});

it('should throw an error for unknown alias', () => {
const options = {
alias: { '-t': '--test' },
args: {
'--test': {},
test: {
alias: { '-T': '--subtest', '-y': '--y' },
args: { '--subtest': {}, '--y': {} }
}
}
} satisfies Options;

const errOpts = {
cause: ArgsTreeError.UNRECOGNIZED_ALIAS_ERROR,
options,
equal: options as Options
};
expect(() => argstree(['-t'], options)).to.not.throw(ArgsTreeError);
expectError({ ...errOpts, args: ['-tx'] });
expectError({ ...errOpts, args: ['-xt'] });
expectError({ ...errOpts, args: ['-xtx'] });

errOpts.equal = options.args.test;
expect(() =>
argstree(['test', '-T', '-Ty', '-yT'], options)
).to.not.throw(ArgsTreeError);
expectError({ ...errOpts, args: ['test', '-Tx'] });
expectError({ ...errOpts, args: ['test', '-xT'] });
expectError({ ...errOpts, args: ['test', '-xTx'] });
const options = { args: { test: { min: 1, max: 2 } } } satisfies Options;
expectError({
args: ['test'],
cause: ArgsTreeError.INVALID_RANGE_ERROR,
options,
equal: options.args.test
});
});

it('should throw an error for unknown alias', () => {
const options = {
alias: { '-t': '--test' },
args: {
'--test': {},
test: {
alias: { '-T': '--subtest', '-y': '--y' },
args: { '--subtest': {}, '--y': {} }
}
}
} satisfies Options;

const errOpts = {
cause: ArgsTreeError.UNRECOGNIZED_ALIAS_ERROR,
options,
equal: options as Options
};
expect(() => argstree(['-t'], options)).to.not.throw(ArgsTreeError);
expectError({ ...errOpts, args: ['-tx'] });
expectError({ ...errOpts, args: ['-xt'] });
expectError({ ...errOpts, args: ['-xtx'] });

errOpts.equal = options.args.test;
expect(() => argstree(['test', '-T', '-Ty', '-yT'], options)).to.not.throw(
ArgsTreeError
);
expectError({ ...errOpts, args: ['test', '-Tx'] });
expectError({ ...errOpts, args: ['test', '-xT'] });
expectError({ ...errOpts, args: ['test', '-xTx'] });
});
});

0 comments on commit bc9c8eb

Please sign in to comment.