是否有一个相当于 Node.js 的 Python argparse 的模块?

发布于 2024-12-26 23:37:36 字数 221 浏览 1 评论 0原文

argparse for python 可以快速轻松地处理命令行输入、处理位置参数、可选参数、标志、输入验证等等。我已经开始在 Node.js 中编写应用程序,并且发现手动编写所有这些内容既乏味又耗时。

有没有一个node.js 模块可以处理这个问题?

argparse for python makes it quick and easy to handle command-line input, handling positional arguments, optional arguments, flags, input validation and much more. I've started writing applications in node.js and I'm finding it tedious and time consuming to write all that stuff manually.

Is there a node.js module for handling this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

遗忘曾经 2025-01-02 23:37:36

有一个直接端口,方便地也称为 argparse。

There is one direct port, conveniently also called argparse.

尛丟丟 2025-01-02 23:37:36

https://github 上有大量各种命令行参数处理程序。 com/joyent/node/wiki/modules#wiki-parsers-commandline

我在大多数项目中使用的是 https://github.com/visionmedia/commander.js 不过我会看一下所有这些,看看哪一个适合您的特定需求。

There are a slew of various command line argument handlers at https://github.com/joyent/node/wiki/modules#wiki-parsers-commandline

The one I use in most projects is https://github.com/visionmedia/commander.js though I would take a look at all of them to see which one suits your specific needs.

离去的眼神 2025-01-02 23:37:36

在 18.3.0 中,nodejs 增加了一个核心功能 util.parseArgs([config])

详细文档可在此处获取:https://github.com/pkgjs/parseargs#faqs

In 18.3.0 nodejs has landed a core addition util.parseArgs([config])

Detailed documentation is available here: https://github.com/pkgjs/parseargs#faqs

π浅易 2025-01-02 23:37:36

yargs,它似乎非常完整且有据可查。

There's yargs, which seems to be pretty complete and well documented.

你的他你的她 2025-01-02 23:37:36

以下是一些简单的样板代码,允许您提供命名参数:

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

您还可以添加一组接受的名称,并在提供无效名称时抛出异常:

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);
}

Here is some simple boilerplate code which allows you to provide named args:

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);
}

Example usage:

# pass arg name equal to monkey
node arg_test.js name=monkey
# Output
>> monkey

You can also add a Set of accepted names and throw exception if an invalid name is provided:

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);
}
作业与我同在 2025-01-02 23:37:36

以下是 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]

ArgumentParser 包装器类

我编写了一个包装器,其行为与 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

我为 kebab-case、camelCase 和 UPPER_SNAKE_CASE(也称为 SCREAMING_SNAKE_CASE)编写了自己的大小写转换策略,但您可以使用 js-convert-case npm 模块代替。

Here is an example of Node 18's util.parseArgs library:

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]

ArgumentParser wrapper class

I wrote a wrapper that acts very similar to the argparse library in Python. Any option that does not actually get passed to the internal util.parseArgs is added to a private Map and fetched when displaying the help.

Note: This is a stripped-down version of Python's argparse library, so this is not complete.

/* 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
}

Usage

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]

Output

$ 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

I wrote my own case conversion strategy for kebab-case, camelCase, and UPPER_SNAKE_CASE (also referred to as SCREAMING_SNAKE_CASE), but you could us the js-convert-case npm module instead.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文