Update: YACLP for Slack/Hubot

on Monday, April 29, 2019

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

0 comments:

Post a Comment


Creative Commons License
This site uses Alex Gorbatchev's SyntaxHighlighter, and hosted by herdingcode.com's Jon Galloway.