Skip to content

Repository files navigation

Command Parser

A small, dependency-free TypeScript/JavaScript parser for prefix-based bot commands. It recognizes command prefixes, splits quoted arguments, and separates positional arguments from boolean flags and key-value options. It is designed for Discord, Fluxer, and similar message-based command interfaces, but works with any string input.

Installation

npm install @miyako-discord/command-parser

The package is ESM-only.

import { parse, parseArgs, parseCommand, matchPrefix, tokenize } from '@miyako-discord/command-parser';

Quick start

import { parse } from '@miyako-discord/command-parser';

const result = parse('!ban "Very Loud User" --reason=spam /silent', {
    prefix: '!'
});

// {
//   prefix: '!',
//   command: 'ban',
//   rawArgs: '"Very Loud User" --reason=spam /silent',
//   args: ['Very Loud User'],
//   flags: Set { 'silent' },
//   options: Map { 'reason' => 'spam' }
// }

parse() returns null when the input is empty, no configured prefix matches, or the matched prefix is not followed by a command.

Concepts

Positional arguments

Positional arguments are ordinary values that remain after recognized flags and options are removed. Whitespace separates them; matching quotes group whitespace into one argument.

parseArgs('send "hello world" general');
// { args: ['send', 'hello world', 'general'], flags: Set {}, options: Map {} }

Flags vs. options

A flag is a presence-only switch. It has a name but no value, so it is returned in a Set<string>.

--force
/silent
parseArgs('deploy --force /silent').flags;
// Set { 'force', 'silent' }

An option has both a name and a value, so it is returned in a Map<string, string>.

--color=blue
/timeout:30
parseArgs('paint --color=blue /timeout:30').options;
// Map { 'color' => 'blue', 'timeout' => '30' }

By default, an option needs : or = between its key and value. A bare prefixed token such as --verbose is therefore a flag. Enable spaceOptions when --key value should be an option instead:

parseArgs('copy --destination archive --overwrite', { spaceOptions: true });
// {
//   args: ['copy'],
//   flags: Set { 'overwrite' },
//   options: Map { 'destination' => 'archive' }
// }

When a key appears more than once, the last parsed option value wins because options are stored in a Map.

Full-message parsing: parse

parse(message, options): ParseResult | null

parse is the usual entry point. It first calls parseCommand to find the prefix, command, and raw argument text, then calls parseArgs on that raw text. Its options combine prefix-matching options and argument-parsing options.

const parsed = parse('!search "command parser" --page=2 --exact', {
    prefix: '!',
    flags: ['exact'],
    options: ['page']
});

// {
//   prefix: '!',
//   command: 'search',
//   rawArgs: '"command parser" --page=2 --exact',
//   args: ['command parser'],
//   flags: Set { 'exact' },
//   options: Map { 'page' => '2' }
// }

Prefix matching: matchPrefix

matchPrefix(message, options?): string | null

Returns the matched prefix string, or null if nothing matches. Matching happens in this order:

  1. The configured prefix or prefixes, in the order supplied.
  2. A Discord-style mention made from clientId: <@id> or <@!id>.
  3. The full match of regexPrefix.
matchPrefix('!help', { prefix: ['?', '!'] });
// '!'

matchPrefix('<@!123456> ping', { prefix: '!', clientId: '123456' });
// '<@!123456>'

matchPrefix('hey ruby, help', {
    regexPrefix: /^(?:(?:hey|hi) ruby,? )/i
});
// 'hey ruby, '

An empty-string prefix is valid and matches every message. Use it only when the whole input should be treated as a command invocation.

matchPrefix('help topic', { prefix: '' });
// ''

Command parsing: parseCommand

parseCommand(message, options): ParsedCommand | null

Finds a prefix with matchPrefix, then returns the command name and untouched argument substring (apart from leading/trailing whitespace around the command boundary). It does not tokenize arguments or interpret flags/options.

parseCommand('!remind "drink water" --after=10m', { prefix: '!' });
// {
//   prefix: '!',
//   command: 'remind',
//   rawArgs: '"drink water" --after=10m'
// }

This is useful when command metadata determines how its arguments should be parsed:

const command = parseCommand(message.content, { prefix: ['!', '?'] });
if (!command) return;

const definition = commands.get(command.command);
if (!definition) return;

const arguments_ = parseArgs(command.rawArgs, {
    flags: definition.flags,
    options: definition.options
});

Argument parsing: parseArgs

parseArgs(input, options?): ParsedArgs

Splits input with tokenize, classifies recognized prefixed tokens, and returns:

{
    args: string[];
    flags: Set<string>;
    options: Map<string, string>;
}

Its defaults are:

{
    flags: true,
    options: true,
    argumentPrefixes: ['--', '/'],
    optionDelimiters: [':', '='],
    spaceOptions: false
}

Restricting accepted names

Set flags or options to an array to parse only known names. Unrecognized prefixed tokens stay in args, which is useful for command-specific validation.

parseArgs('user --permanent --reason=spam --unknown', {
    flags: ['permanent'],
    options: ['reason']
});
// {
//   args: ['user', '--unknown'],
//   flags: Set { 'permanent' },
//   options: Map { 'reason' => 'spam' }
// }

Set either value to false to disable that category, or leave it as true to accept every name.

parseArgs('hello --loud --count=2', { flags: false, options: false });
// {
//   args: ['hello', '--loud', '--count=2'],
//   flags: Set {},
//   options: Map {}
// }

Custom syntax

argumentPrefixes controls which leading strings introduce a flag or option. optionDelimiters controls which strings divide an option key from its value.

parseArgs('publish !draft @channel:announcements @retries=3', {
    argumentPrefixes: ['!', '@'],
    optionDelimiters: [':', '=']
});
// {
//   args: ['publish'],
//   flags: Set { 'draft' },
//   options: Map { 'channel' => 'announcements', 'retries' => '3' }
// }

For delimiter-based options, the first matching delimiter in the configured delimiter order is used, and only the first occurrence of that delimiter splits the token. Values may be empty:

parseArgs('--label= --url=https://example.com/path');
// options: Map { 'label' => '', 'url' => 'https://example.com/path' }

With spaceOptions: true, a recognized bare option consumes exactly the next token as its value—even if that next token looks like a flag. If no next token exists, the bare token is handled as a flag when flags are enabled.

An empty prefix is also supported to allow for arguments like hello world option=true to be used, however because an empty prefix will match everything, it is recommended to use the flags/options options with an array to filter the intended options only.

parseArgs('hello world option=true', {
    argumentPrefixes: [''],
    flags: false, // disable flag parsing or every word becomes a flag (or pass a filtered array)
    options: ['option']
});
// options: Map(1) { 'option' => 'true' }

Tokenizing: tokenize

tokenize(input, options?): string[]

Splits text on whitespace and removes matching quote characters while preserving the text inside them. Quotes can appear within a token, so adjacent text is combined.

tokenize('say "hello world" now');
// ['say', 'hello world', 'now']

tokenize('file="my notes.txt"');
// ['file=my notes.txt']

The default quote pairs are exposed as DefaultQuotes:

import { DefaultQuotes } from '@miyako-discord/command-parser';

// [ ['"', '"'], ['“', '”'], ['‘', '’'], ['«', '»'], ['‹', '›'],
//   ['「', '」'], ['『', '』'], ['《', '》'], ['〈', '〉'] ]

Pass quotes to replace that list entirely:

tokenize('say [hello world]', { quotes: [['[', ']']] });
// ['say', 'hello world']

Quotes are simple grouping markers: there is no escape syntax, and an opening quote without a later matching closing quote is treated as a normal character.

Types and options reference

ParseOptions combines MatchPrefixOptions and ParseArgsOptions. ParseResult combines ParsedCommand and ParsedArgs.

Type Fields Meaning
MatchPrefixOptions prefix?: string | string[] One prefix or an ordered list of prefixes. '' is allowed.
clientId?: string Enables <@id> and <@!id> mention prefixes after explicit prefixes are checked.
regexPrefix?: RegExp A regular expression whose full match becomes the prefix.
ParseArgsOptions flags?: boolean | string[] true accepts all flags; false accepts none; an array allowlists names.
options?: boolean | string[] true accepts all options; false accepts none; an array allowlists names.
argumentPrefixes?: readonly string[] Prefixes for flags/options. Default: ['--', '/'].
optionDelimiters?: readonly string[] Separators for inline options. Default: [':', '='].
spaceOptions?: boolean Enables --name value options. Default: false.
TokenizeOptions quotes?: readonly (readonly [string, string])[] Opening/closing quote pairs; replaces DefaultQuotes.
ParsedCommand prefix, command, rawArgs Information extracted before argument parsing.
ParsedArgs args, flags, options Parsed positional values, presence switches, and key-value settings.

Behaviour notes

  • Prefixes are checked in the supplied order. Put a longer overlapping prefix before its shorter form when that distinction matters.
  • Prefix matching is anchored by string prefix checks for prefix and mention prefixes. A regexPrefix is used as provided, so anchor it with ^ when it must match only at the beginning.
  • Command names are separated from arguments by a literal space. The remaining argument text is then tokenized using all whitespace.
  • The parser does not support combined short flags such as -abc, -- as an end-of-options marker, escaping quotes, type coercion, or automatic validation. Values are always strings.

License

MIT

About

A simple, flexible command parser with support for prefixes, quoted arguments, flags, and options.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages