From d0e968b2b7568c0cd4fb87793623a41078a08ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Mon, 13 Feb 2012 12:12:26 -0600 Subject: [PATCH] New jlp-based documentation for pit-cli. --- .../pit/PersonalIssueTrackerCLI.groovy | 281 +++++++++++++----- 1 file changed, 207 insertions(+), 74 deletions(-) diff --git a/pit-cli/src/main/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy b/pit-cli/src/main/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy index dcf93d7..d0b4d42 100644 --- a/pit-cli/src/main/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy +++ b/pit-cli/src/main/com/jdbernard/pit/PersonalIssueTrackerCLI.groovy @@ -1,3 +1,10 @@ +/** + * # Personal Issue Tracker Command Line Interface + * @author Jonathan Bernard + * @copyright 2009-2012 Jonathan Bernard + * + * This is a command-line interface to my personal issue tracker system. + */ package com.jdbernard.pit import com.jdbernard.pit.file.* @@ -11,47 +18,127 @@ import static java.lang.Math.min def log = LoggerFactory.getLogger(getClass()) -// -------- command-line interface specification -------- // - +/// ## Command Line Options ## +/// -------------------------- def cli = new CliBuilder(usage: 'pit-cli [options]') + +/// -h,--help +/// : Show help information cli.h(longOpt: 'help', 'Show help information.') + +/// -v,--verbose +/// : Show verbose task information. cli.v(longOpt: 'verbose', 'Show verbose task information') + +/// -l,--list +/// : List issues in the current project. cli.l(longOpt: 'list', 'List issues in the current project.') + +/// -i,--id +/// : Filter issues by id. Accepts a comma-delimited list. +/// *Example:* `pit -l -i 0001,0002` cli.i(argName: 'id', longOpt: 'id', args: 1, 'Filter issues by id. Accepts a comma-delimited list.') + +/// -c,--category +/// : Filter issues by category (bug, feature, task). Accepts a +/// comma-delimited list. By default all categories are selected. The full +/// category name is not required, just enough to be uniquely identifiable. +/// *Example:* `pit -l -c bug,t # List bugs and tasks.` cli.c(argName: 'category', longOpt: 'category', args: 1, 'Filter issues by category (bug, feature, task). Accepts a ' + 'comma-delimited list. By default all categories are selected.') + +/// -s,--status +/// : Filter issues by status (new, reassigned, rejected, resolved, +/// validation_required). The full status is not required, just enough to +/// uniquely identify the status. +/// *Example:* `pit -l -s reas,rej # List Reassigned and Rejected issues.` cli.s(argName: 'status', longOpt: 'status', args: 1, 'Filter issues by status (new, reassigned, rejected, resolved, ' + 'validation_required)') + +/// -p,--priority +/// : Filter issues by priority. This acts as a threshhold, listing all +/// issues greater than or equal to the given priority. +/// *Example:* `pit -l -p 5 # List all issues with priority >= 5` cli.p(argName: 'priority', longOpt: 'priority', args: 1, 'Filter issues by priority. This acts as a threshhold, listing all issues ' + 'greater than or equal to the given priority.') + +/// -r,--project +/// : Filter issues by project (relative to the current directory). Accepts a +/// comma-delimited list. This option should be used in conjunction with the +/// `R,--recursive` option. +/// *Example:* `pit -l -R --project ` cli.r(argName: 'project', longOpt: 'project', args: 1, 'Filter issues by project (relative to the current directory). Accepts a ' + 'comma-delimited list.') + +/// -R,--recursive +/// : Recursively include subprojects. cli.R(longOpt: 'recursive', 'Include subprojects.') + +/// -e,--extended-property +/// : Filter for issues by extended property. Format is +/// `-e =`. cli.e(argName: 'extended-property', args: 1, 'Filter for issues by extended ' + 'property. Format is "-e =".') + /*cli.s(longOpt: 'show-subprojects', 'Include sup projects in listing (default behaviour)') cli.S(longOpt: 'no-subprojects', 'Do not list subprojects.')*/ // TODO: figure out better flags for these options. + +/// -P,--set-priority +/// : Modify the priority of the selected issues. Requires a value from 0-9. cli.P(argName: 'new-priority', longOpt: 'set-priority', args: 1, 'Modify the priority of the selected issues.') + +/// -C,--set-category +/// : Modify the category of the selected issues. cli.C(argName: 'new-category', longOpt: 'set-category', args: 1, 'Modify the category of the selected issues.') + +/// -S,--set-status +/// : Modify the status of the selected issues. cli.S(argName: 'new-status', longOpt: 'set-status', args: 1, 'Modify the status of the selected issues.') -cli.E(argName: 'new-extended-property', args: 1, 'Modify the extended ' + - 'property of the selected issues. Format is "-E ="') + +/// -E,--new-issue +/// : Modify the extended property of the selected issues. Format is +/// `-E =` +cli.E(argName: 'new-extended-property', longOpt: 'set-extended-property', + args: 1, 'Modify the extended property of the selected issues. Format ' + + 'is "-E ="') + +/// -n,--new-issue +/// : Create a new issue cli.n(longOpt: 'new-issue', 'Create a new issue.') + +/// --title +/// : Give the title for a new issue or modify the title for an existing +/// issue. By default the title for a new issue is expected on stanard +/// input. cli._(longOpt: 'title', args: 1, argName: 'title', 'Give the title for a new' + ' issue or modify the title for an existing issue. By default the title' + ' for a new issue is expected on stanard input.') + +/// --text +/// : Give the text for a new issue or modify the text for an exising +/// issue. By default the text for a new issue is expected on standard +/// input. cli._(longOpt: 'text', args: 1, argName: 'text', 'Give the text for a new' + ' issue or modify the text for an exising issue. By default the text for' + ' a new issue is expected on standard input.') + +/** -o,--order + * : Order (sort) the results by the given properties. Provide a + * comma-seperated list of property names to sort by in order of + * importance. The basic properties (id, category, status, and priority) + * can be given using their one-letter forms (i,c,s,p) for brevity. For + * example: `-o Due,p,c` would sort first by the extended property `Due`, + * then for items that have the same `Due` value it would sort by + * priority, then by category. */ cli.o(longOpt: 'order', argName: 'order', args: 1, required: false, 'Order (sort) the results by the given properties. Provide a comma-' + 'seperated list of property names to sort by in order of importance. The' + @@ -60,31 +147,64 @@ cli.o(longOpt: 'order', argName: 'order', args: 1, required: false, ' "-o Due,p,c" would sort first by the extended property "Due", then for' + ' items that have the same "Due" value it would sort by priority, then' + ' by category.') + +/// -d,--dir +/// : Use `` as the base directory (defaults to current directory). cli.d(longOpt: 'dir', argName: 'dir', args: 1, required: false, 'Use as the base directory (defaults to current directory).') + +/// -D,--daily-list +/// : Print a Daily Task list based on issue Due, Scheduled, and Reminder +/// extended properties. cli.D(longOpt: 'daily-list', 'Print a Daily Task list based on issue Due and' + ' Reminder properties.') + +/// --dl-scheduled +/// : Show scheduled tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-scheduled', 'Show scheduled tasks in the daily list (all' + ' are shown by default).') + +/// --dl-due +/// : Show due tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-due', 'Show due tasks in the daily list (all are shown by' + ' default).') + +/// --dl-reminder +/// : Show upcoming tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-reminder', 'Show upcoming tasks in the daily list (all ' + ' are shown by default).') + +/// --dl-open +/// : Show open tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-open', 'Show open tasks in the daily list (all are shown ' + ' by default).') + +/// --dl-hide-scheduled +/// : Hide scheduled tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-hide-scheduled', 'Hide scheduled tasks in the daily list' + ' (all are shown by default).') + +/// --dl-hide-due +/// : Show due tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-hide-due', 'Show due tasks in the daily list (all are' + ' shown by default).') + +/// --dl-hide-reminder +/// : Show upcoming tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-hide-reminder', 'Show upcoming tasks in the daily list' + ' (all are shown by default).') + +/// --dl-hide-open +/// : Show open tasks in the daily list (all are shown by default). cli._(longOpt: 'dl-hide-open', 'Show open tasks in the daily list (all are' + ' shown by default).') + +/// --version +/// : Display PIT version information. cli._(longOpt: 'version', 'Display PIT version information.') -// =================================== // -// ======== Parse CLI Options ======== // -// =================================== // +/// ## Parse CLI Options ## +/// ----------------------- log.trace("Parsing options.") @@ -93,7 +213,7 @@ def opts = cli.parse(args) def issuedb = [:] def workingDir = new File('.') -// defaults for the issue filter/selector +/// Defaults for the issue filter/selector. def selectOpts = [ categories: ['bug', 'feature', 'task'], status: ['new', 'reassigned', 'rejected', @@ -104,19 +224,19 @@ def selectOpts = [ extendedProperties: [:], acceptProjects: true] -// options for changing properties of issue(s) +/// Defaults for changing properties of issue(s) def assignOpts = [:] if (!opts || opts.h) { cli.usage() System.exit(0) } -// read the category filter designation(s) +///Read the `-c` option: 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 +/// Parse the categories names into Category objects. try { selectOpts.categories = selectOpts.categories.collect { Category.toCategory(it) } } catch (Exception e) { @@ -125,15 +245,15 @@ catch (Exception e) { println " (abbreviations are accepted)." System.exit(1) } -// read the status filter designation(s) +/// Read the `-s` option: status filter designation(s). if (opts.s) { // -s all if (opts.s =~ /all/) selectOpts.status = ['new', 'reassigned', 'rejected', 'resolved', 'validation_required'] - // is + // else selectOpts.status = opts.s.split(/[,\s]/) } -// parse the statuses into Status objects +/// Parse the statuses into Status objects. try { selectOpts.status = selectOpts.status.collect { Status.toStatus(it) } } catch (Exception e) { @@ -142,7 +262,7 @@ catch (Exception e) { println " (abbreviations are accepted.)" System.exit(1) } -// read and parse the priority filter +/// Read and parse the `-p` option: priority filter. if (opts.p) try { selectOpts.priority = opts.p.toInteger() } catch (NumberFormatException nfe) { @@ -150,14 +270,14 @@ catch (NumberFormatException nfe) { println "Valid values are: 0-9" System.exit(1) } -// read and parse the projects filter +/// Read and parse the `-r` option: projects filter. if (opts.r) { selectOpts.projects = opts.r.toLowerCase().split(/[,\s]/).asType(List.class) } -// read and parse the ids filter +/// Read and parse the `-i` option: id filter. if (opts.i) { selectOpts.ids = opts.i.split(/[,\s]/).asType(List.class) } -// read and parse sort criteria +/// Read and parse the `-o` option: sort criteria. if (opts.o) { def sortProps = opts.o.split(',') selectOpts.issueSorter = sortProps.collect { prop -> @@ -168,16 +288,14 @@ if (opts.o) { case ~/^c$/: return { issue -> issue.category } default: return { issue -> issue[prop] } }}} -// read and parse extended property selection criteria +/// Read and parse any extended property selection criteria. if (opts.e) { opts.es.each { option -> def parts = option.split("=") selectOpts.extendedProperties[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }} -// TODO: accept projects value from input - -// read and parse the category to assign +/// Read and parse the `-C` option: category to assign. if (opts.C) try { assignOpts.category = Category.toCategory(opts.C) } catch (Exception e) { println "Invalid category option: '-C ${e.localizedMessage}'." @@ -185,7 +303,7 @@ catch (Exception e) { println " (abbreviations are accepted)." System.exit(1) } -// read and parse the status to assign +/// Read and parse the `-S` option: status to assign. if (opts.S) try { assignOpts.status = Status.toStatus(opts.S) } catch (Exception e) { println "Invalid status option: '-S ${e.localizedMessage}'." @@ -193,25 +311,26 @@ catch (Exception e) { println " (abbreviations are accepted)." System.exit(1) } -// read and parse the priority to assign +/// Read and parse the `-P` option: 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) } +/// Read an parse any extended properties to be set. if (opts.E) { opts.Es.each { option -> def parts = option.split("=") assignOpts[parts[0]] = ExtendedPropertyHelp.parse(parts[1]) }} -// Read the title if given. +/// Read the title if given. if (opts.title) { assignOpts.title = opts.title } -// Read the text if given +/// Read the text if given. if (opts.text) { assignOpts.text = opts.text } -// set the project working directory +/// Set the project working directory. if (opts.d) { workingDir = new File(opts.d.trim()) if (!workingDir.exists()) { @@ -223,32 +342,33 @@ def EOL = System.getProperty('line.separator') log.debug("Finished parsing options:\nworkingDir: {}\nselectOpts: {}\nassignOpts: {}", workingDir.canonicalPath, selectOpts, assignOpts) -// ========================= // -// ======== Actions ======== // -// ========================= // +/// ## Actions ## +/// ------------- -// list version information first +/// ### Version information. if (opts.version) { println "PIT CLI Version ${VERSION}" println "Written by Jonathan Bernard\n" } + +/// ---- else { -// build issue list +/// Build issue list. log.trace("Building issue database.") issuedb = new FileProject(workingDir) -// build filter from options +/// Build filter from options. log.trace("Defining the filter.") def filter = new Filter(selectOpts) -// list second +/// ### List if (opts.l) { log.trace("Listing issues.") - // local function (closure) to print a single issue + /// Local function (closure) to print a single issue. def printIssue = { issue, offset -> println "${offset}${issue}" if (opts.v) { @@ -260,7 +380,7 @@ if (opts.l) { println "${offset} * ${name}: ${formattedValue}"} println ""}} - // local function (closure) to print a project and all visible subprojects + /// Local function (closure) to print a project and all visible subprojects. def printProject printProject = { project, offset -> println "\n${offset}${project.name}" @@ -268,65 +388,70 @@ if (opts.l) { project.eachIssue(filter) { printIssue(it, offset) } project.eachProject(filter) { printProject(it, offset + " ") } } - // print all the issues in the root of this db + /// Print all the issues in the root of this db. issuedb.eachIssue(filter) { printIssue(it, "") } + /// If the user set the recursive flag print all projects. if (opts.R) { - // print all projects issuedb.eachProject(filter) { printProject(it, "") }} } -// daily list second +/// ### Daily List else if (opts.D) { log.trace("Showing a daily list.") - // Parse daily list specific display options + /// #### Parse daily list specific display options. def visibleSections = [] def suppressedSections - // Parse the additive options first. + /// Parse the additive options first. if (opts.'dl-scheduled') { visibleSections << 'scheduled' } if (opts.'dl-due') { visibleSections << 'due' } if (opts.'dl-reminder') { visibleSections << 'reminder' } if (opts.'dl-open') { visibleSections << 'open' } - // If the user did not add any sections assume they want them all. + /// If the user did not add any sections assume they want them all. if (visibleSections.size() == 0) { visibleSections = ['scheduled', 'due', 'reminder', 'open'] } - // Now go through the negative options. + /// Now go through the negative options. if (opts.'dl-hide-scheduled') { visibleSections -= 'scheduled' } if (opts.'dl-hide-due') { visibleSections -= 'due' } if (opts.'dl-hide-reminder') { visibleSections -= 'reminder' } if (opts.'dl-hide-open') { visibleSections -= 'open' } - // If the user did not specifically ask for a status filter, we want a - // different filter for the default when we are doing a daily list. + /// If the user did not specifically ask for a status filter, we want a + /// different default filter when we are doing a daily list. if (!opts.s) { filter.status = [Status.NEW, Status.VALIDATION_REQUIRED] } - // If the user did not give a specific sorting order, define our own. + /// If the user did not give a specific sorting order, define our own: due + /// date, then priority, then id. if (!opts.o) { filter.issueSorter = [ {it.due}, {it.priority}, {it.id} ] } - // Get our issues + /// #### Get all the issues involved. def allIssues = opts.R ? - // If -R passed, get all issues, including subprojects. + /// If `-R` passed, get all issues, including subprojects. issuedb.getAllIssues(filter) : - // Otherwise, just use the issues for this project. + /// Otherwise, just use the issues for this project. issuedb.issues.values().findAll { filter ? filter.accept(it) : true } - // Set up our time interval. + /// Set up our time intervals. def today = new DateMidnight() def tomorrow = today.plusDays(1) + /// We are going to sort the issues into these buckets based on when they are + /// scheduled, when they are duem and if they have a reminder set. def scheduledToday = [] def dueToday = [] def reminderToday = [] def notDueOrReminder = [] + /// Helper closure to print an issue. def printIssue = { issue -> if (issue.due) println "${issue.due.toString('EEE, MM/dd')} -- ${issue}" else println " -- ${issue}" } + /// A sorter which sorts by date first, then by priority. def priorityDateSorter = { i1, i2 -> if (i1.priority == i2.priority) { def d1 = i1.due ?: new DateTime() @@ -335,25 +460,26 @@ else if (opts.D) { return d1.compareTo(d2) } else { return i1.priority - i2.priority }} - // Sort the issues into seperate lists based on their due dates and - // reminders. + /// #### Categorize and sort the issues. + /// Sort the issues into seperate lists based on their due dates and + /// reminders. allIssues.each { issue -> - // Find the issues that are scheduled for today. + /// * Find the issues that are scheduled for today. if (issue.scheduled && issue.scheduled < tomorrow) { scheduledToday << issue } - // Find the issues that are due today or are past due. + /// * Find the issues that are due today or are past due. else if (issue.due && issue.due < tomorrow) { dueToday << issue } - // Find the issues that are not yet due but have a reminder for today or - // days past. + /// * Find the issues that are not yet due but have a reminder for today or + /// days past. else if (issue.reminder && issue.reminder < tomorrow) { reminderToday << issue } - // All the others (not due and no reminder). + /// * All the others (not due and no reminder). else notDueOrReminder << issue } - // Print the issues + /// #### Print the issues if (visibleSections.contains('scheduled') && scheduledToday.size() > 0) { println "Tasks Scheduled for Today" println "-------------------------" @@ -386,7 +512,7 @@ else if (opts.D) { println "" }} -// new issues fourth +/// ### Create a New Issue. else if (opts.n) { log.trace("Creating a new issue.") @@ -394,17 +520,17 @@ else if (opts.n) { Issue issue def sin = System.in.newReader() - // Set the created extended property + /// Set the created extended property. assignOpts.created = new DateTime() - // Prompt for the different options if they were not given on the command - // line. We will loop until they have entered a valid value. How it works: - // In the body of the loop we will try to read the input, parse it and - // assign it to a variable. If the input is invalid it will throw as - // exception before the assignment happens, the variable will still be - // null, and we will prompt the user again. + /// Prompt for the different options if they were not given on the command + /// line. We will loop until they have entered a valid value. How it works: + /// In the body of the loop we will try to read the input, parse it and + /// assign it to a variable. If the input is invalid it will throw an + /// exception before the assignment happens, the variable will still be + /// null, and we will prompt the user again. - // Prompt for category. + /// Prompt for category. while(!assignOpts.category) { try { print "Category (bug, feature, task): " @@ -415,7 +541,7 @@ else if (opts.n) { println "Valid options are: \n${Category.values().join(', ')}" println " (abbreviations are accepted)." } } - // Prompt for the priority. + /// Prompt for the priority. while (!assignOpts.priority) { try { print "Priority (0-9): " @@ -423,13 +549,13 @@ else if (opts.n) { break } catch (e) { println "Not a valid value." } } - // Prompt for the issue title. No need to loop as the input does not need - // to be validated. + /// Prompt for the issue title. No need to loop as the input does not need + /// to be validated. if (!assignOpts.title) { println "Issue title: " assignOpts.title = sin.readLine().trim() } - // Prompt for the issue text. + /// Prompt for the issue text. if (!assignOpts.text) { assignOpts.text = "" println "Enter issue text (use EOF to stop): " @@ -444,35 +570,42 @@ else if (opts.n) { assignOpts.text += line + EOL } } catch (e) {} } + /// Create the issue. issue = issuedb.createNewIssue(assignOpts) println "New issue created: " println issue } -// last, changes to existing issues +/// ### Change Existing Issues. else if (assignOpts.size() > 0) { log.trace("Changing existing issues.") - // We are going to add some extra properties if the status is being changed, - // because we are nice like that. + /// We are going to add some extra properties if the status is being changed, + /// because we are nice like that. if (assignOpts.status) { switch (assignOpts.status) { case Status.RESOLVED: assignOpts.resolved = new DateTime(); break case Status.REJECTED: assignOpts.rejected = new DateTime(); break default: break }} + /// #### processIssue + /// A local function to handle the changes for one issue. def processIssue = { issue -> println issue + /// Walk the assigned options map and set the properties on the issue. assignOpts.each { propName, value -> issue[propName] = value def formattedValue = ExtendedPropertyHelp.format(value) println " set ${propName} to ${formattedValue}" } } + /// If the user passed `-R`, walk the whole project, including subprojects. if (opts.R) { issuedb.walkProject(filter, processIssue) } + /// Otherwise, just process the issues in this project. else { issuedb.issues.values() .findAll { filter ? filter.accept(it) : true } .each(processIssue) }} +/// ### Invalid Input else { log.trace("Unknown request.") cli.usage(); return -1 }}