diff --git a/src/Yuuko.ts b/src/Yuuko.ts
index 1be6564..5c6d51f 100644
--- a/src/Yuuko.ts
+++ b/src/Yuuko.ts
@@ -1,10 +1,22 @@
/** @module Yuuko */
-// Exort all things from other files
+// Export all things from other files
+
+// Main classes
export * from './Client';
export * from './Command';
export * from './EventListener';
+// Argument parsing core
+export * from './argumentParsing';
+
+// Argument types for basic values
+export * from './argumentTypes/basic/IntegerArgument';
+export * from './argumentTypes/basic/NumberArgument';
+
+// Argument types for Discord stuff
+export * from './argumentTypes/discord/MemberArgument';
+
// Also export plain Eris for convenience working with its types/etc
import * as Eris from 'eris';
export {Eris};
diff --git a/src/argumentParsing.ts b/src/argumentParsing.ts
new file mode 100644
index 0000000..d929eea
--- /dev/null
+++ b/src/argumentParsing.ts
@@ -0,0 +1,61 @@
+/** An argument type's options object. */
+export interface ArgumentSpecifier {
+ type: string;
+}
+
+/** An argument's options interface and its return type inside a type tuple. */
+export type ArgumentType = [A, R];
+
+/**
+ * A function that takes an args array and the options for the argument and
+ * returns a value that matches the expected type of the argument.
+ */
+export type ArgumentResolver = (args: string[], opts: A[0]) => A[1] | Promise;
+
+// A map containing all argument parsers registered
+const argResolvers = new Map();
+
+/** Registers a custom argument type for use with `parseArgs` */
+export function registerArgumentType (id: T[0]['type'], func: ArgumentResolver): void {
+ // We're storing parsers as generic ArgumentResolvers, rather than their
+ // more specific "true" types. Type safety is still guaranteed since we've
+ // checked the type string here and we check the type string again when
+ // calling `parseArgs`, ensuring that the "true" type of the resolver here
+ // will always match the type expected by the parsing code.
+ argResolvers.set(id, func as unknown as ArgumentResolver);
+}
+
+/** Parses command arguments according to a list of argument types. */
+// There is still no good way to handle array types like this
+export async function parseArgs (args: string[], specifiers: [T[0][0]]): Promise<[T[0][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0]]): Promise<[T[0][1], T[1][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0]]): Promise<[T[0][1], T[1][1], T[2][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0], T[8][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1], T[8][1]]>;
+export async function parseArgs (args: string[], specifiers: [T[0][0], T[1][0], T[2][0], T[3][0], T[4][0], T[5][0], T[6][0], T[7][0], T[8][0], T[9][0]]): Promise<[T[0][1], T[1][1], T[2][1], T[3][1], T[4][1], T[5][1], T[6][1], T[7][1], T[8][1], T[9][1]]>;
+export async function parseArgs (args: string[], specifiers: ArgumentSpecifier[]): Promise {
+ const returnValues: any[] = [];
+ for (const specifier of specifiers) {
+ const resolver = argResolvers.get(specifier.type);
+ if (!resolver) {
+ throw new Error(`No resolver for type ${specifier.type}`);
+ }
+
+ try {
+ // Resolvers must be processed serially since they modify the `args`
+ // array, and changes need to be reflected for the next resolver
+ // eslint-disable-next-line no-await-in-loop
+ returnValues.push(await resolver(args, specifier));
+ } catch (error) {
+ // The resolver threw - re-throw the error, adding context
+ error.specifier = specifier;
+ error.argIndex = specifiers.indexOf(specifier);
+ throw error;
+ }
+ }
+ return returnValues;
+}
diff --git a/src/argumentTypes/basic/IntegerArgument.ts b/src/argumentTypes/basic/IntegerArgument.ts
new file mode 100644
index 0000000..0f2033b
--- /dev/null
+++ b/src/argumentTypes/basic/IntegerArgument.ts
@@ -0,0 +1,31 @@
+import {ArgumentType, registerArgumentType} from '../../Yuuko';
+
+/** An argument that represents an integer number. */
+type NumberArgument = ArgumentType<{
+ type: 'number';
+ /**
+ * The lower bound of accepted integers. Defaults to
+ * `Number.MIN_SAFE_INTEGER`.
+ */
+ lowerBound?: number;
+ /**
+ * The upper bound of accepted integers. Defaults to
+ * `Number.MAX_SAFE_INTEGER`.
+ */
+ upperBound?: number;
+ /** The radix/base used for parsing. Defaults to `10`. */
+ radix?: number;
+}, number>;
+
+registerArgumentType('number', (args, {
+ lowerBound = Number.MIN_SAFE_INTEGER,
+ upperBound = Number.MAX_SAFE_INTEGER,
+ radix = 10,
+}) => {
+ // shift()! is appropriate here solely because `parseFloat(undefined)` is `NaN`
+ const num = parseInt(args.shift()!, radix);
+ if (num < lowerBound || num > upperBound) {
+ throw new RangeError(`${num} is not between ${lowerBound} and ${upperBound}`);
+ }
+ return num;
+});
diff --git a/src/argumentTypes/basic/NumberArgument.ts b/src/argumentTypes/basic/NumberArgument.ts
new file mode 100644
index 0000000..2655ceb
--- /dev/null
+++ b/src/argumentTypes/basic/NumberArgument.ts
@@ -0,0 +1,22 @@
+import {ArgumentType, registerArgumentType} from '../../Yuuko';
+
+/** An argument that represents a floating-pointer number. */
+type NumberArgument = ArgumentType<{
+ type: 'number';
+ /** The lower bound of accepted integers. Defaults to `-Infinity`. */
+ lowerBound?: number;
+ /** The upper bound of accepted integers. Defaults to `Infinity`. */
+ upperBound?: number;
+}, number>;
+
+registerArgumentType('number', (args, {
+ lowerBound = -Infinity,
+ upperBound = Infinity,
+}) => {
+ // shift()! is appropriate here solely because `parseFloat(undefined)` is `NaN`
+ const num = parseFloat(args.shift()!);
+ if (num < lowerBound || num > upperBound) {
+ throw new RangeError(`${num} is not between ${lowerBound} and ${upperBound}`);
+ }
+ return num;
+});
diff --git a/src/argumentTypes/discord/MemberArgument.ts b/src/argumentTypes/discord/MemberArgument.ts
new file mode 100644
index 0000000..9379594
--- /dev/null
+++ b/src/argumentTypes/discord/MemberArgument.ts
@@ -0,0 +1,50 @@
+import Eris from 'eris';
+import {ArgumentType, registerArgumentType} from '../../Yuuko';
+
+export type MemberArgument = ArgumentType<{
+ type: 'member';
+ /** If provided, the word "me" will be interpreted the given member. */
+ me?: Eris.Member;
+ /** The guild to search for the member in. */
+ guild: Eris.Guild;
+}, Eris.Member>;
+
+registerArgumentType('member', async (args, {
+ me,
+ guild,
+}) => {
+ if (!args.length) {
+ throw new Error('Not enough arguments');
+ }
+
+ // The "me" keyword, if we're provided with a context for it
+ if (me && args[0].toLowerCase() === 'me') {
+ args.shift();
+ return me;
+ }
+
+ let match;
+
+ // Mentions and straight IDs
+ match = args[0].match(/^(?:<@!?)?(\d+)>?(?:\s+|$)/);
+ if (match) {
+ const member = guild.members.get(match[1]) || await guild.getRESTMember(match[1]).catch(() => undefined);
+ if (member) {
+ args.shift();
+ return member;
+ }
+ }
+
+ // User tags (name#discrim)
+ match = args[0].match(/^([^#]{2,32})#(\d{4})(?:\s+|$)/);
+ if (match) {
+ const member = guild.members.find(m => m.username === match[1] && m.discriminator === match[2]);
+ if (member) {
+ args.shift();
+ return member;
+ }
+ }
+
+ // Found nothing
+ throw new Error('No member found');
+});