Skip to main content

Using arguments in your commands

Often when using commands, you may want to get data from the user and change the response accordingly. In this section, we'll demonstrate how to create a command that pulls a string from the message and echoes it back to the user!

String arguments

A string argument is any textual data after the command's invocation. For example, in @bot say Hello!, Hello! would be the first string argument; however, note that multiple words (e.g. Hello there!) are separated individually into multiple arguments. We'll look into how to handle those later.

In your commands folder, make a new file called echo.js and set up the command class just like the one in the ping command.

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class EchoCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
aliases: ['parrot', 'copy'],
description: 'Replies with the text you provide'
});
}

public async messageRun(message: Message, args: Args) {
// ...
}
}

We have a new parameter in the messageRun method: args! This is a structure that abstracts all the argument parsing and resolution.

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class EchoCommand extends Command {
public async messageRun(message: Message, args: Args) {
const text = await args.pick('string');
await message.channel.send(text);
}
}

Sapphire's arguments work by consuming the delimited elements of user input one by one upon success, similar to how an iterator works. We'll go into more details and examples in another command.

In this command, args.pick('<type>') is used to read a single argument from the user's input. The <type> here is string, so it'll return the argument as-is since all data in a message's body is textual.

If @bot say Hello there! is sent, it'll return Hello since each word is split into its own argument as aforementioned, so Hello there! is converted to ['Hello', 'there!'] but args.pick() only reads one argument. However, there are alternatives:

  • Quoted arguments: If the argument content is quoted, e.g. "Hello there!" or β€œHello there!” instead of Hello there!, the entire argument (excluding the quotes) will be read as one.

  • args.rest(), such as:

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class EchoCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
quotes: []
});
}

public async messageRun(message: Message, args: Args) {
const text = await args.rest('string');
await message.channel.send(text);
}
}

By default, Command defines 3 pairs of valid quotes:

  • " and " (double quotes)
  • β€œ and ” (iOS smart quotes)
  • γ€Œ and 」 (CJK corner brackets)

This is troublesome if you want to allow sending quotes within the content, as those would be excluded from rest, so quotes must be set to an empty array. This way, when @bot echo Hello "there!" is sent, your bot will reply with Hello "there!".

caution

args.rest() consumes all the arguments and then processes them, so you will not be able to consume any more arguments after it.

Multiple arguments

Let's make an add command, which takes two numbers and adds them. Create an add.js file in your commands folder and set up the command class again. Since two arguments are needed, use args.pick() twice:

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
description: 'Adds two numbers'
});
}

public async messageRun(message: Message, args: Args) {
const a = await args.pick('number');
const b = await args.pick('number');
return message.channel.send(`The result is ${a + b}!`);
}
}

Each time args.pick('<type>') is called, it consumes the next argument and parses it as the <type> given, so @bot add 1 2 would return The result is 3! for instance.

Optional arguments

Sapphire offers two options regarding optional arguments. Let's make a pow.js command that takes two numbers, where the base is required, and the exponent defaults to 2.

  • Optional Pattern
import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public async messageRun(message: Message, args: Args) {
const base = await args.pick('number');
const exponent = await args.pick('number').catch(() => 2);
return message.channel.send(`The result is ${Math.pow(base, exponent)}!`);
}
}

Since exponent defaults to 2, this allows users to send @bot pow 4, returning The result is 16!. However, this pattern ignores any possible errors, including invalid numbers, so @bot pow 4 hello! would return the same response as before. There is another pattern that enables you to use optional arguments but retain error reporting:

  • Semi-required Pattern
import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public async messageRun(message: Message, args: Args) {
const base = await args.pick('number');
const exponent = args.finished ? 2 : await args.pick('number');
return message.channel.send(`The result is ${Math.pow(base, exponent)}!`);
}
}

This is slightly more verbose, but it only uses the default value for exponent of 2 if there were no arguments left to parse, so @bot pow 4 will return The result is 16!, while @bot pow 4 hello! will return an error.

Each pattern has its use-cases. The former is especially helpful for handling several optional arguments, although you can also distinguish all errors by checking error.identifier. This way you can tell why something failed to parse, for instance, a number may fail to parse if the user did not provide a valid number.

Repeating arguments

To work with repeating arguments, you can use args.repeat(), which works like args.pick() but returns an array of parsed elements up to the desired amount of times, resolving when it can validate at least the first argument.

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public async messageRun(message: Message, args: Args) {
const numbers = await args.repeat('number');
return message.channel.send(`The highest number is ${Math.max(...numbers)}!`);
}
}

Sapphire will try to parse all the arguments until there are none left to consume or one fails to parse. As such, the following inputs will yield the following results:

  • @bot max 42 2 5 60 3 will return The highest number is 60!.
  • @bot max aa 42 2 5 6 will throw an error as it cannot parse aa to a number.
  • @bot max 42 2 5 aa 60 will return The highest number is 42!. The 60 is not included because it stopped parsing at aa.

While the last case seems restrictive, it can be used to parse repeated arguments at any given position in the user's input. For example, a ban command that takes multiple users could be written as:

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
description: 'Adds two numbers'
});
}

public async messageRun(message: Message, args: Args) {
const members = await args.repeat('member');
const reason = args.finished ? null : args.rest('string');

// ...
}
}

The code above allows the following uses:

  • @bot ban @User1 Hello there (members is [User1] and reason is 'Hello there').
  • @bot ban @User1 @User2 (members is [User1, User2] and reason is null).
  • @bot ban Hello there throws an error as it could not match a member.

Additionally, we can tell the argument parser how many times it should parse at most by specifying times when we call args.repeat():

import { Command, Args } from '@sapphire/framework';
import type { Message } from 'discord.js';

export class MathsCommand extends Command {
public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
description: 'Adds two numbers'
});
}

public async messageRun(message: Message, args: Args) {
const members = await args.repeat('member', { times: 5 });
// ...
}
}

The code above matches any number of members up to 5, so it will never return an array of 6 or more.