Declarative Configuration
Gunshi allows you to configure your commands declaratively, making your CLI code more organized and maintainable. This approach separates the command definition from its execution logic.
Basic Declarative Structure
A declaratively configured command in Gunshi typically has this structure:
const command = {
// Command metadata
name: 'command-name',
description: 'Command description',
// Command arguments
args: {
// Argument definitions
},
// Command examples
examples: 'Example usage',
// Command execution function
run: ctx => {
// Command implementation
}
}
Complete Example
Here's a complete example of a command with declarative configuration:
import { cli } from 'gunshi'
// Define a command with declarative configuration
const command = {
// Command metadata
name: 'greet',
description: 'A greeting command with declarative configuration',
// Command arguments with descriptions
args: {
name: {
type: 'string',
short: 'n',
description: 'Name to greet'
},
// Add a positional argument using 'file' as the key
file: {
type: 'positional',
description: 'Input file to process'
},
greeting: {
type: 'string',
short: 'g',
default: 'Hello',
description: 'Greeting to use (default: "Hello")'
},
times: {
type: 'number',
short: 't',
default: 1,
description: 'Number of times to repeat the greeting (default: 1)'
},
verbose: {
type: 'boolean',
short: 'V',
description: 'Enable verbose output',
negatable: true // Add this to enable --no-verbose
},
banner: {
// Added another boolean option for grouping example
type: 'boolean',
short: 'b',
description: 'Show banner'
}
},
// Command examples
examples: `# Examples
$ node index.js <input-file.txt> --name World
$ node index.js <input-file.txt> -n World -g "Hey there" -t 3
# Boolean short options can be grouped: -V -b is the same as -Vb
$ node index.js <input-file.txt> -Vb -n World
# Using the negatable option
$ node index.js <input-file.txt> --no-verbose -n World
# Using rest arguments after \`--\` (arguments after \`--\` are not parsed by gunshi)
$ node index.js <input-file.txt> -n User -- --foo --bar buz
`, // Added comma here
// Command execution function
run: ctx => {
// If 'verbose' is defined with negatable: true:
// - true if -V or --verbose is passed
// - false if --no-verbose is passed
// - undefined if neither is passed (or default value if set)
// Access positional argument 'file' via ctx.values.file
const { name = 'World', greeting, times, verbose, banner, file } = ctx.values
if (banner) {
// Added check for banner
console.log('*** GREETING ***')
}
if (verbose) {
console.log('Running in verbose mode...')
console.log('Context values:', ctx.values)
console.log('Input file (from positional via ctx.values.file):', file)
console.log('Raw positional array (ctx.positionals):', ctx.positionals) // Still available
}
// Process the input file (example placeholder)
console.log(`\nProcessing file: ${file}...`)
// Repeat the greeting the specified number of times
for (let i = 0; i < times; i++) {
console.log(`${greeting}, ${name}!`)
}
// Print rest arguments if they exist
if (ctx.rest.length > 0) {
console.log('\nRest arguments received:')
for (const [index, arg] of ctx.rest.entries()) {
console.log(` ${index + 1}: ${arg}`)
}
}
}
}
// Run the command with the declarative configuration
await cli(process.argv.slice(2), command, {
name: 'declarative-example',
version: '1.0.0',
description: 'Example of declarative command configuration'
})
Command Configuration Options
Command Metadata
name
: The name of the commanddescription
: A description of what the command does
Command Options
Each option can have the following properties:
type
: The data type ('string', 'number', 'boolean')short
: A single-character alias for the option.Multiple boolean short options can be grouped together.
(e.g.,
-Vb
is equivalent to-V -b
). Options requiring values (likestring
,number
,enum
) cannot be part of a group.description
: A description of what the option doesdefault
: Default value if the option is not providedrequired
: Set totrue
if the option is required (Note: Positional arguments defined withtype: 'positional'
are implicitly required by the parser).multiple
: Set totrue
if the multiple option values are be allowedtoKebab
: Set totrue
to convert camelCase argument names to kebab-case in help text and command-line usageparse
: A function to parse and validate the argument value. Required whentype
is 'custom'
Positional Arguments
To define arguments that are identified by their position rather than a name/flag (like --name
), set their type
to 'positional'
. The key you use for the argument in the args
object serves as its name for accessing the value later.
const command = {
args: {
// ... other options
// 'source' is the key and the name used to access the value
source: {
type: 'positional',
description: 'The source file path'
},
// 'destination' is the key and the name used to access the value
destination: {
type: 'positional',
description: 'The destination file path'
}
// ... potentially more positional arguments
}
}
- Implicitly Required: When you define an argument with
type: 'positional'
in the schema, Gunshi (viaargs-tokens
) expects it to be present on the command line. If it's missing, a validation error will occur. They cannot be truly optional like named flags. - Order Matters: Positional arguments are matched based on the order they appear on the command line and the order they are defined in the
args
object. - Accessing Values: The resolved value is accessible via
ctx.values
, using the key you defined in theargs
object (e.g.,ctx.values.source
,ctx.values.destination
). ctx.positionals
: This array still exists and contains the raw string values of positional arguments in the order they were parsed (e.g.,ctx.positionals[0]
,ctx.positionals[1]
). While available, usingctx.values.<key>
is generally preferred for clarity and consistency.- Descriptions: The
description
property is used for generating help/usage messages. - Type Conversion:
args-tokens
resolves positional arguments as strings. You typically need to perform type conversions or further validation on the values accessed viactx.values.<key>
within yourrun
function based on your application's needs.
Custom Type Arguments
Gunshi supports custom argument types with user-defined parsing logic. This allows you to create complex argument types that can parse and validate input in any way you need, and a validation library like zod
.
To define a custom argument type:
import { z } from 'zod'
// custom schema with `zod`
const config = z.object({
debug: z.boolean(),
mode: z.string()
})
const command = {
name: 'example',
description: 'Example command with custom argument types',
args: {
// CSV parser example
tags: {
type: 'custom',
short: 't',
description: 'Comma-separated list of tags',
parse: value => value.split(',').map(tag => tag.trim())
},
// JSON parser example with `zod`
config: {
type: 'custom',
short: 'c',
description: 'JSON configuration',
parse: value => {
return config.parse(JSON.parse(value))
}
},
// Custom validation example
port: {
type: 'custom',
short: 'p',
description: 'Port number (1024-65535)',
parse: value => {
const port = Number(value)
if (Number.isNaN(port) || port < 1024 || port > 65_535) {
throw new TypeError(`Invalid port: ${value}. Must be a number between 1024 and 65535`)
}
return port
}
}
},
run: ctx => {
// Access the parsed values
console.log('Tags:', ctx.values.tags) // Array of strings
console.log('Config:', ctx.values.config) // Parsed JSON object
console.log('Port:', ctx.values.port) // Validated port number
}
}
Custom type arguments support:
- Type safety: The return type of the
parse
function is properly inferred in TypeScript - Validation: Throw an error from the
parse
function to indicate invalid input - Default values: Set a
default
property to provide a value when the argument is not specified - Multiple values: Set
multiple: true
to allow multiple instances of the argument - Short aliases: Set a
short
property to provide a single-character alias
Kebab-Case Argument Names
TIP
This feature is particularly useful for users migrating from the cac
library, which automatically converts camelCase argument names to kebab-case. If you're transitioning from cac
to Gunshi, enabling the toKebab
option will help maintain the same command-line interface for your users.
By default, argument names are displayed in the help text and used on the command line exactly as they are defined in the args
object. However, it's common practice in CLI applications to use kebab-case for multi-word argument names (e.g., --user-name
instead of --userName
).
Gunshi supports automatic conversion of camelCase argument names to kebab-case with the toKebab
property. There are two different toKebab
properties in Gunshi:
Command-level
toKebab
: This is a property of theCommand
object itself. When set totrue
, it applies kebab-case conversion to all arguments in the command, unless overridden at the argument level.Argument-level
toKebab
: This is a property of theArgSchema
object (individual argument definition). It controls kebab-case conversion for a specific argument and takes precedence over the command-level setting.
The toKebab
property can be set at two levels:
Command level: Apply to all arguments in the command
jsconst command = { name: 'example', description: 'Example command', toKebab: true, // Apply to all arguments args: { userName: { type: 'string' }, // Will be displayed as --user-name maxRetries: { type: 'number' } // Will be displayed as --max-retries }, run: ctx => { /* ... */ } }
Argument level: Apply to specific arguments only
jsconst command = { name: 'example', description: 'Example command', args: { userName: { type: 'string', toKebab: true // Will be displayed as --user-name }, maxRetries: { type: 'number' } // Will remain as --maxRetries }, run: ctx => { /* ... */ } }
When toKebab
is enabled:
- Argument names are converted from camelCase to kebab-case in help text and usage information
- Parameter placeholders are also displayed in kebab-case (e.g.,
--user-name <user-name>
) - Negatable boolean options use kebab-case (e.g.,
--no-auto-save
forautoSave: { type: 'boolean', negatable: true, toKebab: true }
)
NOTE
The argument values are still accessed using the original camelCase keys in your code (e.g., ctx.values.userName
), regardless of how they appear on the command line.
Negatable Boolean Options
To enable a negatable version of a boolean option (e.g., allowing both --verbose
and --no-verbose
), you need to add the negatable: true
property to the option's definition.
- If you define an option like
verbose: { type: 'boolean', negatable: true }
, Gunshi will recognize both--verbose
and--no-verbose
. - If
-V
or--verbose
is passed, the value will betrue
. - If
--no-verbose
is passed, the value will befalse
. - If neither is passed, the value will be
undefined
(unless adefault
is specified).
Without negatable: true
, only the positive form (e.g., --verbose
) is recognized, and passing it sets the value to true
.
The description for the negatable option (e.g., --no-verbose
) is automatically generated (e.g., "Negatable of --verbose"). You can customize this message using internationalization resource files by providing a translation for the specific arg:no-<optionName>
key (e.g., arg:no-verbose
).
Examples
The examples
property provides example commands showing how to use the CLI.
Command Execution
The run
function receives a command context object (ctx
) with:
args
: The command arguments configuration (ArgSchema
object).values
: An object containing the resolved values for both named options (e.g.,ctx.values.name
) and positional arguments (accessed via their key from theargs
definition, e.g.,ctx.values.file
). Positional values are stored as strings.positionals
: An array of strings containing the raw values of the arguments identified as positional, in the order they were parsed. Useful if you need the original order, butctx.values.<key>
is generally recommended.rest
: An array of strings containing arguments that appear after the--
separator.argv
: The raw argument array passed to thecli
function.tokens
: The raw tokens parsed byargs-tokens
.omitted
: A boolean indicating if the command was run without specifying a subcommand name.command
: The resolved command definition object itself.cliOptions
: The resolved CLI options passed tocli
.name
: The name of the currently executing command.description
: The description of the currently executing command.env
: The command environment settings (version, logger, renderers, etc.).
CLI Configuration
When calling the cli
function, you can provide additional configuration:
await cli(process.argv.slice(2), command, {
name: 'app-name',
version: '1.0.0',
description: 'Application description'
// Additional configuration options
})
Benefits of Declarative Configuration
Using declarative configuration offers several advantages:
- Separation of concerns: Command definition is separate from implementation
- Self-documentation: The structure clearly documents the command's capabilities
- Maintainability: Easier to understand and modify
- Consistency: Enforces a consistent structure across commands