argparse 可以快速轻松地处理命令行输入、处理位置参数、可选参数、标志、输入验证等等。我已经开始在 Node.js 中编写应用程序,我发现手动编写所有这些内容既乏味又耗时。
是否有一个node.js模块来处理这个问题?
有一个直接端口,方便地也称为 argparse。
有大量各种命令行参数处理程序,位于 https://github.com/joyent/node/wiki/modules#wiki-parsers-commandline
我在大多数项目中使用的是https://github.com/visionmedia/commander.js,尽管我会查看所有这些,看看哪一个适合您的特定需求。
在 18.3.0 中,nodejs 增加了一个核心功能
util.parseArgs([config])
详细文档可在此处找到:https://github.com/pkgjs/parseargs#faqs
有 yargs,它似乎相当完整且有据可查。
这是一些简单的样板代码,允许您提供命名参数:
const parse_args = () => {
const argv = process.argv.slice(2);
let args = {};
for (const arg of argv){
const [key,value] = arg.split("=");
args[key] = value;
}
return args;
}
const main = () => {
const args = parse_args()
console.log(args.name);
}
使用示例:
# pass arg name equal to monkey
node arg_test.js name=monkey
# Output
>> monkey
您还可以添加
Set
接受的名称,并在提供无效名称时抛出异常:
const parse_args = (valid_args) => {
const argv = process.argv.slice(2);
let args = {};
let invalid_args = [];
for (const arg of argv){
const [key,value] = arg.split("=");
if(valid_args.has(key)){
args[key] = value;
} else {
invalid_args.push(key);
}
}
if(invalid_args.length > 0){
throw new Exception(`Invalid args ${invalid_args} provided`);
}
return args;
}
const main = () => {
const valid_args = new Set(["name"])
const args = parse_args(valid_args)
console.log(args.name);
}
这是 Node 18 的
util.parseArgs
库的示例:
import path from 'node:path'
import url from 'node:url'
import util from 'node:util'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Parse arguments
const {
values: {
quiet: quietMode
},
} = util.parseArgs({
args: process.argv.slice(2),
options: {
quiet: {
type: 'boolean',
short: 'q',
},
},
})
console.log('Quiet mode:', quietMode); // Usage: node ./script.mjs [-q|--quiet]
我编写了一个包装器,其行为与 Python 中的
argparse
库非常相似。任何实际上未传递到内部 util.parseArgs
的选项都会添加到私有 Map
中,并在显示帮助时获取。
注意: 这是 Python
argparse
库的精简版本,因此并不完整。
/* eslint-disable no-console */
/* eslint-disable @typescript-eslint/no-unused-vars */
import path from 'node:path'
import url from 'node:url'
import util from 'node:util'
const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const capitalize = (s) => s[0].toUpperCase() + s.slice(1)
class CaseConverter {
constructor() {}
transform(_input) {
throw new Error('Not implemented')
}
toCamelCase(input) {
const [head, ...rest] = this.transform(input)
return head + rest.map(capitalize).join('')
}
toUpperSnakeCase(input) {
return this.transform(input)
.map((s) => s.toUpperCase())
.join('_')
}
}
class CamelCaseConverter extends CaseConverter {
constructor() {
super()
}
transform(input) {
return input.split(/(?=[A-Z])/)
}
}
class KebabCaseConverter extends CaseConverter {
constructor() {
super()
}
transform(input) {
return input.split('-')
}
}
const camelCaseConv = new CamelCaseConverter()
const kebabCaseConv = new KebabCaseConverter()
class ArgumentParser {
constructor(options) {
const opts = { ...ArgumentParser.DEFAULT_OPTIONS, ...options }
this.prog = opts.prog
this.usage = opts.usage
this.description = opts.description
this.epilog = opts.epilog
this.arguments = []
this.helpMap = new Map()
this.metavarMap = new Map()
}
addArgument(...args) {
if (args.length === 0) {
throw new Error('No argument supplied')
}
let options = {}
if (typeof args.slice(-1) === 'object') {
options = args.pop()
}
if (args.length === 0) {
throw new Error('No name or flag argument supplied')
}
this.#addInternal(args, options)
}
#addInternal(nameOrFlags, options) {
let longName, shortName
for (let nameOrFlag of nameOrFlags) {
if (/^--\w[\w-]+$/.test(nameOrFlag)) {
longName = kebabCaseConv.toCamelCase(nameOrFlag.replace(/^--/, ''))
} else if (/^-\w$/.test(nameOrFlag)) {
shortName = kebabCaseConv.toCamelCase(nameOrFlag.replace(/^-/, ''))
}
}
if (!longName) {
throw new Error('A long name must be provided')
}
if (options.type !== 'boolean') {
this.metavarMap.set(longName, options.metavar || camelCaseConv.toUpperSnakeCase(longName))
}
this.arguments.push({
long: longName,
short: shortName,
default: options.default,
type: options.type,
})
if (options.help) {
this.helpMap.set(longName, options.help)
}
}
#wrapText(text) {
return wordWrap(text.trim().replace(/\n/g, ' ').replace(/\s+/g, ' '), 80, '\n')
}
#getScriptName() {
return path.basename(process.argv[1])
}
#buildHelpMessage(options) {
let helpMessage = ''
const flags = Object.entries(options)
.map(([long, option]) => {
return [options.short ? `-${option.short}` : `--${long}`, this.metavarMap.get(long)].filter((o) => o).join(' ')
})
.join(' ')
helpMessage += `usage: ${this.prog ?? this.#getScriptName()} [${flags}]\n\n`
if (this.description) {
helpMessage += this.#wrapText(this.description) + '\n\n'
}
helpMessage += 'options:\n'
const opts = Object.entries(options).map(([long, option]) => {
const tokens = [`--${long}`]
if (option.short) {
tokens[0] += `, -${option.short}`
}
if (option.type) {
tokens.push(option.type)
}
return [tokens.join(' '), this.helpMap.get(long) ?? '']
})
const leftPadding = Math.max(...opts.map(([left]) => left.length))
helpMessage +=
opts
.map(([left, right]) => {
return left.padEnd(leftPadding, ' ') + ' ' + right
})
.join('\n') + '\n\n'
if (this.epilog) {
helpMessage += this.#wrapText(this.epilog)
}
return helpMessage
}
parseArgs(args) {
const options = this.arguments.concat(ArgumentParser.defaultHelpOption()).reduce((opts, argument) => {
opts[argument.long] = {
type: argument.type,
short: argument.short,
default: argument.default,
}
return opts
}, {})
const result = util.parseArgs({ args, options })
if (result.values.help === true) {
console.log(this.#buildHelpMessage(options))
process.exit(0)
}
return result
}
}
ArgumentParser.defaultHelpOption = function () {
return {
long: 'help',
short: 'h',
type: 'boolean',
}
}
ArgumentParser.DEFAULT_OPTIONS = {
prog: null, // The name of the program (default: os.path.basename(sys.argv[0]))
usage: null, // The string describing the program usage (default: generated from arguments added to parser)
description: '', // Text to display before the argument help (by default, no text)
epilog: '', // Text to display after the argument help (by default, no text)
}
/**
* Wraps a string at a max character width.
*
* If the delimiter is set, the result will be a delimited string; else, the lines as a string array.
*
* @param {string} text - Text to be wrapped
* @param {number} [maxWidth=80] - Maximum characters per line. Default is `80`
* @param {string | null | undefined} [delimiter=null] - Joins the lines if set. Default is `null`
* @returns {string | string[]} - The joined lines as a string, or an array
*/
function wordWrap(text, maxWidth = 80, delimiter = null) {
let lines = [],
found,
i
while (text.length > maxWidth) {
found = false
// Inserts new line at first whitespace of the line (right to left)
for (i = maxWidth - 1; i >= 0 && !found; i--) {
if (/\s/.test(text.charAt(i))) {
lines.push(text.slice(0, i))
text = text.slice(i + 1)
found = true
}
}
// Inserts new line at maxWidth position, since the word is too long to wrap
if (!found) {
lines.push(text.slice(0, maxWidth - 1) + '-') // Hyphenate
text = text.slice(maxWidth - 1)
}
}
if (text) lines.push(text)
return delimiter ? lines.join(delimiter) : lines
}
const argParser = new ArgumentParser({
description: `this description
was indented weird
but that is okay`,
epilog: `
likewise for this epilog whose whitespace will
be cleaned up and whose words will be wrapped
across a couple lines`,
})
argParser.addArgument('-p', '--profile', { type: 'string', help: 'environment profile' })
argParser.addArgument('-q', '--quiet', { type: 'boolean', default: false, help: 'silence logging' })
const args = argParser.parseArgs(process.argv.slice(2))
const { values } = args
const { profile, quiet: quietMode } = values
console.log('Profile:', profile)
console.log('Quiet mode:', quietMode) // Usage: node ./script.mjs [-q|--quiet]
$ node scripts/quietMode.mjs --help
usage: quietMode.mjs [--profile PROFILE --quiet --help]
this description was indented weird but that is okay
options:
--profile, -p string environment profile
--quiet, -q boolean silence logging
--help, -h boolean
likewise for this epilog whose whitespace will be cleaned up and whose words
will be wrapped across a couple lines
$ node scripts/quietMode.mjs -p foo
Profile: foo
Quiet mode: false