This is an update for Yet Another Command Line Parser for Slack/Hubot.
Since the last post, a few minor updates have been made:
- Added a validation set for a required parameter
product-name action {required-param-name=option1|option2|option3}
- Added a validation set for an optional parameter with a default value
product-name action {required-param-name=option1|option2:option2}
- Made the parameter check Case Insensitive
- Added ability to take in an empty string as an input value (“”)
- Added parsing that can handle multiple spaces between parameters
- Added parameters passed in format parameter-name=value format
Here’s the updated version:
require("./array-includes-polyfill") | |
function parse(format, input) { | |
format = format.toLowerCase() | |
var optionsIndex = format.indexOf("{") | |
var start = format.substring(0, optionsIndex).trim() | |
// console.log(`start = ${start}`) | |
var optionslist = format.substring(optionsIndex) | |
// console.log(`optionslist: ${optionslist}`) | |
var optionsSplit = optionslist.split(' ') | |
// console.log(`optionsSplit: ${optionsSplit}`) | |
// attempt to recombine options which might have multiterm default values | |
var recombinedOptions = [] | |
for(var i = 0; i < optionsSplit.length; i++) { | |
var term = optionsSplit[i] | |
if(term.startsWith('{') == false) { | |
recombinedOptions.push(term) | |
continue | |
} | |
if(term.endsWith('}') == false) { | |
var combined = term | |
while(term.endsWith('}') == false && i < optionsSplit.length) { | |
i++ | |
term = optionsSplit[i] | |
combined += " " + term | |
} | |
recombinedOptions.push(combined) | |
} else { | |
recombinedOptions.push(term) | |
} | |
} | |
optionsSplit = recombinedOptions | |
// parse out the default values | |
var defaults = {} | |
var validationSets = {} | |
optionsSplit.forEach(option => { | |
option = option.replace('{','') | |
option = option.replace('}','') | |
optionSplit = option.split(':') | |
if(optionSplit.length === 1) { | |
value = "required" | |
} else { | |
value = optionSplit[1] | |
} | |
var name = optionSplit[0] | |
var nameSplit = name.split('=') | |
var validValues = undefined | |
if(nameSplit.length > 1) { | |
name = nameSplit[0] | |
validValues = nameSplit[1].split('|') | |
} | |
// console.log(`option: ${name} = ${value}`) | |
defaults[name] = value | |
validationSets[name] = validValues | |
}); | |
argv = JSON.parse(JSON.stringify(defaults)); // deep copy | |
used = JSON.parse(JSON.stringify(defaults)); // deep copy | |
var usedvalue = "used-" + (Math.random() * 99999) | |
// if no matching input was given, return the defaults | |
var match = input.match(start) | |
if(match === null) { | |
argv["success"] = false | |
argv["errors"] = { "required" : `Usage: ${usageString(start, defaults, validationSets)}` } | |
return argv | |
} | |
// parse input for the parameters | |
var inputIndex = input.indexOf(start) | |
// console.log(`input: ${input}`) | |
// console.log(`inputIndex: ${inputIndex}`) | |
// console.log(`start.length: ${start.length}`) | |
// console.log(`inputIndex + start.length: ${inputIndex + start.length}`) | |
var inputOptions = input.substring(inputIndex + start.length).trim() | |
if(inputOptions.length > 0) { | |
// console.log(`inputOptions: ${inputOptions}`) | |
// console.log(`inputOptions.length: ${inputOptions.length}`) | |
var inputOptionsSplit = inputOptions.split(/[\s=]+/) | |
// console.log(`inputOptionsSplit: ${inputOptionsSplit}`) | |
// console.log(`inputOptionsSplit.length: ${inputOptionsSplit.length}`) | |
// console.log(`argv-1:`);console.log(argv) | |
// try to fix typos, where a multiterm value has the double-quote accidently in the middle of a word | |
var fixedMissingSpaceTypoSplit = [] | |
for(var i = 0; i < inputOptionsSplit.length; i++) { | |
var term = inputOptionsSplit[i] | |
var doubleQuotesIndex = term.indexOf('"') | |
if(doubleQuotesIndex > 0 && doubleQuotesIndex < term.length - 1) { | |
var first = term.substring(0, doubleQuotesIndex) | |
var last = term.substring(doubleQuotesIndex) | |
fixedMissingSpaceTypoSplit.push(first) | |
fixedMissingSpaceTypoSplit.push(last) | |
} else { | |
fixedMissingSpaceTypoSplit.push(term) | |
} | |
} | |
inputOptionsSplit = fixedMissingSpaceTypoSplit | |
// attempt to recombine options which might have multiterm default values | |
var recombinedOptions = [] | |
for(var i = 0; i < inputOptionsSplit.length; i++) { | |
var term = inputOptionsSplit[i] | |
if(term.startsWith('"') == false) { | |
recombinedOptions.push(term) | |
continue | |
} | |
if(term.endsWith('"') == false) { | |
var combined = term | |
while(term.endsWith('"') == false && i < inputOptionsSplit.length) { | |
i++ | |
term = inputOptionsSplit[i] | |
combined += " " + term | |
} | |
combined = combined.substring(1, combined.length - 1) // removes start and end quotes | |
recombinedOptions.push(combined) | |
} else { | |
recombinedOptions.push(term) | |
} | |
} | |
inputOptionsSplit = recombinedOptions | |
var currentIndex = 0 | |
for(var i = 0; i < inputOptionsSplit.length; i++) { | |
var term = inputOptionsSplit[i] | |
var key = "" | |
if(term.startsWith("-") || term.startsWith("--")) { | |
// so a named parameter was explicitely passed in, set the appropriate value by name | |
key = term | |
if(key.startsWith("--")) { key = key.substring(2) } | |
if(key.startsWith("-")) { key = key.substring(1) } | |
i++ | |
term = inputOptionsSplit[i] | |
var ki = keyIndex(defaults, key) | |
if(ki == currentIndex) currentIndex++ | |
} else { | |
var ki = keyIndex(defaults, term) | |
if(ki !== -1) { | |
// a named parameter was inferred as passed in, set the appropriate value by name | |
key = term | |
i++ | |
term = inputOptionsSplit[i] | |
var ki = keyIndex(defaults, key) | |
if(ki == currentIndex) currentIndex++ | |
} else { | |
// no named parameter was given, so just set the next value | |
key = keyAt(defaults, currentIndex) | |
if(key == undefined) { | |
// sometimes there's a typo and you can get too many input values, then prevents an infinite loop when that happens | |
// for example, input of: @sysbot servicepro close 42191"Verified the Financial application and thank everybody for this big step in simplifying the deployments!"" | |
// there should only be two parameters; but because there is a missing space, there are 15 parameters; so we need | |
// to stop when we run out of parameters to fill in. | |
continue; | |
} | |
currentIndex++ | |
key = key | |
while(used[key] == usedvalue) { // check the value hasn't already been set | |
key = keyAt(defaults, currentIndex) | |
key = key | |
currentIndex++ | |
} | |
} | |
} | |
if(term == "\"\"") { term = "" } | |
var lkey = key.toLowerCase() | |
argv[lkey] = term | |
used[lkey] = usedvalue | |
} | |
} | |
// validate required parameters were set | |
var errors = {} | |
var failures = false | |
for(var d in defaults) { | |
var a = argv[d] | |
if(defaults[d] == "required") { | |
if(a == "required" || a.length == 0) { | |
var usage = usageString(start, defaults, validationSets) | |
errors[d] = `'${d}' is a required parameter. Usage: ${usage}` | |
failures = true | |
} | |
} | |
var vs = validationSets[d] | |
if(vs != undefined) { | |
if(!vs.includes(a.toLowerCase())) { | |
var valids = combineParams(vs, ", ") | |
var usage = usageString(start, defaults, validationSets) | |
errors[d] = `'${d}' is not set to a valid value (value: ${a}). Valid values include: ${valids}. Usage: ${usage}` | |
failures = true | |
} | |
} | |
} | |
argv["success"] = failures == false | |
argv["errors"] = errors | |
return argv | |
} | |
function keyIndex(object, key) { | |
var i = 0 | |
var lkey = key.toLowerCase() | |
for(var current in object) { | |
if(current == lkey) return i | |
i++ | |
} | |
return -1 | |
} | |
function keyAt(object, index) { | |
var i = 0 | |
for(var current in object) { | |
if(i == index) return current | |
i++ | |
} | |
} | |
function usageString(start, defaults, validationSets) { | |
var usage = "" | |
for(var d in defaults) { | |
var p = `${d}` | |
if(validationSets[d] != undefined) { | |
var vs = combineParams(validationSets[d], '|') | |
p = `${d}=${vs}` | |
} | |
var v = defaults[d] | |
var term = "" | |
if(v == "required") { | |
term = `<${p}>` | |
} else { | |
term = `[${p}:${v}]` | |
} | |
if(usage.length > 0) { usage += " " } | |
usage += term | |
} | |
if(!start.endsWith(" ")) { start += " " } | |
var result = start + usage | |
return result | |
} | |
function sendErrors(params, msg) { | |
errorMessage = "" | |
for(k in params.errors) { | |
if(errorMessage.length > 0) errorMessage += "\r\n" | |
errorMessage += params.errors[k] | |
} | |
errorMessage = "```" + errorMessage + "```" | |
msg.send(errorMessage) | |
} | |
function combineParams(toCombine, delim) { | |
var combined = "" | |
for(var k in toCombine) { | |
var v = toCombine[k] | |
if(combined.length > 0) { combined += delim } | |
combined += v | |
} | |
return combined | |
} | |
function displayParams(params, hide) { | |
var display = "" | |
hide = hide || [] | |
if(hide.indexOf("success") === -1) hide.push("success") | |
if(hide.indexOf("errors") === -1) hide.push("errors") | |
for(var key in params) { | |
var hidden = false | |
for(var i = 0; i < hide.length; i++) { | |
var h = hide[i] | |
if(key == h) hidden = true | |
} | |
if(hidden) continue; | |
if(display.length > 0) display += ", " | |
display += `${key}:'${params[key]}'` | |
} | |
return display | |
} | |
module.exports = { parse: parse, sendErrors: sendErrors, displayParams: displayParams } | |
// function test(format, input, hide) { | |
// console.log("format: '" + format + "'") | |
// console.log("input: '" + input + "'") | |
// var results = parse(format, input) | |
// console.log(results) | |
// if(hide === undefined) | |
// console.log("displayParams: '" + displayParams(results) + "'") | |
// else | |
// console.log("displayParams: '" + displayParams(results, hide) + "'") | |
// } | |
// test("octo cancel {projectname:*} {releaseid:*}", "sysbot octo cancel gold") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel gold 20") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel --projectname gold") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel --projectname gold 20") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel projectname gold 20") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel --releaseid 20 gold") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel releaseid 20 gold") | |
// test("octo cancel {projectname:*} {releaseid:*} {somethignrequired}", "octo cancel releaseid 20 gold") | |
// test("octo cancel {projectname:*} {releaseid:*}", "octo cancel releaseid 20 gold", ["releaseid"]) | |
// test("octo cancel {projectname:*} {releaseid:*}", "sysbot octo list") | |
// test("octo list {projectname:*} {releaseid:*}", "sysbot octo list") | |
// test("octo list {first:*} {second:*} {third:*} {fourth:*}", "sysbot octo list --first first-value -third 3 second-value fourth-value") | |
// test("servicepro close {ticketnumber} {memo:Verified and Closing}", "servicepro close 487156") | |
// test("servicepro close {ticketnumber} {memo:Verified and Closing}", "servicepro close 487156 \"a long description with a lot of terms\"") | |
// test("servicepro close {ticketnumber} {memo:Verified and Closing}", "sysbot servicepro close 487156 \"a long description with a lot of terms\"") | |
// test("servicepro close {ticketnumber} {memo:Verified and Closing}", "@sysbot servicepro close 42191\"Verified the Financial application and thank everybody for this big step in simplifying the deployments!\"") | |
// test("servicepro close {ticketnumber} {resolution=success-no-help|success-help|nil} {memo:Verified and Closing}", "@sysbot servicepro close 11111 success-help") | |
// test("servicepro close {ticketnumber} {resolution=success-no-help|success-help|nil} {memo:Verified and Closing}", "@sysbot servicepro close 11111 something-unknown") | |
// test("servicepro close {ticketnumber} {resolution=success-no-help|success-help|nil:nil} {memo:Verified and Closing}", "@sysbot servicepro close 11111 something-unknown") | |
// test("servicepro close {ticketnumber} {caseinsensitive}", "@sysbot servicepro close CaseInsensitive done 90809") | |
// test("notify sub list {contactType:*} {destination:*} {component:*} {messageType:*} {tags:*} {createdby:current-user}", "@sysbot notify sub list -CreatedBy maglio-s") | |
// test("notify sub list {contactType:*} {destination:*} {component:*} {messageType:*} {tags:*} {createdby:current-user}", "@sysbot notify sub list createdby \"\"") | |
// test("db create {dbname} {env=all|dev|test|prod}", "@sysbot db create Test All") | |
// test("octo cancel {projectname} {releaseid}", "sysbot octo cancel blah.bal 1.0.2342") | |
// test("octo cancel {projectname} {releaseid}", "sysbot octo cancel blah.bal releaseid=1.0.2342") |