vilicvane / clime

⌨ The command-line interface framework for TypeScript.
252 stars 10 forks source link

Add ability to provide own parsing logic #46

Open ajafff opened 6 years ago

ajafff commented 6 years ago

While trying to port my CLI, I noticed there is no way to declare options that may not consume the next argument if not necessary.

A popular example are TypeScript's compilerOptions:

tsc --project tsconfig.json
# parses as {strict: undefined, project: 'tsconfig.json'}

tsc --strict --project tsconfig.json
# parses as {strict: true, project: 'tsconfig.json'}

tsc --strict true --project tsconfig.json
# parses as {strict: true, project: 'tsconfig.json'}

tsc --strict false --project tsconfig.json
# parses as {strict: false, project: 'tsconfig.json'}

In my use case this is not limited to a switch option (boolean value). It's actually an optional boolean or number:

wotan foo.ts
# parses as {fix: false, files: ['foo.ts']}

wotan --fix foo.ts
# parses as {fix: true, files: ['foo.ts']}

wotan --fix foo.ts
# parses as {fix: true, files: ['foo.ts']}

wotan --fix false foo.ts
# parses as {fix: false, files: ['foo.ts']}

wotan --fix 5 foo.ts
# parses as {fix: 5, files: ['foo.ts']}

I can think of 2 possible solutions:

Add optional flag

class MyOptions extends Options {
  @option({optional: true, default: false, validator: /^(true|false|\d+)$/})
  public fix: boolean | number;
}

If optional is true and validating the argument using the validator fails, do not consume the argument and use the default value instead.

Add parse function

class MyOptions extends Options {
  @option({
    parse(consume: () => string | undefined, unconsume: (arg: string) => void) {
      const option = consume();
      if (option === undefined)
        return false; // no more arguments available
      if (option === 'true')
        return true;
      if (option === 'false')
        return false;
      const numeric = +option;
      if (!Number.isNaN(numeric))
        return numeric;
      // argument is not in the expected format, push it back so that it may be parsed as another option or parameter
      unconsume(option);
      return false; // use a default value
    },
    validator(value: boolean | number) {
      if (typeof value === 'number' && value < 0)
        throw new ExpectedError('--fix must be a positive number'); 
    }
  })
  public fix: boolean | number;
}

If a parse function is provided, this function is responsible for consuming the arguments, parsing and converting the value. By calling consume() more than once the parser may consume 0..n of the remaining arguments. validators are still executed after parsing. That means errors about invalid option values can be reported. That's not possible with the optional proposal.

vilicvane commented 6 years ago

Hi, thanks for this feature request. I am quite into the second solution (though I might prefer peek and consume), it looks like a more elegant way consuming options while giving developers more flexibility.

Will come out with some refactoring ideas later.