Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control style with a flag #62

Open
elliot-nelson opened this issue Apr 14, 2020 · 3 comments
Open

Control style with a flag #62

elliot-nelson opened this issue Apr 14, 2020 · 3 comments

Comments

@elliot-nelson
Copy link
Member

I was looking into how to control output styling with a flag.

It seems the best bet is probably to do two different passes of sywac - the first only checks for the flag (for example, a "compact" flag, that uses a condensed style instead of a nice colorful style), and the second does the complete, complicated API of your app.

However, since the parsing happens asynchronously, you actually need to set up the second one inside an async function... something like this:

const sywac = require('sywac');
const Api = require('sywac/api');
const fancyStyle = require('sywac-style-fancy'); // made up
const compactStyle = require('sywac-style-compact'); // made up

async function main() {
  const firstPass = await (new Api().boolean('compact').parse());
    
  sywac
    .strict()
    .boolean('compact', { hidden: true })
    .style(firstPass.argv.compact ? compactStyle : fancyStyle)
    .command(...)
    // ... another 100 lines of fancy options and commands here
    .parseAndExit();
}

main();

Is there a simpler method than this for using an incoming flag to control an aspect of the synchronous configuration of your API? (A similar example might be a program with a hundred flags, one of which is actually --strict itself - turning on strict mode to warn if some other flag is misspelled.)

@nexdrew
Copy link
Member

nexdrew commented Apr 18, 2020

There's a way to do this that doesn't require multiple passes of parsing. The trick is getting access to the appropriate Api instance and using the right hooks to apply the style/configuration at the right time.

Here are a couple examples to choose from. Both of them dynamically apply a help text style and strict mode based on a boolean option given on the command line.

Note that both examples presume the following custom styles (but you can obviously plug in whatever styles you want):

const chalk = require('chalk')
const styleVerbose = {
  hints: s => chalk.dim(s)
}
const styleCompact = {
  hints: s => ''
}

Example 1: Extend Api to force custom check

Pros:

  • Does not require custom types
  • Uses .check() handler for hook

Cons:

  • Tight coupling to sywac's Api#newChild(commandName, childOptions) internal implementation (only needed when using commands)
  • Abuses Api#shouldCoerceAndCheck(context)
  • The .check() handler is static (as shown)
  • Tight coupling between static .check() handler and global boolean options
// issue62a.js
// extend Api to force check handler to always run (and at every level)
const Api = require('sywac/api')
class AlwaysCheck extends Api {
  constructor (opts) {
    super(opts)
    // apply static check handler at every level
    this.check(argv => {
      this.style(argv.compact ? styleCompact : styleVerbose)
      this.strict(argv.strict)
    })
  }

  shouldCoerceAndCheck (context) {
    // force check handler to always run
    return true
  }

  // this only needed if using commands
  newChild (commandName, childOptions) {
    return new AlwaysCheck(Object.assign({
      factories: this._factories,
      utils: this.utils,
      pathLib: this.pathLib,
      fsLib: this.fsLib,
      name: this.name + ' ' + commandName,
      parentName: this.name,
      modulesSeen: this._modulesSeen.slice(),
      helpOpts: this._assignHelpOpts({}, this.helpOpts),
      showHelpByDefault: this._showHelpByDefault,
      strictMode: this._strictMode
    }, childOptions))
  }
}

new AlwaysCheck()
  .command('yo', argv => console.log('yo:', argv))
  .boolean('--compact', { desc: 'Output help text without hints' })
  .boolean('--strict', { desc: 'Error on unknown flags/args' })
  .help('--help')
  .outputSettings({ maxWidth: 55 })
  .showHelpByDefault()
  .parseAndExit()
$ node issue62a.js --help
Usage: issue62a <command> [options]

Commands:
  yo

Options:
  --compact  Output help text without hints   [boolean]
  --strict   Error on unknown flags/args      [boolean]
  --help     Show help       [commands: help] [boolean]
$ node issue62a.js --help --compact
Usage: issue62a <command> [options]

Commands:
  yo

Options:
  --compact  Output help text without hints
  --strict   Error on unknown flags/args
  --help     Show help
$ node issue62a.js yo -x
yo: { x: true, _: [], compact: false, strict: false, help: false }
$ node issue62a.js yo -x --strict
Usage: issue62a yo [options]

Options:
  --compact  Output help text without hints   [boolean]
  --strict   Error on unknown flags/args      [boolean]
  --help     Show help       [commands: help] [boolean]

Unknown options: -x
$ node issue62a.js yo -x --strict --compact
Usage: issue62a yo [options]

Options:
  --compact  Output help text without hints
  --strict   Error on unknown flags/args
  --help     Show help

Unknown options: -x

Example 2: Use custom types to override postParse logic

Pros:

  • Uses custom types for hooks
  • Does not require extending Api (no tight coupling to Api internals)

Cons:

  • Tight coupling between dynamic boolean type and custom command type
  • Relies on custom type overrides instead of simpler .check() handler
// issue62b.js
// define functions to apply config at different levels
function applyStyle (api, compact) {
  api.style(compact ? styleCompact : styleVerbose)
}

function applyStrict (api, strict) {
  api.strict(strict)
}

// define custom boolean type (for top-level api)
const TypeBoolean = require('sywac/types/boolean')
class DynamicConfig extends TypeBoolean {
  constructor (opts, api, apply) {
    super(opts)
    this.api = api
    this.apply = apply
  }

  postParse (context) {
    this.apply(this.api, this.getValue(context))
    return super.postParse(context)
  }
}

// define custom command type (for lower level apis)
// this only needed if using commands
const TypeCommand = require('sywac/types/command')
class CustomCommand extends TypeCommand {
  setValue (context, value) {
    super.setValue(context, value)
    if (value) {
      applyStyle(this.api, context.argv.compact)
      applyStrict(this.api, context.argv.strict)
    }
  }
}

// register factory for custom command type, apply custom boolean types
const api = require('sywac')
api
  .registerFactory('commandType', opts => new CustomCommand(opts))
  .command('yo', argv => console.log('yo:', argv))
  .custom(new DynamicConfig({
    flags: '--compact',
    desc: 'Output help text without hints'
  }, api, applyStyle))
  .custom(new DynamicConfig({
    flags: '--strict',
    desc: 'Error on unknown flags/args'
  }, api, applyStrict))
  .help('--help')
  .outputSettings({ maxWidth: 55 })
  .showHelpByDefault()
  .parseAndExit()
$ node issue62b.js --help
Usage: issue62b <command> [options]

Commands:
  yo

Options:
  --compact  Output help text without hints   [boolean]
  --strict   Error on unknown flags/args      [boolean]
  --help     Show help       [commands: help] [boolean]
$ node issue62b.js --help --compact
Usage: issue62b <command> [options]

Commands:
  yo

Options:
  --compact  Output help text without hints
  --strict   Error on unknown flags/args
  --help     Show help
$ node issue62b.js yo -x
yo: { x: true, _: [], compact: false, strict: false, help: false }
$ node issue62b.js yo -x --strict
Usage: issue62b yo [options]

Options:
  --compact  Output help text without hints   [boolean]
  --strict   Error on unknown flags/args      [boolean]
  --help     Show help       [commands: help] [boolean]

Unknown options: -x
$ node issue62b.js yo -x --strict --compact
Usage: issue62b yo [options]

Options:
  --compact  Output help text without hints
  --strict   Error on unknown flags/args
  --help     Show help

Unknown options: -x

Let me know if you have any questions.

@elliot-nelson
Copy link
Member Author

Thanks for the very detailed write-up!

I guess for me, it's hard to justify your two examples because I don't think people could write those without reading the source. (Unless they were copied verbatim from an "Advanced Examples" section in the docs.)

(My approach isn't as streamlined, but I think you could explain it to someone that had never looked at the Sywac source.)

I don't know any change is required but something that would be really impressive, I think, is if globally all "synchronous" config was allowed to take functions instead of statics/objects. An example of this in action is the https://github.com/terkelg/prompts API - all config properties can take a function which is evaluated lazily.

An example of how this might look:

sywac
  .boolean('compact')
  .boolean('strict')
  .strict(argv => argv.strict)
  .style(argv => argv.compact ? styleCompact : styleNormal)
  .parseAndExit()

(Lots of details being glossed over here - when exactly is stuff evaluated, what parameters would be passed, etc.)

An absurd example:

sywac
  .string('flag')
  .string(argv => argv.flag)
  .strict()
  .parseAndExit()

This of course is pretty cool but I don't see how it could work in practice.

@nexdrew
Copy link
Member

nexdrew commented Apr 18, 2020

My goal in sharing code examples is just to see/prove if a possible solution exists. I agree that, many times, the examples are advanced, and I don't really expect most CLI authors to figure them out on their own, but at least it shows that the framework is extensible enough to support most use-cases.

Otherwise, I'd like to improve sywac's api to make practical use-cases easy to implement.

I don't know any change is required but something that would be really impressive, I think, is if globally all "synchronous" config was allowed to take functions instead of statics/objects.

I'm definitely open to this idea and would be willing to explore it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants