Reworked CLI command line interface.

CLI interface options make more sense, are parsed in a more organized fashion,
and the interface is more informative regarding its actions.
This commit is contained in:
Jonathan Bernard 2011-08-09 17:24:53 -05:00
parent e99b65fb16
commit b04655a428
8 changed files with 165 additions and 90 deletions

View File

@ -3,8 +3,8 @@ build.dir=build
src.dir=src src.dir=src
lib.shared.dir=../shared-libs lib.shared.dir=../shared-libs
test.dir=test test.dir=test
build.number=4 build.number=1
expected.application.version=2.5.1 expected.application.version=2.6.0
lib.dir=lib lib.dir=lib
release.dir=release release.dir=release
release.jar=pit-${application.version}.jar release.jar=pit-${application.version}.jar

Binary file not shown.

BIN
pit-cli/lib/pit-2.6.0.jar Normal file

Binary file not shown.

View File

@ -1,9 +1,9 @@
#Sat Feb 27 03:01:15 CST 2010 #Tue, 09 Aug 2011 17:21:28 -0500
build.dir=build build.dir=build
src.dir=src src.dir=src
build.jar=pit-cli-${application.version}.${build.number}.jar build.jar=pit-cli-${application.version}.${build.number}.jar
build.number=5 build.number=7
expected.application.version=2.5.1 expected.application.version=2.6.0
lib.dir=lib lib.dir=lib
release.dir=release release.dir=release
release.jar=pit-cli-${application.version}.jar release.jar=pit-cli-${application.version}.jar

Binary file not shown.

Binary file not shown.

View File

@ -5,6 +5,8 @@ import com.jdbernard.pit.file.*
import static java.lang.Math.max import static java.lang.Math.max
import static java.lang.Math.min import static java.lang.Math.min
// -------- command-line interface specification -------- //
def cli = new CliBuilder(usage: 'pit-cli [options]') def cli = new CliBuilder(usage: 'pit-cli [options]')
cli.h(longOpt: 'help', 'Show help information.') cli.h(longOpt: 'help', 'Show help information.')
cli.v(longOpt: 'verbose', 'Show verbose task information') cli.v(longOpt: 'verbose', 'Show verbose task information')
@ -14,8 +16,8 @@ cli.i(argName: 'id', longOpt: 'id', args: 1,
'Filter issues by id. Accepts a comma-delimited list.') 'Filter issues by id. Accepts a comma-delimited list.')
cli.c(argName: 'category', longOpt: 'category', args: 1, cli.c(argName: 'category', longOpt: 'category', args: 1,
'Filter issues by category (bug, feature, task). Accepts a ' 'Filter issues by category (bug, feature, task). Accepts a '
+ 'comma-delimited list.') + 'comma-delimited list. By default all categories are selected.')
cli.t(argName: 'status', longOpt: 'status', args: 1, cli.s(argName: 'status', longOpt: 'status', args: 1,
'Filter issues by status (new, reassigned, rejected, resolved, ' + 'Filter issues by status (new, reassigned, rejected, resolved, ' +
'validation_required)') 'validation_required)')
cli.p(argName: 'priority', longOpt: 'priority', args: 1, cli.p(argName: 'priority', longOpt: 'priority', args: 1,
@ -24,152 +26,225 @@ cli.p(argName: 'priority', longOpt: 'priority', args: 1,
cli.r(argName: 'project', longOpt: 'project', args: 1, cli.r(argName: 'project', longOpt: 'project', args: 1,
'Filter issues by project (relative to the current directory). Accepts a ' 'Filter issues by project (relative to the current directory). Accepts a '
+ 'comma-delimited list.') + 'comma-delimited list.')
cli.s(longOpt: 'show-subprojects', /*cli.s(longOpt: 'show-subprojects',
'Include sup projects in listing (default behaviour)') 'Include sup projects in listing (default behaviour)')
cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.') cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')*/ // TODO: figure out better flags for these options.
cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1, cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1,
required: false, 'Modify the priority of the selected issues.') required: false, 'Modify the priority of the selected issues.')
cli.C(argName: 'new-category', longOpt: 'set-category', args: 1, cli.C(argName: 'new-category', longOpt: 'set-category', args: 1,
required: false, 'Modify the category of the selected issues.') required: false, 'Modify the category of the selected issues.')
cli.T(argName: 'new-status', longOpt: 'set-status', args: 1, cli.S(argName: 'new-status', longOpt: 'set-status', args: 1,
required: false, 'Modify the status of the selected issues.') required: false, 'Modify the status of the selected issues.')
cli.n(longOpt: 'new-issue', 'Create a new issue.') cli.n(longOpt: 'new-issue', 'Create a new issue.')
cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false, cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false,
'Use <dir> as the base directory (defaults to current directory).') 'Use <dir> as the base directory (defaults to current directory).')
// -------- parse CLI options -------- //
def opts = cli.parse(args) def opts = cli.parse(args)
def issuedb = [:] def issuedb = [:]
def workingDir = new File('.') def workingDir = new File('.')
if (!opts) System.exit(1) // better solution? // defaults for the issue filter/selector
def selectOpts = [
categories: ['bug', 'feature', 'task'],
status: ['new', 'validation_required'],
priority: 9,
projects: [],
ids: [],
acceptProjects: true]
if (opts.h) cli.usage() // defaults for changing properties of issue(s)
def assignOpts = [
category: Category.TASK,
status: Status.NEW,
priority: 5,
text: "New issue."]
def categories = ['bug','feature','task'] if (!opts) opts.l = true; // default to 'list'
if (opts.c) categories = opts.c.split(/[,\s]/)
categories = categories.collect { Category.toCategory(it) }
def statusList = ['new', 'validation_required'] if (opts.h) {
if (opts.t) statusList = opts.t.split(/[,\s]/) cli.usage()
statusList = statusList.collect { Status.toStatus(it) } System.exit(0) }
// read the category filter designation(s)
if (opts.c) {
if (opts.c =~ /all/) {} // no-op, same as defaults
else { selectOpts.categories = opts.c.split(/[,\s]/) } }
// parse the categories names into Category objects
try { selectOpts.categories =
selectOpts.categories.collect { Category.toCategory(it) } }
catch (Exception e) {
println "Invalid category option: '-c ${e.localizedMessage}'."
println "Valid options are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read the status filter designation(s)
if (opts.s) {
// -s all
if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected',
'resolved', 'validation_required']
// is <list>
else selectOpts.status = opts.s.split(/[,\s]/) }
// parse the statuses into Status objects
try { selectOpts.status =
selectOpts.status.collect { Status.toStatus(it) } }
catch (Exception e) {
println "Invalid status option: '-s ${e.localizedMessage}'."
println "Valid options are: \b${Status.values().join(', ')}"
println " (abbreviations are accepted.)"
System.exit(1) }
// read and parse the priority filter
if (opts.p) try {
selectOpts.priority = opts.p.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-p ${opts.p}'."
println "Valid values are: 0-9"
System.exit(1) }
// read and parse the projects filter
if (opts.r) { selectOpts.projects =
opts.r.toLowerCase().split(/[,\s]/).asType(List.class) }
// read and parse the ids filter
if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) }
// TODO: accept projects value from input
// read and parse the category to assign
if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) }
catch (Exception e) {
println "Invalid category option: '-C ${e.localizedMessage}'."
println "Valid categories are: \n${Category.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the status to assign
if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) }
catch (Exception e) {
println "Invalid status option: '-S ${e.localizedMessage}'."
println "Valid stasus options are: \n{Status.values().join(', ')}"
println " (abbreviations are accepted)."
System.exit(1) }
// read and parse the priority to assign
if (opts.P) try {assignOpts.priority = opts.P.toInteger() }
catch (NumberFormatException nfe) {
println "Not a valid priority value: '-P ${opts.P}'."
println "Valid values are: 0-9"
System.exit(1) }
// look for assignment text
if (opts.getArgs().length > 0) {
assignOpts.text = opts.getArgs()[0] }
// set the project working directory
if (opts.d) { if (opts.d) {
workingDir = new File(opts.d.trim()) workingDir = new File(opts.d.trim())
if (!workingDir.exists()) { if (!workingDir.exists()) {
println "Directory '${workingDir}' does not exist." println "Directory '${workingDir}' does not exist."
return -1 return -1 } }
}
}
def EOL = System.getProperty('line.separator') def EOL = System.getProperty('line.separator')
// build issue list // build issue list
issuedb = new FileProject(workingDir) issuedb = new FileProject(workingDir)
// build filter from options // build filter from options
def filter = new Filter('categories': categories, def filter = new Filter(selectOpts)
'status': statusList,
'priority': (opts.p ? opts.p.toInteger() : 9),
'projects': (opts.r ? opts.r.toLowerCase().split(/[,\s]/).asType(List.class) : []),
'ids': (opts.i ? opts.i.split(/[,\s]/).asType(List.class) : []),
'acceptProjects': (opts.s || !opts.S))
// list first // list first
if (opts.l) { if (opts.l) {
// local function (closure) to print a single issue
def printIssue = { issue, offset -> def printIssue = { issue, offset ->
println "${offset}${issue}" println "${offset}${issue}"
if (opts.v) { if (opts.v) {
println "" println ""
issue.text.eachLine { println "${offset} ${it}" } issue.text.eachLine { println "${offset} ${it}" }
println "" println "" } }
}
}
// local function (closure) to print a project and all visible subprojects
def printProject def printProject
printProject = { project, offset -> printProject = { project, offset ->
println "\n${offset}${project.name}" println "\n${offset}${project.name}"
println "${offset}${'-'.multiply(project.name.length())}" println "${offset}${'-'.multiply(project.name.length())}"
project.eachIssue(filter) { printIssue(it, offset) } project.eachIssue(filter) { printIssue(it, offset) }
project.eachProject(filter) { printProject(it, offset + " ") } project.eachProject(filter) { printProject(it, offset + " ") } }
}
// print all the issues in the root of this db
issuedb.eachIssue(filter) { printIssue(it, "") } issuedb.eachIssue(filter) { printIssue(it, "") }
issuedb.eachProject(filter) { printProject(it, "") } // print all projects
} issuedb.eachProject(filter) { printProject(it, "") } }
// change priority second // new issues second
else if (opts.P) {
def priority
try { priority = max(0, min(9, opts.P.toInteger())) }
catch (e) { println "Invalid priority: ${opts.P}"; return 1 }
walkProject(issuedb, filter) { it.priority = priority }
}
// change category third
else if (opts.C) {
def cat
try { cat = Category.toCategory(opts.C) }
catch (e) { println "Invalid category: ${opts.C}"; return 1 }
walkProject(issuedb, filter) { it.category = cat }
}
// change status fourth
else if (opts.T) {
def status
try { status = Status.toStatus(opts.T) }
catch (e) { println "Invalid status: ${opts.T}"; return 1 }
walkProject(issuedb, filter) { it.status = status }
}
// new entry last
else if (opts.n) { else if (opts.n) {
def cat, priority def cat, priority
String text = "" String text = ""
Issue issue Issue issue
def sin = System.in.newReader() def sin = System.in.newReader()
while(true) { if (opts.C) { cat = assignOpts.category }
try { else while(true) {
print "Category (bug, feature, task, closed): " try {
cat = Category.toCategory(sin.readLine()) print "Category (bug, feature, task, closed): "
break cat = Category.toCategory(sin.readLine())
} catch (e) { break }
println "Invalid category: " + e.getLocalizedMessage() catch (e) {
println "Valid options are: \n${Category.values().join(', ')}\n " + println "Invalid category: " + e.getLocalizedMessage()
"(abbreviations are accepted.)" println "Valid options are: \n${Category.values().join(', ')}"
} println " (abbreviations are accepted)." } }
}
while (true) { if (opts.P) { priority = assignOpts.priority }
else while (true) {
try { try {
print "Priority (0-9): " print "Priority (0-9): "
priority = max(0, min(9, sin.readLine().toInteger())) priority = max(0, min(9, sin.readLine().toInteger()))
break break }
} catch (e) { println "Not a valid value." } catch (e) { println "Not a valid value." } }
}
println "Enter issue (use EOF or ^D to end): " if (opts.getArgs().length > 0) { text = assignOpts.text }
try { else {
sin.eachLine { line -> println "Enter issue (use EOF): "
def m = line =~ /(.*)EOF.*/ try {
if (m) { def line = ""
text += m[0][1] + EOL while(true) {
sin.close() line = sin.readLine()
} else text += line + EOL
}
} catch (e) {}
if (line =~ /EOF/) break
text += line + EOL
} }
catch (e) {} }
issue = issuedb.createNewIssue(category: cat, priority: priority, text: text) issue = issuedb.createNewIssue(category: cat, priority: priority, text: text)
println "New issue created: " println "New issue created: "
println issue println issue }
// last, changes to existing issues
else {
// change priority
if (opts.P) walkProject(issuedb, filter) {
it.priority = assignOpts.priority
println "[${it}] -- set priority to ${assignOpts.priority}"}
// change third
else if (opts.C) walkProject(issuedb, filter) {
it.category = assignOpts.cat
println "[${it}] -- set category to ${assignOpts.category}"}
// change status
else if (opts.S) walkProject(issuedb, filter) {
it.status = assignOpts.status
println "[${it}] -- set status to ${assignOpts.status}"}
} }
// walk every issue and project in this project recursively and execute the
// given closure on each issue that meets the filter criteria
def walkProject(Project p, Filter filter, Closure c) { def walkProject(Project p, Filter filter, Closure c) {
p.eachIssue(filter, c) p.eachIssue(filter, c)
p.eachProject(filter) { walkProject(it, filter, c) } p.eachProject(filter) { walkProject(it, filter, c) }
} }
def printProject(Project project, String offset, Filter filter, boolean verbose = false) {
}

View File

@ -1 +1 @@
application.version=2.5.1 application.version=2.6.0