npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

argstree

v1.5.5

Published

Parse arguments into a tree structure.

Downloads

32

Readme

argstree

npm Node.js CI

Parse arguments into a tree structure.

Features

argstree is meant to be a minimal and less opinionated argument parser with the following goals and features:

  • Preserve the structure of the provided arguments.
  • No data types other than strings.
  • Variadic arguments by default unless limits are specified.
  • All arguments are treated as normal parameters unless configured to be an option or command.
  • Aliases are not restricted to single characters and can be expanded to multiple options or commands with additional arguments.
  • Recognize and split combined aliases (e.g. from -abaa to -a, -ba, -a).
  • Recognize assignment (=) for options and commands (e.g. --flag=value, command=value).
  • No errors for unrecognized options or commands by default (except for misconfigured aliases and unrecognized aliases from a combined alias).
  • Double-dash (--) is not treated as anything special but can be configured to be a non-strict subcommand.

Note that argstree only parses and transforms arguments into a tree structure. It is still up to the consumers/developers to read, interpret, and process the parsed arguments and decide how to use them to suit their application's needs.

If you're looking for something oddly specific and want more control when working with arguments, then argstree might be for you. Otherwise, you can check out the more popular tools like commander, yargs, minimist, and many more.

Install

npm install argstree

Usage

Import the module (ESM only):

import argstree from 'argstree';

The argstree(args, options) function accepts an array of strings and an options object. It returns a Node object (root node).

const node = argstree(['--hello', 'world'], { min: 1 });
console.log(node.id, node.args);
null [ '--hello', 'world' ]

[!TIP]

See src/core/core.types.ts for Node, NodeData, and Options types.

The spec(options) function can also be used to parse arguments while also being an options builder. See the Spec API section for more details.

[!IMPORTANT]

While the sections ahead use the core argstree() function to show usage examples, it is recommended to use the Spec API instead for the ease of building your parsing spec as it grows with more options and commands.

Also, for the sake of brevity, not all features/properties are included in this document and it is advised to view the referenced types to learn more.

Options and Commands

Configure options and commands by setting the args object or function option.

While they may be configured similarly, options start with a hyphen (e.g. -a, --add) while commands do not (e.g. run, start).

When setting the args object, the properties are used to match the arguments while the values are also options objects.

Options and commands will capture arguments and stop when another option or command is matched or the specified limit is reached.

const node = argstree(['--add', 'foo', '--add', 'run', 'bar'], {
  args: { '--add': {}, run: {} }
});
for (const child of node.children) {
  console.log(child.id, child.args);
}
--add [ 'foo' ]
--add []
run [ 'bar' ]

You can also pass a function to the args option. It has two parameters: the arg string and the NodeData object. It should return an options object, null, or undefined.

const node = argstree(['-a=bar', 'baz'], {
  alias: { '-a': '--add' },
  args(arg, data) {
    console.log(arg);
    // options object with initial arguments
    return arg === '--add' ? { initial: ['foo'] } : null;
  }
});
const child = node.children[0];
console.log(child.id, child.args);
-a=bar
-a
--add
baz
--add [ 'foo', 'bar', 'baz' ]

[!WARNING]

Be aware that __proto__ and other inherited object properties can be used as arguments. argstree does not block these possibly unsafe arguments, but it has some checks in place for the args object and the incoming options object. Make sure to check for __proto__ and other related properties.

const options = {
  __proto__: null, // <-- set null prototype
  '--add': {}
};
const node = argstree(['__proto__', 'constructor'], {
  args: arg => options[arg] // should be safe
});
console.log(node.args); // [ '__proto__', 'constructor' ]

Strict Mode

Set the strict boolean option to throw an error for unrecognized options that are not provided in the args option.

By default, this is set to false for the root node. If not explicitly provided, this option is inherited from the parent option or command.

try {
  argstree(['run', 'build', '--if-preset'], {
    strict: true,
    args: {
      run: { args: { '--if-present': {} } }
    }
  });
} catch (error) {
  console.error(error.toString());
}
ArgsTreeError: Command 'run' does not recognize the option: --if-preset

Assignment

Using =, options or commands will treat the assigned value as their own argument regardless of any matches.

By default, this behavior is enabled for options but not for commands. Set the assign boolean option to change this behavior.

const node = argstree(['--add=foo', 'copy=foo', '--save=bar', 'move=bar'], {
  args: {
    // assign enabled for options by default
    '--add': {},
    // change behavior
    '--save': { assign: false },
    // assign disabled for commands by default
    copy: {},
    // change behavior
    move: { assign: true }
  }
});
for (const child of node.children) {
  console.log(child.id, child.args);
}
--add [ 'foo', 'copy=foo', '--save=bar' ]
move [ 'bar' ]

Suboptions and Subcommands

Matching a suboption or subcommand means that its parent option or command will stop receiving arguments.

const node = argstree(['--add', 'run', '--add', 'build'], {
  args: {
    '--add': {},
    run: {
      // setting the `args` object (even empty)
      // will treat `run` as a subcommand
      args: {
        // can also set `args` object for this subcommand
      }
    }
  }
});
const add = node.children[0];
const run = node.children[1];
console.log('root', node.args);
console.log(add.id, add.args);
console.log(run.id, run.args, run.parent === node);
root []
--add []
run [ '--add', 'build' ] true

In this example:

  • The parent command of the run subcommand is the root node.
  • Once the run subcommand was matched, its options are used for parsing the proceeding arguments.
  • The second --add argument was not recognized as an option by run (not in its args option) and is treated as a normal argument.

Limits

Specify the required minimum and maximum number of arguments per option or command.

  • min (type: number | null) - Required number of arguments to read before the next parsed option or command.

    An error is thrown if the option or command does not satisfy this condition.

  • max (type: number | null) - Maximum number of arguments to read before the next parsed option or command.

    Arguments over the maximum limit are saved as arguments for the parent option or command instead.

    Direct assignment will always read the assigned value as an argument for the option or command.

    An error is thrown if the option or command does not satisfy this condition.

  • maxRead (type: number | null) - Similar to the max option but does not throw an error.

    If not provided, the value for max is used instead.

    This takes priority over the max option when reading arguments, but the max option is still used for validating the maximum number of arguments.

const args =
  '--min foo1 bar1 ' +
  '--max foo2 bar2 ' +
  '--max-read foo3 bar3 ' +
  '--max-read=foo4 bar4';
const node = argstree(args.split(' '), {
  args: {
    '--min': { min: 1 },
    '--max': { max: 1 },
    '--max-read': { maxRead: 0 }
  }
});
console.log('root', node.args);
for (const child of node.children) {
  console.log(child.id, child.args);
}
root [ 'bar2', 'foo3', 'bar3', 'bar4' ]
--min [ 'foo1', 'bar1' ]
--max [ 'foo2' ]
--max-read []
--max-read [ 'foo4' ]

Aliases

Configure aliases by setting the alias object option.

It should include the list of aliases mapped to options or commands specified in the args object option. An error is thrown if the option or command is not valid.

Only aliases that start with one hyphen (e.g. -a, -bc) can be combined together into one alias shorthand (e.g. -abc).

Note that the assign option applies to aliases of the options or commands and that limits also apply to their arguments.

const node = argstree(['-abacada=0', 'a=1', 'b=2'], {
  alias: {
    // alias -> option/command
    '-a': '--add',
    // alias -> option/command + arguments
    '-ba': ['--add', 'bar', 'baz'],
    // alias -> multiple options/commands
    '-ca': [['--add'], ['--save']],
    // alias -> multiple options/commands + arguments
    '-da': [
      ['--add', 'multi', 'bar', 'baz'],
      ['--save', 'multi', 'foo', 'baz']
    ],
    // non-combinable alias (example only, above usage can also apply)
    a: '--create', // assign enabled by default (option)
    b: 'start' // assign disabled by default (command)
  },
  args: { '--add': {}, '--save': {}, '--create': {}, start: {} }
});
for (const child of node.children) {
  // node.alias is set to the alias string used to parse the node
  console.log(child.id, child.alias, child.args);
}
--add -a []
--add -ba [ 'bar', 'baz' ]
--add -ca []
--save -ca []
--add -da [ 'multi', 'bar', 'baz' ]
--save -da [ 'multi', 'foo', 'baz', '0' ]
--create a [ '1', 'b=2' ]

Validation

Set the validate function option to validate the arguments after they are saved for the option or command. It has a NodeData object parameter and it should either return a boolean or throw an error manually. A validate error is thrown when false is returned.

Note that a call to validate means that the Node has already parsed all of its arguments and that it has passed all validation checks beforehand. It is also safe to directly mutate the data.args array in validate, but you can always choose to transform and validate arguments after parsing.

const node = argstree(['--list', 'a,b,c'], {
  args: {
    '--list': {
      min: 1,
      max: 1,
      validate(data) {
        // safely assume data.args has 1 length (min-max range: 1-1)
        const args = data.args[0].split(',');
        // you can validate args here, then mutate and replace data.args
        data.args.splice(0, data.args.length, ...args);
        return true;
      }
    }
  }
});
const list = node.children[0];
console.log(list.id, list.args);
--list [ 'a', 'b', 'c' ]

ArgsTreeError

For errors related to parsing and misconfiguration, an ArgsTreeError is thrown. You can import this class to reference in catch blocks.

import argstree, { ArgsTreeError } from 'argstree';

try {
  argstree(['--add', 'foo'], { args: { '--add': { min: 2 } } });
} catch (error) {
  if (error instanceof ArgsTreeError) {
    console.error(error.toJSON());
  }
}
{
  name: 'ArgsTreeError',
  cause: 'invalid-range',
  message: "Option '--add' expected at least 2 arguments, but got 1.",
  raw: '--add',
  alias: null,
  args: [ 'foo' ],
  options: { min: 2 }
}

[!TIP]

See src/core/error.ts for more details.

split

The split function is used to split the combined value string based on the provided matches. It returns an object with the split values and the remaining values that were not split. Note that longer match strings take priority and are split first.

import { split } from 'argstree';

console.log(split('foobar', ['foo', 'bar']));
console.log(split('foobarfoobaz', ['fo', 'foo', 'oba']));
console.log(split('foobarfoobaz', ['fo', 'oba', 'foo']));
{ values: [ 'foo', 'bar' ], remainder: [] }
{ values: [ 'foo', 'foo' ], remainder: [ 'bar', 'baz' ] }
{ values: [ 'fo', 'oba', 'fo', 'oba' ], remainder: [ 'r', 'z' ] }

stringify

The stringify function returns a stringified Node object. It also accepts an options object where you can specify what to show or hide from the generated tree string.

import argstree, { stringify } from 'argstree';

const node = argstree(['start', 'foo', '-a', 'bar', '--save', 'baz'], {
  args: {
    start: {
      alias: { '-a': '--add', '-s': '--save' },
      args: { '--add': {}, '--save': {} }
    }
  }
});
const tree = stringify(node, {
  args: true, // default: true
  ancestors: true, // default: false
  descendants: true // default: false
});
console.log(tree);
null (depth: 0)
├─┬ start (depth: 1)
│ ├─┬ :args (total: 1)
│ │ └── foo
│ ├─┬ --add (depth: 2, alias: -a)
│ │ ├─┬ :args (total: 1)
│ │ │ └── bar
│ │ └─┬ :ancestors (total: 2)
│ │   ├── null (depth: 0)
│ │   └── start (depth: 1)
│ ├─┬ --save (depth: 2)
│ │ ├─┬ :args (total: 1)
│ │ │ └── baz
│ │ └─┬ :ancestors (total: 2)
│ │   ├── null (depth: 0)
│ │   └── start (depth: 1)
│ ├─┬ :ancestors (total: 1)
│ │ └── null (depth: 0)
│ └─┬ :descendants (total: 2)
│   ├── --add (depth: 2, alias: -a)
│   └── --save (depth: 2)
└─┬ :descendants (total: 3)
  ├── start (depth: 1)
  ├── --add (depth: 2, alias: -a)
  └── --save (depth: 2)

[!TIP]

See src/core/stringify.ts for more details.

Spec API

The spec(options) function returns a Spec object (options builder). Calling parse(args) uses the argstree core function to parse the arguments and it also returns a Node object (root node). To get the options object, use the options() method.

import { spec } from 'argstree';

// create command spec
const cmd = spec({ min: 1 });

// add options or commands with aliases and with arguments
cmd.option('--add').alias('-a').alias('--no-add', '0');
cmd.option('--save').alias('-s', ['bar']);

// get the options object with <spec>.options()
const options = cmd.options();

// parse arguments with <spec>.parse(args)
const node = cmd.parse(['foo', '--add', 'bar', '-s', 'baz']);
console.log('root', node.args);
for (const child of node.children) {
  console.log(child.id, child.args);
}
root [ 'foo' ]
--add [ 'bar' ]
--save [ 'bar', 'baz' ]

[!TIP]

See src/core/spec.types.ts for more details.

Spec Options and Commands

  • Use the option(arg, options) and command(arg, options) methods to add options and commands to the args object option respectively.
  • Use the args() method to add an empty object to the args option.
  • Use the args(handler) method to use a function for the args option and use the handler callback as a fallback.
const cmd = spec();

// add option
cmd.option('--add');

// add command
cmd.command('start');

// <spec>.command() is an alias for the following:
// cmd.option('start').spec(startCmd => startCmd.args());

// add an empty args object
cmd.args();

// or add args function (string and NodeData)
cmd.args((arg, data) => {
  // callback not fired for added options/commands: '--add' and 'start'
  return arg === '--save' ? {} : null;
});

Spec Aliases

  • Use the aliases method to assign aliases not bound to the current option or command.
  • Use the alias method to assign aliases to the current option or command. An error is thrown if the current option or command does not exist.
const cmd = spec();

// set aliases similar to argstree options alias
cmd.aliases({ '-A': [['--add'], ['--save']] });

// set aliases to current option or command
// spec object method chaining (applies to all methods)
cmd.option('--add', { maxRead: 0 }).alias('-a').alias('--no-add', '0');

// multiple aliases and arguments
cmd.option('--save', { maxRead: 0 }).alias(['-s', '-sa'], ['1', '2', '3']);

Spec Suboptions and Subcommands

Use the spec method that accepts a setup callback which contains a Spec object parameter (subspec) to modify the options of the current option or command. This can be used multiple times for the same option or command.

function commonSpec(spec) {
  spec.option('--help', { maxRead: 0 }).alias('-h');
  spec.command('--', { strict: false });
}
const cmd = spec({ strict: true })
  .command('run-script')
  .alias(['run', 'urn'])
  .spec(run => run.option('--workspace', { min: 1, max: 1 }).alias('-w'))
  .spec(commonSpec);
commonSpec(cmd);
console.log('%o', cmd.options());
{
  strict: true,
  args: [Object: null prototype] {
    'run-script': {
      args: [Object: null prototype] {
        '--workspace': { min: 1, max: 1 },
        '--help': { maxRead: 0 },
        '--': { strict: false, args: [Object: null prototype] {} }
      },
      alias: [Object: null prototype] { '-w': '--workspace', '-h': '--help' }
    },
    '--help': { maxRead: 0 },
    '--': { strict: false, args: [Object: null prototype] {} }
  },
  alias: [Object: null prototype] {
    run: 'run-script',
    urn: 'run-script',
    '-h': '--help'
  }
}

License

Licensed under the MIT License.