X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Fbin%2Frt.in;h=09b52ae34238185fba7710642e551b62ba52d963;hb=990e040448691dd4bd26c871274c65204cb7707b;hp=b44e04158f6030a9c747e7c1a8d0bc029e263e7f;hpb=c582e92888b4a5553e1b4e5214cf35217e4a0cf0;p=freeside.git diff --git a/rt/bin/rt.in b/rt/bin/rt.in index b44e04158..09b52ae34 100644 --- a/rt/bin/rt.in +++ b/rt/bin/rt.in @@ -1,9 +1,15 @@ #!@PERL@ -w -# BEGIN LICENSE BLOCK +# BEGIN BPS TAGGED BLOCK {{{ # -# Copyright (c) 1996-2003 Jesse Vincent +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC +# # -# (Except where explictly superceded by other copyright notices) +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have @@ -15,13 +21,33 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # -# Unless otherwise specified, all modifications, corrections or -# extensions to this work which alter its source code become the -# property of Best Practical Solutions, LLC when submitted for -# inclusion in the work. +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/copyleft/gpl.html. +# # +# CONTRIBUTION SUBMISSION POLICY: # -# END LICENSE BLOCK +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} +# Designed and implemented for Best Practical Solutions, LLC by +# Abhijit Menon-Sen use strict; @@ -30,7 +56,9 @@ use strict; use Cwd; use LWP; +use Text::ParseWords; use HTTP::Request::Common; +use Term::ReadLine; # We derive configuration information from hardwired defaults, dotfiles, # and the RT* environment variables (in increasing order of precedence). @@ -45,7 +73,7 @@ my %config = ( debug => 0, user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME}, passwd => undef, - server => 'http://localhost/rt/', + server => 'http://localhost/', query => undef, orderby => undef, ), @@ -55,6 +83,8 @@ my %config = ( my $session = new Session("$HOME/.rt_sessions"); my $REST = "$config{server}/REST/1.0"; +my $prompt = 'rt> '; + sub whine; sub DEBUG { warn @_ if $config{debug} >= shift } @@ -62,7 +92,7 @@ sub DEBUG { warn @_ if $config{debug} >= shift } # (XXX: Ask Autrijus how i18n changes these definitions.) my $name = '[\w.-]+'; -my $field = '[a-zA-Z][a-zA-Z0-9_-]*'; +my $field = '(?:[a-zA-Z](?:[a-zA-Z0-9_-]|\s+)*)'; my $label = '[a-zA-Z0-9@_.+-]+'; my $labels = "(?:$label,)*$label"; my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; @@ -77,6 +107,7 @@ my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; my %handlers = ( # handler => [ ...aliases... ], version => ["version", "ver"], + shell => ["shell"], logout => ["logout"], help => ["help", "man"], show => ["show", "cat"], @@ -86,20 +117,35 @@ my %handlers = ( link => ["link", "ln"], merge => ["merge"], grant => ["grant", "revoke"], + take => ["take", "steal", "untake"], + quit => ["quit", "exit"], ); -# Once we find and call an appropriate handler, we're done. - -my (%actions, $action); +my %actions; foreach my $fn (keys %handlers) { foreach my $alias (@{ $handlers{$fn} }) { $actions{$alias} = \&{"$fn"}; } } -if (@ARGV && exists $actions{$ARGV[0]}) { - $action = shift @ARGV; + +# Once we find and call an appropriate handler, we're done. + +sub handler { + my $action; + + push @ARGV, 'shell' if (!@ARGV); # default to shell mode + shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt' + if (@ARGV && exists $actions{$ARGV[0]}) { + $action = shift @ARGV; + $actions{$action}->($action); + } + else { + print STDERR "rt: Unknown command '@ARGV'.\n"; + print STDERR "rt: For help, run 'rt help'.\n"; + } } -$actions{$action || "help"}->($action || ()); + +handler(); exit; # Handler functions. @@ -107,6 +153,17 @@ exit; # # The following subs are handlers for each entry in %actions. +sub shell { + $|=1; + my $term = new Term::ReadLine 'RT CLI'; + while ( defined ($_ = $term->readline($prompt)) ) { + next if /^#/ || /^\s*$/; + + @ARGV = shellwords($_); + handler(); + } +} + sub version { print "rt $VERSION\n"; } @@ -115,19 +172,27 @@ sub logout { submit("$REST/logout") if defined $session->cookie; } +sub quit { + logout(); + exit; +} + +my %help; sub help { my ($action, $type) = @_; - my (%help, $key); + my $key; # What help topics do we know about? - local $/ = undef; - foreach my $item (@{ Form::parse() }) { - my $title = $item->[2]{Title}; - my @titles = ref $title eq 'ARRAY' ? @$title : $title; + if (!%help) { + local $/ = undef; + foreach my $item (@{ Form::parse() }) { + my $title = $item->[2]{Title}; + my @titles = ref $title eq 'ARRAY' ? @$title : $title; foreach $title (grep $_, @titles) { $help{$title} = $item->[2]{Text}; } + } } # What does the user want help with? @@ -168,7 +233,9 @@ sub help { # Displays a list of objects that match some specified condition. sub list { - my ($q, $type, %data, $orderby); + my ($q, $type, %data); + my $orderby = $config{orderby}; + if ($config{orderby}) { $data{orderby} = $config{orderby}; } @@ -205,7 +272,6 @@ sub list { $bad = 1; last; } } - if (!defined $q) { $q = $config{query}; } @@ -216,7 +282,8 @@ sub list { whine "No $item specified."; $bad = 1; } - return help("list", $type) if $bad; + #return help("list", $type) if $bad; + return suggest_help("list", $type) if $bad; my $r = submit("$REST/search/$type", { query => $q, %data }); print $r->content; @@ -273,10 +340,18 @@ sub show { whine "No objects specified."; $bad = 1; } - return help("show", $type) if $bad; + #return help("show", $type) if $bad; + return suggest_help("show", $type) if $bad; my $r = submit("$REST/show", { id => \@objects, %data }); - print $r->content; + my $c = $r->content; + # if this isn't a text reply, remove the trailing newline so we + # don't corrupt things like tarballs when people do + # show ticket/id/attachments/id/content > foo.tar.gz + if ($r->content_type !~ /^text\//) { + chomp($c); + } + print $c; } # To create a new object, we ask the server for a form with the defaults @@ -380,18 +455,23 @@ sub edit { } @objects = ("$type/new"); } - return help($action, $type) if $bad; + #return help($action, $type) if $bad; + return suggest_help($action, $type) if $bad; # We need a form to make changes to. We usually ask the server for # one, but we can avoid that if we are fed one on STDIN, or if the # user doesn't want to edit the form by hand, and the command line - # specifies only simple variable assignments. + # specifies only simple variable assignments. We *should* get a + # form if we're creating a new ticket, so that the default values + # get filled in properly. + + my @new_objects = grep /\/new$/, @objects; if ($input) { local $/ = undef; $text = ; } - elsif ($edit || %add || %del || !$cl) { + elsif ($edit || %add || %del || !$cl || @new_objects) { my $r = submit("$REST/show", { id => \@objects, format => 'l' }); $text = $r->content; } @@ -460,7 +540,7 @@ sub edit { if ($output) { print $text; - exit; + return; } my $synerr = 0; @@ -486,7 +566,7 @@ EDIT: } else { print $r->content; - exit -1; + return; } } print $r->content; @@ -515,7 +595,7 @@ sub comment { if (/-a/) { unless (-f $ARGV[0] && -r $ARGV[0]) { whine "Cannot read attachment: '$ARGV[0]'."; - exit -1; + return; } push @files, shift @ARGV; } @@ -547,7 +627,8 @@ sub comment { whine "No object specified."; $bad = 1; } - return help($action, "ticket") if $bad; + #return help($action, "ticket") if $bad; + return suggest_help($action, "ticket") if $bad; my $form = [ "", @@ -560,6 +641,7 @@ sub comment { Attachment => [ @files ], TimeWorked => $wtime || '', Text => $msg || '', + Status => '' } ]; @@ -571,7 +653,7 @@ sub comment { do { my $ntext = vi($text); - exit if ($error && $ntext eq $text); + return if ($error && $ntext eq $text); $text = $ntext; $form = Form::parse($text); $error = 0; @@ -583,7 +665,7 @@ sub comment { goto NEXT; } elsif (!@$o) { - exit; + return; } @files = @{ vsplit($k->{Attachment}) }; @@ -599,7 +681,7 @@ sub comment { } $data{content} = $text; - my $r = submit("$REST/ticket/comment/$id", \%data); + my $r = submit("$REST/ticket/$id/comment", \%data); print $r->content; } @@ -626,9 +708,10 @@ sub merge { whine "Too $evil arguments specified."; $bad = 1; } - return help("merge", "ticket") if $bad; + #return help("merge", "ticket") if $bad; + return suggest_help("merge", "ticket") if $bad; - my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]}); + my $r = submit("$REST/ticket/$id[0]/merge/$id[1]"); print $r->content; } @@ -659,7 +742,7 @@ sub link { $bad = 1; } unless (exists $ltypes{lc $rel}) { - whine "Invalid relationship '$rel' specified."; + whine "Invalid link '$rel' specified."; $bad = 1; } %data = (id => $from, rel => $rel, to => $to, del => $del); @@ -669,12 +752,51 @@ sub link { whine "Too $bad arguments specified."; $bad = 1; } - return help("link", "ticket") if $bad; + #return help("link", "ticket") if $bad; + return suggest_help("link", "ticket") if $bad; my $r = submit("$REST/ticket/link", \%data); print $r->content; } +# Take/steal a ticket +sub take { + my ($cmd) = @_; + my ($bad, %data) = (0, ()); + + my $id; + + # get the ticket id + if (@ARGV == 1) { + ($id) = @ARGV; + unless ($id =~ /^\d+$/) { + whine "Invalid ticket ID $id specified."; + $bad = 1; + } + my $form = [ + "", + [ "Ticket", "Action" ], + { + Ticket => $id, + Action => $cmd, + Status => '', + } + ]; + + my $text = Form::compose([ $form ]); + $data{content} = $text; + } + else { + $bad = @ARGV < 1 ? "few" : "many"; + whine "Too $bad arguments specified."; + $bad = 1; + } + return suggest_help("take", "ticket") if $bad; + + my $r = submit("$REST/ticket/$id/take", \%data); + print $r->content; +} + # Grant/revoke a user's rights. sub grant { @@ -786,7 +908,7 @@ sub submit { # For anything else, we just die. elsif ($res->code != 409) { warn "rt: ", $res->content; - exit; + #exit; } } } @@ -1122,6 +1244,7 @@ sub config_from_file { sub parse_config_file { my %cfg; my ($file) = @_; + local $_; # $_ may be aliased to a constant, from line 1163 open(CFG, $file) && do { while () { @@ -1205,7 +1328,7 @@ sub vsplit { my @values = ref $val eq 'ARRAY' ? @$val : $val; foreach my $line (map {split /\n/} @values) { - # XXX: This should become a real parser, à la Text::ParseWords. + # XXX: This should become a real parser, à la Text::ParseWords. $line =~ s/^\s+//; $line =~ s/\s+$//; push @words, split /\s*,\s*/, $line; @@ -1214,17 +1337,29 @@ sub vsplit { return \@words; } +# WARN: this code is duplicated in lib/RT/Interface/REST.pm +# change both functions at once sub expand_list { my ($list) = @_; - my ($elt, @elts, %elts); - foreach $elt (split /,/, $list) { - if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) } - else { push @elts, $elt } + my @elts; + foreach (split /,/, $list) { + push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_; } - @elts{@elts}=(); - return sort {$a<=>$b} keys %elts; + return map $_->[0], # schwartzian transform + sort { + defined $a->[1] && defined $b->[1]? + # both numbers + $a->[1] <=> $b->[1] + :!defined $a->[1] && !defined $b->[1]? + # both letters + $a->[2] cmp $b->[2] + # mix, number must be first + :defined $a->[1]? -1: 1 + } + map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ], + @elts; } sub get_type_argument { @@ -1274,16 +1409,23 @@ sub is_object_spec { return; } +sub suggest_help { + my ($action, $type) = @_; + + print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action; + print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type; +} + __DATA__ Title: intro Title: introduction Text: - ** THIS IS AN UNSUPPORTED PREVIEW RELEASE ** - ** PLEASE REPORT BUGS TO rt-bugs@fsck.com ** + ** THIS IS AN UNSUPPORTED PREVIEW RELEASE ** + ** PLEASE REPORT BUGS TO rt-bugs@bestpractical.com ** - This is a command-line interface to RT 3. + This is a command-line interface to RT 3.0 or newer It allows you to interact with an RT server over HTTP, and offers an interface to RT's functionality that is better-suited to automation @@ -1295,9 +1437,10 @@ Text: For more information: - - rt help actions (a list of possible actions) - - rt help objects (how to specify objects) - rt help usage (syntax information) + - rt help objects (how to specify objects) + - rt help actions (a list of possible actions) + - rt help types (a list of object types) - rt help config (configuration details) - rt help examples (a few useful examples) @@ -1312,6 +1455,8 @@ Text: Syntax: rt [options] [arguments] + or + rt shell Each invocation of this program must specify an action (e.g. "edit", "create"), options to modify behaviour, and other arguments required @@ -1322,6 +1467,10 @@ Text: "rt help ". Some actions may be referred to by more than one name ("create" is the same as "new", for example). + You may also call "rt shell", which will give you an 'rt>' prompt at + which you can issue commands of the form " [options] + [arguments]". See "rt help shell" for details. + Objects are identified by a type and an ID (which can be a name or a number, depending on the type). For some actions, the object type is implied (you can only comment on tickets); for others, the user must @@ -1336,6 +1485,7 @@ Text: - rt help objects (how to specify objects) - rt help actions (a list of actions) - rt help types (a list of object types) + - rt help shell (how to use the shell) -- @@ -1619,13 +1769,15 @@ Text: -t type Specifies object type. -f a,b,c Restrict the display to the specified fields. -S var=val Submits the specified variable with the request. - + -v Verbose display Examples: rt show -t ticket -f id,subject,status 1-3 rt show ticket/3/attachments/29 rt show ticket/3/attachments/29/content rt show ticket/1-3/links + rt show ticket/3/history + rt show -v ticket/3/history rt show -t user 2 -- @@ -1686,7 +1838,7 @@ Text: rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved rt edit ticket/4 set priority=3 owner=bar@example.com \ add cc=foo@example.com bcc=quux@example.net - rt create -t ticket subject='new ticket' priority=10 \ + rt create -t ticket set subject='new ticket' priority=10 \ add cc=foo@example.com -- @@ -1721,7 +1873,7 @@ Text: Examples: - rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23 + rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23 -- @@ -1732,7 +1884,7 @@ Text: rt merge - Merges the two specified tickets. + Merges the first ticket specified into the second ticket specified. -- @@ -1741,16 +1893,16 @@ Text: Syntax: - rt link [-d] + rt link [-d] Creates (or, with -d, deletes) a link between the specified tickets. - The relationship can (irrespective of case) be any of: + The link can (irrespective of case) be any of: DependsOn/DependedOnBy: A depends upon B (or vice versa). RefersTo/ReferredToBy: A refers to B (or vice versa). MemberOf/HasMember: A is a member of B (or vice versa). - To view a ticket's relationships, use "rt show ticket/3/links". (See + To view a ticket's links, use "rt show ticket/3/links". (See "rt help ticket" and "rt help show".) Options: @@ -1807,7 +1959,11 @@ Text: Title: topics Text: - Use "rt help " for help on any of the following subjects: + Syntax: + + rt help + + Get help on any of the following subjects: - tickets, users, groups, queues. - show, edit, ls/list/search, new/create. @@ -1834,3 +1990,71 @@ Text: For the moment, please consult examples provided with each action. -- + +Title: shell +Text: + + Syntax: + + rt shell + + Opens an interactive shell, at which you can issue commands of + the form " [options] [arguments]". + + To exit the shell, type "quit" or "exit". + + Commands can be given at the shell in the same form as they would + be given at the command line without the leading 'rt' invocation. + + Example: + $ rt shell + rt> create -t ticket set subject='new' add cc=foo@example.com + # Ticket 8 created. + rt> quit + $ + +-- + +Title: take +Title: untake +Title: steal +Text: + + Syntax: + + rt + + Sets the owner of the specified ticket to the current user, + assuming said user has the bits to do so, or releases the + ticket. + + 'Take' is used on tickets which are not currently owned + (Owner: Nobody), 'steal' is used on tickets which *are* + currently owned, and 'untake' is used to "release" a ticket + (reset its Owner to Nobody). 'Take' cannot be used on + tickets which are currently owned. + + Example: + alice$ rt create -t ticket set subject="New ticket" + # Ticket 7 created. + alice$ rt take 7 + # Owner changed from Nobody to alice + alice$ su bob + bob$ rt steal 7 + # Owner changed from alice to bob + bob$ rt untake 7 + # Owner changed from bob to Nobody + +-- + +Title: quit +Title: exit +Text: + + Use "quit" or "exit" to leave the shell. Only valid within shell + mode. + + Example: + $ rt shell + rt> quit + $