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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
0 comments:
Post a Comment