Skip to content
This repository has been archived by the owner on Jul 14, 2019. It is now read-only.

Command Modules

Alex Ford edited this page Mar 3, 2014 · 10 revisions

<< home


Shotgun command modules are just Node.js modules. There isn't much special about them except that they must define a special function called invoke. As with any node module you are free to create a named "some-command.js" file or a directory "some-command" with an "index.js" file placed inside it. If your command is growing in complexity it is suggested that you put it in its own directory and split up some of the functionality for that command into separate files for readability.

// shotgun_cmds/echo.js

// The invoke function is where the command logic will go.
exports.invoke = function (shell, options) {
    var iterations = options.iterations;
    for (var count = 0; count < iterations; count++) {
        shell.log(options.message);
    }
};

Within the invoke function two arguments are passed in for you to use. The first is the shell instance, complete with helper functions. You may notice that this is the same shell object as the one you instantiate within your application. This means you can add your own helper methods to the instance when you initialize it and they would be available here. The second argument is an options object which simply stores all the user-supplied options when invoking your command.

Helper Functions

The shell helper functions are as follows:

Optional Command Properties

Command modules may also expose any of four other properties.

// cmds/echo.js continued

// A string containing a short description of the command.
exports.description = 'Displays the supplied message.';

// A string containing helpful usage syntax for the user.
exports.usage = '<message> [options]';

// If true, this command module will not show up in the help menu.
exports.hidden = false;

// Options is an object containing a comprehensive list of parameters that the command accepts and understands.
exports.options = {
    message: {
        noName: true,
        required: true,
        description: 'The message to be displayed.',
        prompt: true
    },
    iterations: {
        aliases: ['i'],
        required: true,
        default: 1,
        description: 'The number of times to display the message.',
        validate: /^[1-9]\d*$/
    }
};

Any options specified in the user input will be passed to the invoke() function of the command regardless of whether or not they appear in the command's options property. The options property is used to validate specific options that the command understands. For instance, in the above example we provided a regular expression to the validation property on the iterations option. You may also supply a function to the validate property if you need more customized validation.

iterations: {
    aliases: ['i'],
    required: true,
    default: 1,
    description: 'The number of times to display the message.',
    validate: function (value, options) {
        return value > 0;
    }
}

When you define options with noName set to true, such as the message option in the above example, that lets shotgun know that this option will have no hyphenated name provided in the user input. Options without names will be added to the options object that is passed to the command's invoke() function in the order they are found in the parsed user input. For example:

echo "Dance monkey, dance!" -i 5

Using the sample 'echo' command we defined earlier the above sample user input would yield the following:

// cmds/echo.js

exports.invoke = function (shell, options) {
    options.iterations === 5; // true
    options.message === "Dance monkey, dance!"; // true
};

Since the message option has noName set to true shotgun simply parses the user input and adds the first non-named option to the options object under message. The order matters if the option has noName enabled. If you have more than one option with noName: true then the user must supply them in the order they appear in your module. Defining a usage string really helps here so the user knows what format to supply their input to satisfy your command.

I stated earlier that named options are passed to the command even if they are not defined in the options property of that command. Thus, the following is valid even though we did not define verbose as an expected option:

echo "Dance monkey, dance!" -i 5 --verbose

would yield:

// cmds/echo.js

exports.invoke = function (shell, options) {
    options.verbose == true; // true
};

Despite verbose not being defined as part of the options property of the command module, it is still accessible if provided by the user. It will just be optional, won't undergo any validation, and won't show up in that command module's help information.

Defined Command Options

As explained above, the user can supply any option they wish and your command module could access that value via the supplied options object. Defining exports.options on your command module is just a way to tell shotgun what options your module understands and what rules to apply to those options if they are found. Here is a comprehensive list of available properties you can set for each option you define for your command module:

aliases

exports.options = {
    message: {
        aliases: ['m', 'msg']
    }
};

Sometimes you may not want the user to have to type --message as a parameter for your command every single time. You can supply aliases so that the user can supply one of the aliases instead. In the above example the user could supply -m or --msg instead of --message if they chose to.

default

exports.options = {
    message: {
        default: 'Hello World'
    }
};

Defining options with a default value ensures that the option will have a value even if the user does not supply one. In the above example the user could supply a message, but if they don't then the default value of "Hello World" would be used.

description

exports.options = {
    message: {
        description: "A message to be displayed."
    }
};

You don't have to supply a description, but if you do then it will show up when the user attempts to get help information for the command by typing help commandName.

noName

exports.options = {
    message: {
        noName: true
    }
};

Supplying noName: true tells shotgun that this option does not have to be specified by name. For example, the user could supply "some value" instead of --message "some value". Keep in mind that options with no name are evaluated in order. If you have two options with noName: true then the first user-supplied value without a name will be used for the first option you defined and the second user-supplied value with no name will be used for the second option. The order does not matter when options are supplied with a name, even if noName is true.

hidden

exports.options = {
    message: {
        hidden: true
    }
};

Supplying hidden: true will cause the default "help" command to hide this option when showing available options for the command. This is useful if you have set noName: true since you will likely include unnamed command options in the command's usage string rather than in the list of named options.

Example:

// login.js
var db = require('./db');
exports.description = "Allows the user to sign in with their username and password.";
exports.usage = "[username] [password]";
exports.options = {
    username: {
        noName: true,
        required: true,
        prompt: "Please enter your username.",
        validate: function (username) {
            return db.checkUserExists(username);
        },
        hidden: true
    },
    password: {
        noName: true,
        required: true,
        prompt: "Please enter your password.",
        validate: function (password, options) {
            var user = db.getUser(options.username);
            return user.password === password;
        },
        hidden: true,
        password: true
    }
};

exports.invoke = function (shell, options) {
    // Do authentication stuff.
    shell.log("Login successful :)");
};

In the above module there is no reason to display "--username" or "--password" in the help menu because users will almost never explicitly supply them. They will either include them with no names or be prompted for them.

prompt

exports.options = {
    message: {
        prompt: true // or prompt: 'Enter a message.'
    }
};

If you specify prompt: true on your option and required is also true then the user will be prompted for the value if they did not supply the option themselves. If prompt is true but required is not set to true and the user supplied the option with no value then they will be prompted for the value. If prompt is set to true then it will prompt the user with a default message like "Enter value for message." You also have the option to supply your own message by simply replacing true with a string. The supplied string will be displayed instead of the default message.

password

exports.options = {
    message: {
        password: true
    }
};

Setting password: true only has an effect if a prompt is also set. This tells shotgun to set data.password = true;. If the UI chooses to it could use this password property to modify the UI input field to be a password field for the prompt. This is useful for a login command where the command will prompt the user for their password.

required

exports.options = {
    message: {
        required: true
    }
};

This is one of the simpler options. If you set required: true on an option then shotgun will display an error to the user if they do not supply a value. One caveat is if you supply a default value or a prompt, either the default value will be used if there is no user-supplied value or the user will be prompted for the value.

validate

// Regular expression.
exports.options = {
    message: {
        validate: /^[a-z0-9]$/i // Only alpha-numeric characters are allowed.
    }
};

// Validation Function
exports.options = {
    message: {
        validate: function (value, shell, options) {
            return value === 'Hello world!';
        }
    }
};

Validate allows you to specify a regular expression or a function that will inform shotgun that the supplied value should be validated. If the value does not pass validation then an error will be displayed to the user and they will have to supply valid input before the command will be invoked. If you use a validation function it passes in three parameters. The first is the value supplied by the user for that option. The second is all of the supplied options the user passed in.

NOTE: Keep in mind that command options are validated in order. If you access any user-supplied options in your validation function, any options that appear after the one you're currently validating will not have been validated yet so be careful with them.

You will rarely (if ever) have to use the options parameter; if your validation logic is dependent upon other options then you most likely want to do that work in the invoke function after all options have been validated. The third option is the shell instance, giving you access to the same functions available in your invoke function.

Our example 'echo' command

What we did in a previous example is create a simple command called 'echo' that will be plugged into the shotgun shell framework simply by placing the module in the 'shotgun_cmds' directory (or the directory you passed into the shell).

The example command we just wrote is a pretty simple command. It performs a small task and only accepts one option. The nice thing about shotgun is that you don't have to do any pre-parsing of user input before passing it along to the module. Shotgun does all the legwork for you, allowing you to focus on creating well-designed command modules instead of worrying about user input, context, etc.

shell.on('log', function (text, options) {
    console.log(text);
}).execute('echo -i 5 "Hello world!"');

The log event would be invoked five times and the console would yield:

Hello world!
Hello world!
Hello world!
Hello world!
Hello world!

Each time the shell.log helper function is called the log event is emitted and contains the text that will be displayed for that line as well as an options object containing meta information about the line such as bold, italic, underline, etc. The UI can then apply whatever display options it is capable of providing, but the options can be safely ignored if necessary. For example, if you were writing a console application then bold, italics, and underline wouldn't be possible. The options are just metadata that does not have to be used.

Overriding user-supplied options.

Within your command modules the invoke function is the main entry point and is passed two arguments. The first is the shell instance containing all your built-in and any custom helper methods. The second is an options object containing the user-supplied options.

shell.execute also takes an options object in case you need to make values available to a command without them needing to be supplied as user input.

// app.js

shell.execute('mycommand', context, { someValue: true });
// cmds/mycommand.js

exports.invoke = function (shell, options) {
    shell.log('Custom value: ' + options.someValue);
};

Values supplied in this manner will override user input that matches it, so be mindful of the options you pass in. For example:

// app.js

shell.execute('mycommand --someValue "pizza"', context, { someValue: 'bacon' });

will yield:

// cmds/mycommand.js

exports.invoke = function (shell, options) {
    options.someValue === 'bacon'; // true
};

Asynchronous Command Modules

It is not uncommon to want to do asynchronous work within your command modules. However, in all the examples we've seen so far shotgun would fire the done event before any asynchronous code were to finish. Luckily, we haven't had any asynchronous code in our examples yet so everything has worked as expected. But what if you had a command module like this?

exports.invoke = function (shell, options) {
    fs.readFile(options.filePath, function (err, fileData) {
        if (err) return shell.error(err);
        shell.log(fileData);
    });
};

The above code would work but it is likely to cause weird problems depending on the application. In the console application we created in the Getting Started tutorial our prompt would resume asking the user for input before the above code was finished running.

Luckily, shotgun has you covered here too :)

exports.invoke = function (shell, options, done) {
    fs.readFile(options.filePath, function (err, fileData) {
        if (err) return shell.error(err);
        shell.log(fileData);
        done();
    });
};

Easy right? Just call the done callback when your asynchronous work is complete and shotgun will not fire the done event until then. Accepting a third argument in our invoke function is what tells shotgun to treat the command module as an asynchronous command module. The done callback also accepts an error argument that it passes to shell.error for convenience.


Next up: Help Command >>