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

问题描述 投票:0回答:6
python 的

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

是否有一个node.js模块来处理这个问题?

node.js module command-line-arguments
6个回答
10
投票

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


8
投票

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

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


5
投票

在 18.3.0 中,nodejs 增加了一个核心功能

util.parseArgs([config])

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


1
投票

yargs,它似乎相当完整且有据可查。


0
投票

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

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

0
投票

这是 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
© www.soinside.com 2019 - 2024. All rights reserved.