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.
npm install @miyako-discord/command-parserThe package is ESM-only.
import { parse, parseArgs, parseCommand, matchPrefix, tokenize } from '@miyako-discord/command-parser';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.
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 {} }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.
parse(message, options): ParseResult | nullparse 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' }
// }matchPrefix(message, options?): string | nullReturns the matched prefix string, or null if nothing matches. Matching happens in this order:
- The configured
prefixor prefixes, in the order supplied. - A Discord-style mention made from
clientId:<@id>or<@!id>. - 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: '' });
// ''parseCommand(message, options): ParsedCommand | nullFinds 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
});parseArgs(input, options?): ParsedArgsSplits 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
}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 {}
// }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' }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.
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. |
- 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
prefixand mention prefixes. AregexPrefixis 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.