Skip to content

Instantly share code, notes, and snippets.

@kamicane
Last active May 23, 2019 02:28
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 kamicane/b9e94c9c214d56314b70aa16d0c41b39 to your computer and use it in GitHub Desktop.
Save kamicane/b9e94c9c214d56314b70aa16d0c41b39 to your computer and use it in GitHub Desktop.
[Clint 2] a command line parser
'use strict'
const CAMEL_REGEXP = /[-_]+([A-z])/g
module.exports = function camelize (string) {
return string.replace(CAMEL_REGEXP, (full, match) => match.toUpperCase())
}
'use strict'
const camelize = require('./camelize')
const OPTION_REGEXP = /^--(?:(no)-)?(\w{2,}(?:[\w-]+)?)$/
const FLOAT_REGEXP = /^[+-]?(\d+)?(\.\d+)?$/
const FLAG_REGEXP = /^-(\w+)$/
const TYPE = {
boolean: boolean,
number: number,
list: {
append: true,
transform: value
},
value: value,
auto: auto,
identity: identity
}
class Clint {
constructor (options) {
this.aliases = Object.create(null)
this.transforms = Object.create(null)
this.appends = Object.create(null)
options = Object.assign({
'*': {
transform: TYPE.identity
},
'--*': {
transform: TYPE.auto
},
input: {
transform: TYPE.auto,
alias: 'i'
},
output: {
transform: TYPE.auto,
alias: 'o'
},
version: {
transform: TYPE.boolean,
alias: 'v'
},
help: {
transform: TYPE.boolean,
alias: 'h'
},
force: {
transform: TYPE.boolean,
alias: 'f'
},
add: {
transform: TYPE.boolean,
alias: 'a'
},
remove: {
transform: TYPE.boolean,
alias: 'r'
}
}, options)
for (let name in options) {
let option = options[name]
let optionName = camelize(name)
if (option == null || option === false) continue
let aliases = []
if (option === true) {
this.transforms[optionName] = (v) => v
} else if (typeof option === 'function') {
this.transforms[optionName] = option
} else {
if (option.alias) aliases.push(option.alias)
if (option.aliases) aliases.push(...option.aliases)
this.transforms[optionName] = option.transform
if (option.append) this.appends[optionName] = option.append
}
for (let alias of aliases) this.aliases[alias] = optionName
}
// check for aliases recursion
for (let alias in this.aliases) {
let value = this.aliases[alias]
if (this.aliases[value] === alias) {
let message = `recursive alias ${alias} > ${value}, ${value} > ${alias}`
throw new Error(message)
}
}
}
parse (argv) {
const args = []
const opts = Object.create(null)
const aliases = this.aliases
let prev = false
// recursively resolve aliases
const alias = function (key) {
while (aliases[key]) key = aliases[key]
return key
}
const setOpt = (key, value) => {
let transform = this.transforms[key] || this.transforms['--*']
if (!transform) return
let list
if (this.appends[key]) list = opts[key] || (opts[key] = [])
value = transform(value)
if (value == null) return
if (list) {
list.push(value)
return true // special return true for successful append
} else {
opts[key] = value
}
}
const setPrev = (value) => {
if (prev) {
let isList = setOpt(prev, value)
if (!isList) prev = false
}
}
for (let arg of argv) {
const match = arg.match(OPTION_REGEXP)
// IS NOT "--(no-)option"
if (!match) {
const match = arg.match(FLAG_REGEXP)
// IS "-o"
if (match) {
setPrev(true)
for (let f of match[1].split('')) setOpt(alias(f), true)
// IS "value"
} else {
const transform = this.transforms['*']
// set value for the previous waiting option
if (prev) setPrev(arg)
// otherwise push the value to the current list (def: args)
else args.push(transform ? transform(arg) : arg)
}
// IS "--(no-)option"
} else {
// set true if last one was waiting
setPrev(true)
const tag = match[1]
const option = alias(camelize(match[2]))
// const ls = match[3]
// IS "--no-option"
if (tag === 'no') {
setOpt(option, 'no')
} else {
// wait for value
prev = option
}
}
}
// set true if last one was waiting
setPrev(true)
return { args, opts }
}
}
function boolean (string) {
if (/^(no|n|false)$/.test(string)) return false
if (/^(yes|y|true)$/.test(string)) return true
}
function number (string) {
let m = FLOAT_REGEXP.test(string)
if (m) return parseFloat(string)
}
function auto (arg) {
let value = boolean(arg)
if (value == null) value = number(arg)
if (value == null) value = arg
return value
}
function identity (arg) {
return arg
}
function value (arg) {
if (typeof arg !== 'string') return
let value = number(arg)
if (value == null) value = arg
return value
}
function clint (...args) {
return new Clint(...args)
}
clint.parse = function () {
return new Clint().parse(...arguments)
}
clint.TYPE = TYPE
clint.Clint = Clint
module.exports = clint
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment