From 4ab4e5876b04549b836465a0738669d48b090d27 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Tue, 28 Oct 2014 01:58:51 -0500 Subject: [PATCH] Rewrite LightOptionParser to support multiple parameter arguments. * LightOptionParser will only make one pass through the arguments array. * Make LOP support multiple instances of an option (-i in1 -i in2) * Make LOP support indeterminate option argument lengths (using arguments: "variable" in the definition). * Add unit tests for LightOptionParser and a testing phase during the `package` build target. --- build.xml | 2 +- project.properties | 4 +- .../jdbernard/util/LightOptionParser.groovy | 126 +++++++++++------- .../util/LightOptionParserTests.groovy | 65 +++++++++ 4 files changed, 146 insertions(+), 51 deletions(-) create mode 100644 src/test/com/jdbernard/util/LightOptionParserTests.groovy diff --git a/build.xml b/build.xml index dda64f7..684935a 100644 --- a/build.xml +++ b/build.xml @@ -4,7 +4,7 @@ - + diff --git a/project.properties b/project.properties index c9271c1..abe9784 100644 --- a/project.properties +++ b/project.properties @@ -1,6 +1,6 @@ -#Thu, 25 Sep 2014 02:29:18 -0500 +#Tue, 28 Oct 2014 01:58:09 -0500 name=jdb-util -version=2.4 +version=3.0 lib.local=true build.number=1 diff --git a/src/main/com/jdbernard/util/LightOptionParser.groovy b/src/main/com/jdbernard/util/LightOptionParser.groovy index ffd36a6..d968c33 100644 --- a/src/main/com/jdbernard/util/LightOptionParser.groovy +++ b/src/main/com/jdbernard/util/LightOptionParser.groovy @@ -11,59 +11,97 @@ public class LightOptionParser { public static def parseOptions(def optionDefinitions, List args) { - def returnOpts = [:] - def foundOpts = [:] - def optionArgIndices = [] + def returnOpts = [args:[]] - /// Find all the options. - args.eachWithIndex { arg, idx -> - if (arg.startsWith('--')) foundOpts[arg.substring(2)] = [idx: idx] - else if (arg.startsWith('-')) foundOpts[arg.substring(1)] = [idx: idx] } + /// Look through each of the arguments to see if it is an option. + /// Note that we are manually advancing the index in the loop. + for (int i = 0; i < args.size();) { - /// Look for option arguments. - foundOpts.each { foundName, optInfo -> + def retVal = false + def optName = false - def retVal + if (args[i].startsWith('--')) optName = args[i].substring(2) + else if (args[i].startsWith('-')) optName = args[i].substring(1) - /// Find the definition for this option. - def optDef = optionDefinitions.find { - it.key == foundName || it.value.longName == foundName } + /// This was recognized as an option, try to find the definition + /// and read in any arguments. + if (optName) { - if (!optDef) throw new IllegalArgumentException( - "Unrecognized option: '${args[optInfo.idx]}.") + /// Find the definition for this option. + def optDef = optionDefinitions.find { + it.key == optName || it.value.longName == optName } - def optName = optDef.key - optDef = optDef.value + if (!optDef) throw new IllegalArgumentException( + "Unrecognized option: '${args[i]}'.") - /// Remember the option index for later. - optionArgIndices << optInfo.idx + optName = optDef.key + optDef = optDef.value - /// If there are no arguments, this is a flag. - if ((optDef.arguments ?: 0) == 0) retVal = true + /// If there are no arguments, this is a flag. Set the value + /// and advance the index. + if ((optDef.arguments ?: 0) == 0) { retVal = true; i++ } - /// Otherwise, read in the arguments - if (optDef.arguments && optDef.arguments > 0) { + /// If there are a pre-determined number of arguments, read them + /// in. + else if (optDef.arguments && + optDef.arguments instanceof Number && + optDef.arguments > 0) { - /// Not enough arguments left - if ((optInfo.idx + optDef.arguments) >= args.size()) { - throw new Exception("Option '${args[optInfo.idx]}' " + - "expects ${optDef.arguments} arguments.") } + retVal = [] - int firstArgIdx = optInfo.idx + 1 + /// Not enough arguments left + if ((i + optDef.arguments) >= args.size()) { + throw new Exception("Option '${args[i]}' " + + "expects ${optDef.arguments} arguments.") } - /// Case of only one argument - if (optDef.arguments == 1) - retVal = args[firstArgIdx] - /// Case of multiple arguments - else retVal = args[firstArgIdx..<(firstArgIdx + optDef.arguments)] + /// Advance past the option onto the first argument. + i++ - /// Remember all the option argument indices for later. - (firstArgIdx..<(firstArgIdx + optDef.arguments)).each { - optionArgIndices << it }} + /// Copy the arguments + retVal += args[i..<(i + optDef.arguments)] - /// Store the value in the returnOpts map - returnOpts[optName] = retVal - if (optDef.longName) returnOpts[optDef.longName] = retVal } + /// Advance the index past end of the arguements + i += optDef.arguments } + + /// If there are a variable number of arguments, treat all + /// arguments until the next argument or the end of options as + /// arguments for this option + else if (optDef.arguments == 'variable') { + + retVal = [] + + /// Advance past the option to the first argument + i++ + + /// As long as we have not hit another option or the end of + /// arguments, keep adding arguments to the list for this + /// option. + for(;i < args.size() && !args[i].startsWith('-'); i++) + retVal << args[i] } + + else { + throw new Exception("Invalid number of arguments " + + "defined for option ${optName}. The number of " + + "arguments must be either an integer or the value " + + "'variable'") } + + /// Set the value on the option. + if (retVal instanceof Boolean) { + returnOpts[optName] = retVal + if (optDef.longName) returnOpts[optDef.longName] = retVal } + + else { + if (!returnOpts.containsKey(optName)) + returnOpts[optName] = [] + returnOpts[optName] += retVal + + if (optDef.longName) { + if (!returnOpts.containsKey(optDef.longName)) + returnOpts[optDef.longName] = [] + returnOpts[optDef.longName] += retVal } } } + + /// This was not as option, it is an unclaomed argument. + else { returnOpts.args << args[i]; i++ } } /// Check that all required options have been found. optionDefinitions.each { optName, optDef -> @@ -71,17 +109,9 @@ public class LightOptionParser { if (optDef.required && /// and it has not been found, by either it's short or long name. !(returnOpts[optName] || - (optDef.longName && returnOpts[longName]))) + (optDef.longName && returnOpts[optDef.longName]))) throw new Exception("Missing required option: '-${optName}'.") } - /// Remove all the option arguments from the args list and return just - /// the non-option arguments. - optionArgIndices.sort().reverse().each { args.remove(it) } - - //optionArgIndices = optionArgIndices.collect { args[it] } - //args.removeAll(optionArgIndices) - - returnOpts.args = args return returnOpts } } diff --git a/src/test/com/jdbernard/util/LightOptionParserTests.groovy b/src/test/com/jdbernard/util/LightOptionParserTests.groovy new file mode 100644 index 0000000..763fdd3 --- /dev/null +++ b/src/test/com/jdbernard/util/LightOptionParserTests.groovy @@ -0,0 +1,65 @@ +package com.jdbernard.util + +import groovy.util.GroovyTestCase + +import org.junit.Test + +import static com.jdbernard.util.LightOptionParser.parseOptions + +public class LightOptionParserTests extends GroovyTestCase { + + def helpDef = ['h': [longName: 'help']] + def confDef = ['c': [longName: 'config', required: true, arguments: 1]] + def fullDef = [ + h: [longName: 'help'], + c: [longName: 'config-file', required: true, arguments: 1], + i: [longName: 'input-file', arguments: 'variable'], + o: [longName: 'output-file2', arguments: 2]] + + void testShortFlagPresent1() { assert parseOptions(helpDef, ["-h"]).h } + void testShortFlagPresent2() { assert parseOptions(helpDef, ["-h"]).help } + void testLongFlagPresent() { assert parseOptions(helpDef, ["--help"]).h} + void testShortFlagPresent() { assert parseOptions(helpDef, ["--help"]).help } + + void testFlagAbsent1() { assert !parseOptions(helpDef, ["arg"]).h } + void testFlagAbsent2() { assert !parseOptions(helpDef, ["arg"]).help } + + void testRequiredOptionMissing() { + try { + parseOptions(confDef, ["arg"]) + assert false } + catch (Exception e) {} } + + void testSingleArg1() { + assert parseOptions(confDef, ["-c", "confFile"]).c == ["confFile"] } + + void testSingleArg2() { + assert parseOptions(confDef, ["-c", "confFile"]).config == ["confFile"] } + + void testUnclaimedArgsAndFlag() { + def opts = parseOptions(helpDef, ["arg1", "-h", "arg2"]) + assert opts.args == ["arg1", "arg2"] } + + void testUnclaimedAndClaimedArgs() { + def opts = parseOptions(fullDef, ["-c", "confFile", "arg1"]) + assert opts.args == ["arg1"] + assert opts.c == ["confFile"] } + + /*void testMultipleArgs1() { + def opts = parseOptions(fullDef, ["-c", "confFile", ""]) + assert .conf == ["confFile"] }*/ + + void testFull() { + def opts = parseOptions(fullDef, + ["-c", "cfgFile", "arg1", "-i", "in1", "in2", "in3", + "-o", "out1", "out2", "arg2", "-h", "-i", "in4"]) + + assert opts.h + assert opts.c == ["cfgFile"] + assert opts['config-file'] == ["cfgFile"] + assert opts.args == ["arg1", "arg2"] + assert opts.i == ["in1", "in2", "in3", "in4"] + assert opts["input-file"] == ["in1", "in2", "in3", "in4"] + assert opts.o == ["out1", "out2"] + assert opts["output-file2"] == ["out1", "out2"] } +}