|
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, "") |
|
} |