import rt 3.6.4
[freeside.git] / rt / bin / rt.in
index d12460b..b75e9e7 100644 (file)
@@ -3,7 +3,7 @@
 # 
 # COPYRIGHT:
 #  
 # 
 # COPYRIGHT:
 #  
-# This software is Copyright (c) 1996-2005 Best Practical Solutions, LLC 
+# This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
 #                                          <jesse@bestpractical.com>
 # 
 # (Except where explicitly superseded by other copyright notices)
 #                                          <jesse@bestpractical.com>
 # 
 # (Except where explicitly superseded by other copyright notices)
@@ -23,7 +23,9 @@
 # 
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # 
 # 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+# 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:
 # 
 # 
 # CONTRIBUTION SUBMISSION POLICY:
@@ -44,7 +46,6 @@
 # those contributions and any derivatives thereof.
 # 
 # END BPS TAGGED BLOCK }}}
 # those contributions and any derivatives thereof.
 # 
 # END BPS TAGGED BLOCK }}}
-
 # Designed and implemented for Best Practical Solutions, LLC by
 # Abhijit Menon-Sen <ams@wiw.org>
 
 # Designed and implemented for Best Practical Solutions, LLC by
 # Abhijit Menon-Sen <ams@wiw.org>
 
@@ -57,6 +58,7 @@ use Cwd;
 use LWP;
 use Text::ParseWords;
 use HTTP::Request::Common;
 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).
 
 # We derive configuration information from hardwired defaults, dotfiles,
 # and the RT* environment variables (in increasing order of precedence).
@@ -71,7 +73,7 @@ my %config = (
         debug   => 0,
         user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
         passwd  => undef,
         debug   => 0,
         user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
         passwd  => undef,
-        server  => 'http://localhost/rt/',
+        server  => 'http://localhost/',
         query   => undef,
         orderby => undef,
     ),
         query   => undef,
         orderby => undef,
     ),
@@ -81,6 +83,8 @@ my %config = (
 my $session = new Session("$HOME/.rt_sessions");
 my $REST = "$config{server}/REST/1.0";
 
 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 }
 
 sub whine;
 sub DEBUG { warn @_ if $config{debug} >= shift }
 
@@ -113,6 +117,8 @@ my %handlers = (
     link        => ["link", "ln"],
     merge       => ["merge"],
     grant       => ["grant", "revoke"],
     link        => ["link", "ln"],
     merge       => ["merge"],
     grant       => ["grant", "revoke"],
+    take        => ["take", "steal", "untake"],
+    quit        => ["quit", "exit"],
 );
 
 my %actions;
 );
 
 my %actions;
@@ -127,10 +133,16 @@ foreach my $fn (keys %handlers) {
 sub handler {
     my $action;
 
 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;
     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();
 }
 
 handler();
@@ -143,16 +155,13 @@ exit;
 
 sub shell {
     $|=1;
 
 sub shell {
     $|=1;
-    print "rt> ";
-    while (<>) {
-        chomp;
+    my $term = new Term::ReadLine 'RT CLI';
+    while ( defined ($_ = $term->readline($prompt)) ) {
         next if /^#/ || /^\s*$/;
 
         @ARGV = shellwords($_);
         handler();
         next if /^#/ || /^\s*$/;
 
         @ARGV = shellwords($_);
         handler();
-        print "rt> ";
     }
     }
-    print "\n";
 }
 
 sub version {
 }
 
 sub version {
@@ -163,6 +172,11 @@ sub logout {
     submit("$REST/logout") if defined $session->cookie;
 }
 
     submit("$REST/logout") if defined $session->cookie;
 }
 
+sub quit {
+    logout();
+    exit;
+}
+
 my %help;
 sub help {
     my ($action, $type) = @_;
 my %help;
 sub help {
     my ($action, $type) = @_;
@@ -268,7 +282,8 @@ sub list {
         whine "No $item specified.";
         $bad = 1;
     }
         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;
 
     my $r = submit("$REST/search/$type", { query => $q, %data });
     print $r->content;
@@ -325,10 +340,18 @@ sub show {
         whine "No objects specified.";
         $bad = 1;
     }
         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 });
 
     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
 }
 
 # To create a new object, we ask the server for a form with the defaults
@@ -432,18 +455,23 @@ sub edit {
         }
         @objects = ("$type/new");
     }
         }
         @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
 
     # 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 = <STDIN>;
     }
 
     if ($input) {
         local $/ = undef;
         $text = <STDIN>;
     }
-    elsif ($edit || %add || %del || !$cl) {
+    elsif ($edit || %add || %del || !$cl || @new_objects) {
         my $r = submit("$REST/show", { id => \@objects, format => 'l' });
         $text = $r->content;
     }
         my $r = submit("$REST/show", { id => \@objects, format => 'l' });
         $text = $r->content;
     }
@@ -599,7 +627,8 @@ sub comment {
         whine "No object specified.";
         $bad = 1;
     }
         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 = [
         "",
 
     my $form = [
         "",
@@ -652,7 +681,7 @@ sub comment {
     }
     $data{content} = $text;
 
     }
     $data{content} = $text;
 
-    my $r = submit("$REST/ticket/comment/$id", \%data);
+    my $r = submit("$REST/ticket/$id/comment", \%data);
     print $r->content;
 }
 
     print $r->content;
 }
 
@@ -679,9 +708,10 @@ sub merge {
         whine "Too $evil arguments specified.";
         $bad = 1;
     }
         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;
 }
 
     print $r->content;
 }
 
@@ -722,12 +752,51 @@ sub link {
         whine "Too $bad arguments specified.";
         $bad = 1;
     }
         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;
 }
 
 
     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 {
 # Grant/revoke a user's rights.
 
 sub grant {
@@ -839,7 +908,7 @@ sub submit {
             # For anything else, we just die.
             elsif ($res->code != 409) {
                 warn "rt: ", $res->content;
             # For anything else, we just die.
             elsif ($res->code != 409) {
                 warn "rt: ", $res->content;
-                exit;
+                #exit;
             }
         }
     }
             }
         }
     }
@@ -1268,17 +1337,29 @@ sub vsplit {
     return \@words;
 }
 
     return \@words;
 }
 
+# WARN: this code is duplicated in lib/RT/Interface/REST.pm
+# change both functions at once
 sub expand_list {
     my ($list) = @_;
 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 {
 }
 
 sub get_type_argument {
@@ -1328,16 +1409,23 @@ sub is_object_spec {
     return;
 }
 
     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:
 
 __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
 
     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
@@ -1349,9 +1437,10 @@ Text:
 
     For more information:
 
 
     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 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)
 
         - rt help config        (configuration details)
         - rt help examples      (a few useful examples)
@@ -1366,6 +1455,8 @@ Text:
     Syntax:
 
         rt <action> [options] [arguments]
     Syntax:
 
         rt <action> [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
 
     Each invocation of this program must specify an action (e.g. "edit",
     "create"), options to modify behaviour, and other arguments required
@@ -1376,6 +1467,10 @@ Text:
     "rt help <action>". Some actions may be referred to by more than one
     name ("create" is the same as "new", for example).  
 
     "rt help <action>". 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 "<action> [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
     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
@@ -1390,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 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)
 
 --
 
 
 --
 
@@ -1673,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.
         -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
     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
 
 --
         rt show -t user 2
 
 --
@@ -1740,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 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
 
 --
                             add cc=foo@example.com
 
 --
@@ -1786,7 +1884,7 @@ Text:
 
         rt merge <from-id> <to-id>
 
 
         rt merge <from-id> <to-id>
 
-    Merges the two specified tickets.
+    Merges the first ticket specified into the second ticket specified.
 
 --
 
 
 --
 
@@ -1861,7 +1959,11 @@ Text:
 Title: topics
 Text:
 
 Title: topics
 Text:
 
-    Use "rt help <topic>" for help on any of the following subjects:
+    Syntax:
+
+        rt help <topic>
+
+    Get help on any of the following subjects:
 
         - tickets, users, groups, queues.
         - show, edit, ls/list/search, new/create.
 
         - tickets, users, groups, queues.
         - show, edit, ls/list/search, new/create.
@@ -1888,3 +1990,71 @@ Text:
     For the moment, please consult examples provided with each action.
 
 --
     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 "<action> [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 <take|untake|steal> <ticket-id>
+
+    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
+        $