Skip to content

Instantly share code, notes, and snippets.

@nexpr
Last active June 3, 2018 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nexpr/2d3871b9794c0707b9c23cbcc24b91af to your computer and use it in GitHub Desktop.
Save nexpr/2d3871b9794c0707b9c23cbcc24b91af to your computer and use it in GitHub Desktop.
parse Node.js's argument (argv)

パース

require("parseargs")(opt).parse(argv)

argv を渡さないと自動で process.argv.slice(2) になる
alias や型など 特別設定いらないなら opt は渡さなくてもいい

{
  options: {
    [name]: {
      alias: "(array) エイリアスの名前 ここに指定した名前のパラメータは[name]に置き換わる",
      type: "(string) number/string/boolean/count/object/auto",
      array: "(boolean) true にすると複数を許可 配列型になる false なら最後のもので上書きされる",
      default: "デフォルト値",
      converter: "(function) この関数で変換される エラーチェックで代替値に置き換えたり",
      required: "(boolean) 必須かどうか ない場合には _error にメッセージが入る",
      description: "(string) ヒントに表示される",
    },
  },
  help: {
    usage: "ヘルプに表示する使い方",
    description: "ヘルプに表示する説明",
  }
}

ヘルプ

require("parseargs")(opt).help()

オプションをもとにヘルプメッセージを作る

import Sargv from "./parseargs"
const sargv = new Sargv({
options: {
foo: {
alias: ["f", "fo"],
type: "number",
array: true,
default: 100,
description: "description of foo parameter",
},
bar: {
type: "string",
description: "description of bar parameter",
}
},
help: {
usage: "node example.mjs --foo [number] --bar [string]",
description: "description...description...description...",
}
})
console.log(sargv.help())
console.log(sargv.parse())
/*
$ node --experimental-modules e.mjs a b --foo 10 -f 20 30 --bar 001 -xyz
## Help ##
Usage: node example.mjs --foo [number] --bar [string]
description...description...description...
--foo
-f
--fo
[Optional] Default: 100
description of foo parameter"
--bar
[Optional]
description of bar parameter"
{ _: [ 'a', 'b', '30' ],
'--': [],
_error: [],
raw:
[ 'a', 'b', '--foo', '10', '-f', '20', '30', '--bar', '001', '-xyz' ],
foo: [ 10, 20 ],
bar: '001',
x: true,
y: true,
z: true }
*/
export default class {
constructor(opt) {
opt = opt || {}
opt.options = opt.options || {}
opt.help = opt.help || {}
this.option = opt
this.defaults = {
alias: [],
type: "auto",
array: false,
default: true,
converter: null,
required: false,
description: "",
}
const map = {}
const requried_list = []
for (const [name, value] of Object.entries(opt.options)) {
for (const n of toArray(value.alias || [])) {
map[n] = name
}
if (value.required) {
requried_list.push(name)
}
}
this.alias_map = map
this.requried_list = requried_list
}
parse(args = process.argv.slice(2)) {
const result = {
_: [],
"--": [],
_error: [],
raw: args,
}
// object -> continue from previous argument
// false -> already found "--", skip and append to "--"
let context = null
for (const arg of args) {
if (context === false) {
result["--"].push(arg)
} else if (arg === "--") {
context = false
} else if (arg.startsWith("--")) {
const [name, value] = split2(arg.substr(2), "=")
const resolved_name = this.alias_map[name] || name
const option = Object.assign({}, this.defaults, this.option.options[resolved_name])
result[resolved_name] = getValue(option, result[resolved_name], value)
context = value || ["boolean", "count"].includes(option.type) ? null : { name: resolved_name, option }
} else if (arg.startsWith("-")) {
const [names_str, value] = split2(arg.substr(1), "=")
const names = names_str.split("")
const last_name = names.pop()
for (const name of names) {
const resolved_name = this.alias_map[name] || name
const option = Object.assign({}, this.defaults, this.option.options[resolved_name])
result[resolved_name] = getValue(option, result[resolved_name])
}
const resolved_name = this.alias_map[last_name] || last_name
const option = Object.assign({}, this.defaults, this.option.options[resolved_name])
result[resolved_name] = getValue(option, result[resolved_name], value)
context =
value || ["boolean", "count"].includes(option.type) ? null : { name: resolved_name, option }
} else if (context === null) {
result._.push(arg)
} else {
const current = result[context.name]
if(Array.isArray(current) && context.option.array) current.pop()
result[context.name] = getValue(context.option, current, arg)
context = null
}
}
for (const name of this.requried_list) {
if (!(name in result)) {
result._error.push(`${name} is required.`)
}
}
return result
function getValue(option, current, value) {
const type = option.type
if (type === "count") {
return ~~current + 1
}
let v = null
if (typeof option.converter === "function") {
v = option.converter(value || null, option)
} else {
if (!value) {
v = option.default
} else {
if (type === "object") {
try {
v = JSON.parse(value)
} catch {
v = option.default
}
} else if (type === "string") {
v = value
} else if (type === "number") {
v = +value
} else if (type === "boolean") {
v = true
} else if (type === "auto") {
v =
value === "true" ? true : value === "false" ? false : !isNaN(+value) ? +value : value
}
}
}
return option.array ? (current ? current.concat([v]) : [v]) : v
}
}
help() {
const option = this.option
const help = option.help
const options_help = Object.entries(option.options)
.map(([name, value]) => {
let str = ""
for (const key of [name, ...toArray(value.alias || [])]) {
str += ` ${key.length > 1 ? "--" : "-"}${key}\n`
}
{
const r = value.required ? "Required" : "Optional"
const d = value.default ? "Default: " + value.default : ""
str += ` [${r}] ${d}\n`
}
if (value.description) {
str += ` ${value.description}\n`
}
return str
})
.join("\n")
return here`>
> ## Help ##
>
> Usage: ${help.usage || "node script.js"}
>
> ${help.description || ""}
>
>${options_help}
`
}
}
function toArray(a) {
return Array.isArray(a) ? a : [a]
}
function split2(str, word) {
const idx = str.indexOf(word)
if (idx >= 0) {
return [str.substr(0, idx), str.substr(idx + word.length)]
} else {
return [str, ""]
}
}
// here`>
// > aaa
// > bb${1}cc
// `
function here({ raw }, ...values) {
const idx = raw.findIndex(e => e.includes("\n"))
if (idx === -1) throw new Error("invalid here doc: only one line found")
const [, first_part, remain_part] = raw[idx].match(/^(.*?)(\n.*)$/s)
const firstline = String.raw(
{ raw: raw.slice(0, idx).concat([first_part]) },
...values.slice(0, idx),
)
const key_char = firstline.trim()
if (!key_char) throw new Error("invalid here doc: missing key_char")
const body = String.raw(
{
raw: [remain_part, ...raw.slice(idx + 1)].map(part => {
const [f, ...r] = part.split("\n")
const trimmed = r.map(line => {
const idx = line.indexOf(key_char)
if (idx < 0) return ""
return line.substr(idx + key_char.length)
})
return [f, ...trimmed].join("\n")
}),
},
...values.slice(idx),
)
return body.replace(/^\n|\n$/g, "")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment