import rt 3.6.4
[freeside.git] / rt / bin / rt.in
1 #!@PERL@ -w
2 # BEGIN BPS TAGGED BLOCK {{{
3
4 # COPYRIGHT:
5 #  
6 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
7 #                                          <jesse@bestpractical.com>
8
9 # (Except where explicitly superseded by other copyright notices)
10
11
12 # LICENSE:
13
14 # This work is made available to you under the terms of Version 2 of
15 # the GNU General Public License. A copy of that license should have
16 # been provided with this software, but in any event can be snarfed
17 # from www.gnu.org.
18
19 # This work is distributed in the hope that it will be useful, but
20 # WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
22 # General Public License for more details.
23
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
27 # 02110-1301 or visit their web page on the internet at
28 # http://www.gnu.org/copyleft/gpl.html.
29
30
31 # CONTRIBUTION SUBMISSION POLICY:
32
33 # (The following paragraph is not intended to limit the rights granted
34 # to you to modify and distribute this software under the terms of
35 # the GNU General Public License and is only of importance to you if
36 # you choose to contribute your changes and enhancements to the
37 # community by submitting them to Best Practical Solutions, LLC.)
38
39 # By intentionally submitting any modifications, corrections or
40 # derivatives to this work, or any other work intended for use with
41 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
42 # you are the copyright holder for those contributions and you grant
43 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
44 # royalty-free, perpetual, license to use, copy, create derivative
45 # works based on those contributions, and sublicense and distribute
46 # those contributions and any derivatives thereof.
47
48 # END BPS TAGGED BLOCK }}}
49 # Designed and implemented for Best Practical Solutions, LLC by
50 # Abhijit Menon-Sen <ams@wiw.org>
51
52 use strict;
53
54 # This program is intentionally written to have as few non-core module
55 # dependencies as possible. It should stay that way.
56
57 use Cwd;
58 use LWP;
59 use Text::ParseWords;
60 use HTTP::Request::Common;
61 use Term::ReadLine;
62
63 # We derive configuration information from hardwired defaults, dotfiles,
64 # and the RT* environment variables (in increasing order of precedence).
65 # Session information is stored in ~/.rt_sessions.
66
67 my $VERSION = 0.02;
68 my $HOME = eval{(getpwuid($<))[7]}
69            || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
70            || ".";
71 my %config = (
72     (
73         debug   => 0,
74         user    => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
75         passwd  => undef,
76         server  => 'http://localhost/',
77         query   => undef,
78         orderby => undef,
79     ),
80     config_from_file($ENV{RTCONFIG} || ".rtrc"),
81     config_from_env()
82 );
83 my $session = new Session("$HOME/.rt_sessions");
84 my $REST = "$config{server}/REST/1.0";
85
86 my $prompt = 'rt> ';
87
88 sub whine;
89 sub DEBUG { warn @_ if $config{debug} >= shift }
90
91 # These regexes are used by command handlers to parse arguments.
92 # (XXX: Ask Autrijus how i18n changes these definitions.)
93
94 my $name   = '[\w.-]+';
95 my $field  = '[a-zA-Z][a-zA-Z0-9_-]*';
96 my $label  = '[a-zA-Z0-9@_.+-]+';
97 my $labels = "(?:$label,)*$label";
98 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
99
100 # Our command line looks like this:
101 #
102 #     rt <action> [options] [arguments]
103 #
104 # We'll parse just enough of it to decide upon an action to perform, and
105 # leave the rest to per-action handlers to interpret appropriately.
106
107 my %handlers = (
108 #   handler     => [ ...aliases... ],
109     version     => ["version", "ver"],
110     shell       => ["shell"],
111     logout      => ["logout"],
112     help        => ["help", "man"],
113     show        => ["show", "cat"],
114     edit        => ["create", "edit", "new", "ed"],
115     list        => ["search", "list", "ls"],
116     comment     => ["comment", "correspond"],
117     link        => ["link", "ln"],
118     merge       => ["merge"],
119     grant       => ["grant", "revoke"],
120     take        => ["take", "steal", "untake"],
121     quit        => ["quit", "exit"],
122 );
123
124 my %actions;
125 foreach my $fn (keys %handlers) {
126     foreach my $alias (@{ $handlers{$fn} }) {
127         $actions{$alias} = \&{"$fn"};
128     }
129 }
130
131 # Once we find and call an appropriate handler, we're done.
132
133 sub handler {
134     my $action;
135
136     push @ARGV, 'shell' if (!@ARGV);    # default to shell mode
137     shift @ARGV if ($ARGV[0] eq 'rt');    # ignore a leading 'rt'
138     if (@ARGV && exists $actions{$ARGV[0]}) {
139         $action = shift @ARGV;
140         $actions{$action}->($action);
141     }
142     else {
143         print STDERR "rt: Unknown command '@ARGV'.\n";
144         print STDERR "rt: For help, run 'rt help'.\n";
145     }
146 }
147
148 handler();
149 exit;
150
151 # Handler functions.
152 # ------------------
153 #
154 # The following subs are handlers for each entry in %actions.
155
156 sub shell {
157     $|=1;
158     my $term = new Term::ReadLine 'RT CLI';
159     while ( defined ($_ = $term->readline($prompt)) ) {
160         next if /^#/ || /^\s*$/;
161
162         @ARGV = shellwords($_);
163         handler();
164     }
165 }
166
167 sub version {
168     print "rt $VERSION\n";
169 }
170
171 sub logout {
172     submit("$REST/logout") if defined $session->cookie;
173 }
174
175 sub quit {
176     logout();
177     exit;
178 }
179
180 my %help;
181 sub help {
182     my ($action, $type) = @_;
183     my $key;
184
185     # What help topics do we know about?
186     if (!%help) {
187         local $/ = undef;
188         foreach my $item (@{ Form::parse(<DATA>) }) {
189             my $title = $item->[2]{Title};
190             my @titles = ref $title eq 'ARRAY' ? @$title : $title;
191
192             foreach $title (grep $_, @titles) {
193                 $help{$title} = $item->[2]{Text};
194             }
195         }
196     }
197
198     # What does the user want help with?
199     undef $action if ($action && $actions{$action} eq \&help);
200     unless ($action || $type) {
201         # If we don't know, we'll look for clues in @ARGV.
202         foreach (@ARGV) {
203             if (exists $help{$_}) { $key = $_; last; }
204         }
205         unless ($key) {
206             # Tolerate possibly plural words.
207             foreach (@ARGV) {
208                 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
209             }
210         }
211     }
212
213     if ($type && $action) {
214         $key = "$type.$action";
215     }
216     $key ||= $type || $action || "introduction";
217
218     # Find a suitable topic to display.
219     while (!exists $help{$key}) {
220         if ($type && $action) {
221             if ($key eq "$type.$action") { $key = $action;        }
222             elsif ($key eq $action)      { $key = $type;          }
223             else                         { $key = "introduction"; }
224         }
225         else {
226             $key = "introduction";
227         }
228     }
229
230     print STDERR $help{$key}, "\n\n";
231 }
232
233 # Displays a list of objects that match some specified condition.
234
235 sub list {
236     my ($q, $type, %data);
237     my $orderby = $config{orderby};
238     
239     if ($config{orderby}) {
240          $data{orderby} = $config{orderby};
241     } 
242     my $bad = 0;
243
244     while (@ARGV) {
245         $_ = shift @ARGV;
246
247         if (/^-t$/) {
248             $bad = 1, last unless defined($type = get_type_argument());
249         }
250         elsif (/^-S$/) {
251             $bad = 1, last unless get_var_argument(\%data);
252         }
253         elsif (/^-o$/) {
254             $data{'orderby'} = shift @ARGV;
255         }
256         elsif (/^-([isl])$/) {
257             $data{format} = $1;
258         }
259         elsif (/^-f$/) {
260             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
261                 whine "No valid field list in '-f $ARGV[0]'.";
262                 $bad = 1; last;
263             }
264             $data{fields} = shift @ARGV;
265         }
266         elsif (!defined $q && !/^-/) {
267             $q = $_;
268         }
269         else {
270             my $datum = /^-/ ? "option" : "argument";
271             whine "Unrecognised $datum '$_'.";
272             $bad = 1; last;
273         }
274     }
275     if (!defined $q) {
276         $q = $config{query}; 
277     }
278     
279     $type ||= "ticket";
280     unless ($type && defined $q) {
281         my $item = $type ? "query string" : "object type";
282         whine "No $item specified.";
283         $bad = 1;
284     }
285     #return help("list", $type) if $bad;
286     return suggest_help("list", $type) if $bad;
287
288     my $r = submit("$REST/search/$type", { query => $q, %data });
289     print $r->content;
290 }
291
292 # Displays selected information about a single object.
293
294 sub show {
295     my ($type, @objects, %data);
296     my $slurped = 0;
297     my $bad = 0;
298
299     while (@ARGV) {
300         $_ = shift @ARGV;
301
302         if (/^-t$/) {
303             $bad = 1, last unless defined($type = get_type_argument());
304         }
305         elsif (/^-S$/) {
306             $bad = 1, last unless get_var_argument(\%data);
307         }
308         elsif (/^-([isl])$/) {
309             $data{format} = $1;
310         }
311         elsif (/^-$/ && !$slurped) {
312             chomp(my @lines = <STDIN>);
313             foreach (@lines) {
314                 unless (is_object_spec($_, $type)) {
315                     whine "Invalid object on STDIN: '$_'.";
316                     $bad = 1; last;
317                 }
318                 push @objects, $_;
319             }
320             $slurped = 1;
321         }
322         elsif (/^-f$/) {
323             if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
324                 whine "No valid field list in '-f $ARGV[0]'.";
325                 $bad = 1; last;
326             }
327             $data{fields} = shift @ARGV;
328         }
329         elsif (my $spec = is_object_spec($_, $type)) {
330             push @objects, $spec;
331         }
332         else {
333             my $datum = /^-/ ? "option" : "argument";
334             whine "Unrecognised $datum '$_'.";
335             $bad = 1; last;
336         }
337     }
338
339     unless (@objects) {
340         whine "No objects specified.";
341         $bad = 1;
342     }
343     #return help("show", $type) if $bad;
344     return suggest_help("show", $type) if $bad;
345
346     my $r = submit("$REST/show", { id => \@objects, %data });
347     my $c = $r->content;
348     # if this isn't a text reply, remove the trailing newline so we
349     # don't corrupt things like tarballs when people do
350     # show ticket/id/attachments/id/content > foo.tar.gz
351     if ($r->content_type !~ /^text\//) {
352         chomp($c);
353     }
354     print $c;
355 }
356
357 # To create a new object, we ask the server for a form with the defaults
358 # filled in, allow the user to edit it, and send the form back.
359 #
360 # To edit an object, we must ask the server for a form representing that
361 # object, make changes requested by the user (either on the command line
362 # or interactively via $EDITOR), and send the form back.
363
364 sub edit {
365     my ($action) = @_;
366     my (%data, $type, @objects);
367     my ($cl, $text, $edit, $input, $output);
368
369     use vars qw(%set %add %del);
370     %set = %add = %del = ();
371     my $slurped = 0;
372     my $bad = 0;
373     
374     while (@ARGV) {
375         $_ = shift @ARGV;
376
377         if    (/^-e$/) { $edit = 1 }
378         elsif (/^-i$/) { $input = 1 }
379         elsif (/^-o$/) { $output = 1 }
380         elsif (/^-t$/) {
381             $bad = 1, last unless defined($type = get_type_argument());
382         }
383         elsif (/^-S$/) {
384             $bad = 1, last unless get_var_argument(\%data);
385         }
386         elsif (/^-$/ && !($slurped || $input)) {
387             chomp(my @lines = <STDIN>);
388             foreach (@lines) {
389                 unless (is_object_spec($_, $type)) {
390                     whine "Invalid object on STDIN: '$_'.";
391                     $bad = 1; last;
392                 }
393                 push @objects, $_;
394             }
395             $slurped = 1;
396         }
397         elsif (/^set$/i) {
398             my $vars = 0;
399
400             while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
401                 my ($key, $op, $val) = ($1, $2, $3);
402                 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
403
404                 vpush($hash, lc $key, $val);
405                 shift @ARGV;
406                 $vars++;
407             }
408             unless ($vars) {
409                 whine "No variables to set.";
410                 $bad = 1; last;
411             }
412             $cl = $vars;
413         }
414         elsif (/^(?:add|del)$/i) {
415             my $vars = 0;
416             my $hash = ($_ eq "add") ? \%add : \%del;
417
418             while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
419                 my ($key, $val) = ($1, $2);
420
421                 vpush($hash, lc $key, $val);
422                 shift @ARGV;
423                 $vars++;
424             }
425             unless ($vars) {
426                 whine "No variables to set.";
427                 $bad = 1; last;
428             }
429             $cl = $vars;
430         }
431         elsif (my $spec = is_object_spec($_, $type)) {
432             push @objects, $spec;
433         }
434         else {
435             my $datum = /^-/ ? "option" : "argument";
436             whine "Unrecognised $datum '$_'.";
437             $bad = 1; last;
438         }
439     }
440
441     if ($action =~ /^ed(?:it)?$/) {
442         unless (@objects) {
443             whine "No objects specified.";
444             $bad = 1;
445         }
446     }
447     else {
448         if (@objects) {
449             whine "You shouldn't specify objects as arguments to $action.";
450             $bad = 1;
451         }
452         unless ($type) {
453             whine "What type of object do you want to create?";
454             $bad = 1;
455         }
456         @objects = ("$type/new");
457     }
458     #return help($action, $type) if $bad;
459     return suggest_help($action, $type) if $bad;
460
461     # We need a form to make changes to. We usually ask the server for
462     # one, but we can avoid that if we are fed one on STDIN, or if the
463     # user doesn't want to edit the form by hand, and the command line
464     # specifies only simple variable assignments.  We *should* get a
465     # form if we're creating a new ticket, so that the default values
466     # get filled in properly.
467
468     my @new_objects = grep /\/new$/, @objects;
469
470     if ($input) {
471         local $/ = undef;
472         $text = <STDIN>;
473     }
474     elsif ($edit || %add || %del || !$cl || @new_objects) {
475         my $r = submit("$REST/show", { id => \@objects, format => 'l' });
476         $text = $r->content;
477     }
478
479     # If any changes were specified on the command line, apply them.
480     if ($cl) {
481         if ($text) {
482             # We're updating forms from the server.
483             my $forms = Form::parse($text);
484
485             foreach my $form (@$forms) {
486                 my ($c, $o, $k, $e) = @$form;
487                 my ($key, $val);
488
489                 next if ($e || !@$o);
490
491                 local %add = %add;
492                 local %del = %del;
493                 local %set = %set;
494
495                 # Make changes to existing fields.
496                 foreach $key (@$o) {
497                     if (exists $add{lc $key}) {
498                         $val = delete $add{lc $key};
499                         vpush($k, $key, $val);
500                         $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
501                     }
502                     if (exists $del{lc $key}) {
503                         $val = delete $del{lc $key};
504                         my %val = map {$_=>1} @{ vsplit($val) };
505                         $k->{$key} = vsplit($k->{$key});
506                         @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
507                     }
508                     if (exists $set{lc $key}) {
509                         $k->{$key} = delete $set{lc $key};
510                     }
511                 }
512                 
513                 # Then update the others.
514                 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
515                 foreach $key (keys %add) {
516                     vpush($k, $key, $add{$key});
517                     $k->{$key} = vsplit($k->{$key});
518                 }
519                 push @$o, (keys %add, keys %set);
520             }
521
522             $text = Form::compose($forms);
523         }
524         else {
525             # We're rolling our own set of forms.
526             my @forms;
527             foreach (@objects) {
528                 my ($type, $ids, $args) =
529                     m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
530
531                 $args ||= "";
532                 foreach my $obj (expand_list($ids)) {
533                     my %set = (%set, id => "$type/$obj$args");
534                     push @forms, ["", [keys %set], \%set];
535                 }
536             }
537             $text = Form::compose(\@forms);
538         }
539     }
540
541     if ($output) {
542         print $text;
543         return;
544     }
545
546     my $synerr = 0;
547
548 EDIT:
549     # We'll let the user edit the form before sending it to the server,
550     # unless we have enough information to submit it non-interactively.
551     if ($edit || (!$input && !$cl)) {
552         my $newtext = vi($text);
553         # We won't resubmit a bad form unless it was changed.
554         $text = ($synerr && $newtext eq $text) ? undef : $newtext;
555     }
556
557     if ($text) {
558         my $r = submit("$REST/edit", {content => $text, %data});
559         if ($r->code == 409) {
560             # If we submitted a bad form, we'll give the user a chance
561             # to correct it and resubmit.
562             if ($edit || (!$input && !$cl)) {
563                 $text = $r->content;
564                 $synerr = 1;
565                 goto EDIT;
566             }
567             else {
568                 print $r->content;
569                 return;
570             }
571         }
572         print $r->content;
573     }
574 }
575
576 # We roll "comment" and "correspond" into the same handler.
577
578 sub comment {
579     my ($action) = @_;
580     my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
581     my $bad = 0;
582
583     while (@ARGV) {
584         $_ = shift @ARGV;
585
586         if (/^-e$/) {
587             $edit = 1;
588         }
589         elsif (/^-[abcmw]$/) {
590             unless (@ARGV) {
591                 whine "No argument specified with $_.";
592                 $bad = 1; last;
593             }
594
595             if (/-a/) {
596                 unless (-f $ARGV[0] && -r $ARGV[0]) {
597                     whine "Cannot read attachment: '$ARGV[0]'.";
598                     return;
599                 }
600                 push @files, shift @ARGV;
601             }
602             elsif (/-([bc])/) {
603                 my $a = $_ eq "-b" ? \@bcc : \@cc;
604                 @$a = split /\s*,\s*/, shift @ARGV;
605             }
606             elsif (/-m/) {
607                 $msg = shift @ARGV;
608                 if ( $msg =~ /^-$/ ) {
609                     undef $msg;
610                     while (<STDIN>) { $msg .= $_ }
611                 }
612             }
613
614             elsif (/-w/) { $wtime = shift @ARGV }
615         }
616         elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
617             $id = $1;
618         }
619         else {
620             my $datum = /^-/ ? "option" : "argument";
621             whine "Unrecognised $datum '$_'.";
622             $bad = 1; last;
623         }
624     }
625
626     unless ($id) {
627         whine "No object specified.";
628         $bad = 1;
629     }
630     #return help($action, "ticket") if $bad;
631     return suggest_help($action, "ticket") if $bad;
632
633     my $form = [
634         "",
635         [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
636         {
637             Ticket     => $id,
638             Action     => $action,
639             Cc         => [ @cc ],
640             Bcc        => [ @bcc ],
641             Attachment => [ @files ],
642             TimeWorked => $wtime || '',
643             Text       => $msg || '',
644             Status => ''
645         }
646     ];
647
648     my $text = Form::compose([ $form ]);
649
650     if ($edit || !$msg) {
651         my $error = 0;
652         my ($c, $o, $k, $e);
653
654         do {
655             my $ntext = vi($text);
656             return if ($error && $ntext eq $text);
657             $text = $ntext;
658             $form = Form::parse($text);
659             $error = 0;
660
661             ($c, $o, $k, $e) = @{ $form->[0] };
662             if ($e) {
663                 $error = 1;
664                 $c = "# Syntax error.";
665                 goto NEXT;
666             }
667             elsif (!@$o) {
668                 return;
669             }
670             @files = @{ vsplit($k->{Attachment}) };
671
672         NEXT:
673             $text = Form::compose([[$c, $o, $k, $e]]);
674         } while ($error);
675     }
676
677     my $i = 1;
678     foreach my $file (@files) {
679         $data{"attachment_$i"} = bless([ $file ], "Attachment");
680         $i++;
681     }
682     $data{content} = $text;
683
684     my $r = submit("$REST/ticket/$id/comment", \%data);
685     print $r->content;
686 }
687
688 # Merge one ticket into another.
689
690 sub merge {
691     my @id;
692     my $bad = 0;
693
694     while (@ARGV) {
695         $_ = shift @ARGV;
696
697         if (/^\d+$/) {
698             push @id, $_;
699         }
700         else {
701             whine "Unrecognised argument: '$_'.";
702             $bad = 1; last;
703         }
704     }
705
706     unless (@id == 2) {
707         my $evil = @id > 2 ? "many" : "few";
708         whine "Too $evil arguments specified.";
709         $bad = 1;
710     }
711     #return help("merge", "ticket") if $bad;
712     return suggest_help("merge", "ticket") if $bad;
713
714     my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
715     print $r->content;
716 }
717
718 # Link one ticket to another.
719
720 sub link {
721     my ($bad, $del, %data) = (0, 0, ());
722     my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
723                                         ReferredToBy HasMember MemberOf);
724
725     while (@ARGV && $ARGV[0] =~ /^-/) {
726         $_ = shift @ARGV;
727
728         if (/^-d$/) {
729             $del = 1;
730         }
731         else {
732             whine "Unrecognised option: '$_'.";
733             $bad = 1; last;
734         }
735     }
736
737     if (@ARGV == 3) {
738         my ($from, $rel, $to) = @ARGV;
739         if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
740             my $bad = $from =~ /^\d+$/ ? $to : $from;
741             whine "Invalid ticket ID '$bad' specified.";
742             $bad = 1;
743         }
744         unless (exists $ltypes{lc $rel}) {
745             whine "Invalid link '$rel' specified.";
746             $bad = 1;
747         }
748         %data = (id => $from, rel => $rel, to => $to, del => $del);
749     }
750     else {
751         my $bad = @ARGV < 3 ? "few" : "many";
752         whine "Too $bad arguments specified.";
753         $bad = 1;
754     }
755     #return help("link", "ticket") if $bad;
756     return suggest_help("link", "ticket") if $bad;
757
758     my $r = submit("$REST/ticket/link", \%data);
759     print $r->content;
760 }
761
762 # Take/steal a ticket
763 sub take {
764     my ($cmd) = @_;
765     my ($bad, %data) = (0, ());
766
767     my $id;
768
769     # get the ticket id
770     if (@ARGV == 1) {
771         ($id) = @ARGV;
772         unless ($id =~ /^\d+$/) {
773             whine "Invalid ticket ID $id specified.";
774             $bad = 1;
775         }
776         my $form = [
777             "",
778             [ "Ticket", "Action" ],
779             {
780                 Ticket => $id,
781                 Action => $cmd,
782                 Status => '',
783             }
784         ];
785
786         my $text = Form::compose([ $form ]);
787         $data{content} = $text;
788     }
789     else {
790         $bad = @ARGV < 1 ? "few" : "many";
791         whine "Too $bad arguments specified.";
792         $bad = 1;
793     }
794     return suggest_help("take", "ticket") if $bad;
795
796     my $r = submit("$REST/ticket/$id/take", \%data);
797     print $r->content;
798 }
799
800 # Grant/revoke a user's rights.
801
802 sub grant {
803     my ($cmd) = @_;
804
805     my $revoke = 0;
806     while (@ARGV) {
807     }
808
809     $revoke = 1 if $cmd->{action} eq 'revoke';
810 }
811
812 # Client <-> Server communication.
813 # --------------------------------
814 #
815 # This function composes and sends an HTTP request to the RT server, and
816 # interprets the response. It takes a request URI, and optional request
817 # data (a string, or a reference to a set of key-value pairs).
818
819 sub submit {
820     my ($uri, $content) = @_;
821     my ($req, $data);
822     my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
823
824     # Did the caller specify any data to send with the request?
825     $data = [];
826     if (defined $content) {
827         unless (ref $content) {
828             # If it's just a string, make sure LWP handles it properly.
829             # (By pretending that it's a file!)
830             $content = [ content => [undef, "", Content => $content] ];
831         }
832         elsif (ref $content eq 'HASH') {
833             my @data;
834             foreach my $k (keys %$content) {
835                 if (ref $content->{$k} eq 'ARRAY') {
836                     foreach my $v (@{ $content->{$k} }) {
837                         push @data, $k, $v;
838                     }
839                 }
840                 else { push @data, $k, $content->{$k} }
841             }
842             $content = \@data;
843         }
844         $data = $content;
845     }
846
847     # Should we send authentication information to start a new session?
848     if (!defined $session->cookie) {
849         push @$data, ( user => $config{user} );
850         push @$data, ( pass => $config{passwd} || read_passwd() );
851     }
852
853     # Now, we construct the request.
854     if (@$data) {
855         $req = POST($uri, $data, Content_Type => 'form-data');
856     }
857     else {
858         $req = GET($uri);
859     }
860     $session->add_cookie_header($req);
861
862     # Then we send the request and parse the response.
863     DEBUG(3, $req->as_string);
864     my $res = $ua->request($req);
865     DEBUG(3, $res->as_string);
866
867     if ($res->is_success) {
868         # The content of the response we get from the RT server consists
869         # of an HTTP-like status line followed by optional header lines,
870         # a blank line, and arbitrary text.
871
872         my ($head, $text) = split /\n\n/, $res->content, 2;
873         my ($status, @headers) = split /\n/, $head;
874         $text =~ s/\n*$/\n/ if ($text);
875
876         # "RT/3.0.1 401 Credentials required"
877         if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
878             warn "rt: Malformed RT response from $config{server}.\n";
879             warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
880             exit -1;
881         }
882
883         # Our caller can pretend that the server returned a custom HTTP
884         # response code and message. (Doing that directly is apparently
885         # not sufficiently portable and uncomplicated.)
886         $res->code($1);
887         $res->message($2);
888         $res->content($text);
889         $session->update($res) if ($res->is_success || $res->code != 401);
890
891         if (!$res->is_success) {
892             # We can deal with authentication failures ourselves. Either
893             # we sent invalid credentials, or our session has expired.
894             if ($res->code == 401) {
895                 my %d = @$data;
896                 if (exists $d{user}) {
897                     warn "rt: Incorrect username or password.\n";
898                     exit -1;
899                 }
900                 elsif ($req->header("Cookie")) {
901                     # We'll retry the request with credentials, unless
902                     # we only wanted to logout in the first place.
903                     $session->delete;
904                     return submit(@_) unless $uri eq "$REST/logout";
905                 }
906             }
907             # Conflicts should be dealt with by the handler and user.
908             # For anything else, we just die.
909             elsif ($res->code != 409) {
910                 warn "rt: ", $res->content;
911                 #exit;
912             }
913         }
914     }
915     else {
916         warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
917         exit -1;
918     }
919
920     return $res;
921 }
922
923 # Session management.
924 # -------------------
925 #
926 # Maintains a list of active sessions in the ~/.rt_sessions file.
927 {
928     package Session;
929     my ($s, $u);
930
931     # Initialises the session cache.
932     sub new {
933         my ($class, $file) = @_;
934         my $self = {
935             file => $file || "$HOME/.rt_sessions",
936             sids => { }
937         };
938        
939         # The current session is identified by the currently configured
940         # server and user.
941         ($s, $u) = @config{"server", "user"};
942
943         bless $self, $class;
944         $self->load();
945
946         return $self;
947     }
948
949     # Returns the current session cookie.
950     sub cookie {
951         my ($self) = @_;
952         my $cookie = $self->{sids}{$s}{$u};
953         return defined $cookie ? "RT_SID_$cookie" : undef;
954     }
955
956     # Deletes the current session cookie.
957     sub delete {
958         my ($self) = @_;
959         delete $self->{sids}{$s}{$u};
960     }
961
962     # Adds a Cookie header to an outgoing HTTP request.
963     sub add_cookie_header {
964         my ($self, $request) = @_;
965         my $cookie = $self->cookie();
966
967         $request->header(Cookie => $cookie) if defined $cookie;
968     }
969
970     # Extracts the Set-Cookie header from an HTTP response, and updates
971     # session information accordingly.
972     sub update {
973         my ($self, $response) = @_;
974         my $cookie = $response->header("Set-Cookie");
975
976         if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
977             $self->{sids}{$s}{$u} = $1;
978         }
979     }
980
981     # Loads the session cache from the specified file.
982     sub load {
983         my ($self, $file) = @_;
984         $file ||= $self->{file};
985         local *F;
986
987         open(F, $file) && do {
988             $self->{file} = $file;
989             my $sids = $self->{sids} = {};
990             while (<F>) {
991                 chomp;
992                 next if /^$/ || /^#/;
993                 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
994                 my ($server, $user, $cookie) = split / /, $_;
995                 $sids->{$server}{$user} = $cookie;
996             }
997             return 1;
998         };
999         return 0;
1000     }
1001
1002     # Writes the current session cache to the specified file.
1003     sub save {
1004         my ($self, $file) = shift;
1005         $file ||= $self->{file};
1006         local *F;
1007
1008         open(F, ">$file") && do {
1009             my $sids = $self->{sids};
1010             foreach my $server (keys %$sids) {
1011                 foreach my $user (keys %{ $sids->{$server} }) {
1012                     my $sid = $sids->{$server}{$user};
1013                     if (defined $sid) {
1014                         print F "$server $user $sid\n";
1015                     }
1016                 }
1017             }
1018             close(F);
1019             chmod 0600, $file;
1020             return 1;
1021         };
1022         return 0;
1023     }
1024
1025     sub DESTROY {
1026         my $self = shift;
1027         $self->save;
1028     }
1029 }
1030
1031 # Form handling.
1032 # --------------
1033 #
1034 # Forms are RFC822-style sets of (field, value) specifications with some
1035 # initial comments and interspersed blank lines allowed for convenience.
1036 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1037 #
1038 # Each form is parsed into an array with four elements: commented text
1039 # at the start of the form, an array with the order of keys, a hash with
1040 # key/value pairs, and optional error text if the form syntax was wrong.
1041
1042 # Returns a reference to an array of parsed forms.
1043 sub Form::parse {
1044     my $state = 0;
1045     my @forms = ();
1046     my @lines = split /\n/, $_[0];
1047     my ($c, $o, $k, $e) = ("", [], {}, "");
1048
1049     LINE:
1050     while (@lines) {
1051         my $line = shift @lines;
1052
1053         next LINE if $line eq '';
1054
1055         if ($line eq '--') {
1056             # We reached the end of one form. We'll ignore it if it was
1057             # empty, and store it otherwise, errors and all.
1058             if ($e || $c || @$o) {
1059                 push @forms, [ $c, $o, $k, $e ];
1060                 $c = ""; $o = []; $k = {}; $e = "";
1061             }
1062             $state = 0;
1063         }
1064         elsif ($state != -1) {
1065             if ($state == 0 && $line =~ /^#/) {
1066                 # Read an optional block of comments (only) at the start
1067                 # of the form.
1068                 $state = 1;
1069                 $c = $line;
1070                 while (@lines && $lines[0] =~ /^#/) {
1071                     $c .= "\n".shift @lines;
1072                 }
1073                 $c .= "\n";
1074             }
1075             elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1076                 # Read a field: value specification.
1077                 my $f  = $1;
1078                 my @v  = ($2 || ());
1079
1080                 # Read continuation lines, if any.
1081                 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1082                     push @v, shift @lines;
1083                 }
1084                 pop @v while (@v && $v[-1] eq '');
1085
1086                 # Strip longest common leading indent from text.
1087                 my $ws = "";
1088                 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1089                     $ws = $ls if (!$ws || length($ls) < length($ws));
1090                 }
1091                 s/^$ws// foreach @v;
1092
1093                 push(@$o, $f) unless exists $k->{$f};
1094                 vpush($k, $f, join("\n", @v));
1095
1096                 $state = 1;
1097             }
1098             elsif ($line !~ /^#/) {
1099                 # We've found a syntax error, so we'll reconstruct the
1100                 # form parsed thus far, and add an error marker. (>>)
1101                 $state = -1;
1102                 $e = Form::compose([[ "", $o, $k, "" ]]);
1103                 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1104             }
1105         }
1106         else {
1107             # We saw a syntax error earlier, so we'll accumulate the
1108             # contents of this form until the end.
1109             $e .= "$line\n";
1110         }
1111     }
1112     push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1113
1114     foreach my $l (keys %$k) {
1115         $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1116     }
1117
1118     return \@forms;
1119 }
1120
1121 # Returns text representing a set of forms.
1122 sub Form::compose {
1123     my ($forms) = @_;
1124     my @text;
1125
1126     foreach my $form (@$forms) {
1127         my ($c, $o, $k, $e) = @$form;
1128         my $text = "";
1129
1130         if ($c) {
1131             $c =~ s/\n*$/\n/;
1132             $text = "$c\n";
1133         }
1134         if ($e) {
1135             $text .= $e;
1136         }
1137         elsif ($o) {
1138             my @lines;
1139
1140             foreach my $key (@$o) {
1141                 my ($line, $sp);
1142                 my $v = $k->{$key};
1143                 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1144
1145                 $sp = " "x(length("$key: "));
1146                 $sp = " "x4 if length($sp) > 16;
1147
1148                 foreach $v (@values) {
1149                     if ($v =~ /\n/) {
1150                         $v =~ s/^/$sp/gm;
1151                         $v =~ s/^$sp//;
1152
1153                         if ($line) {
1154                             push @lines, "$line\n\n";
1155                             $line = "";
1156                         }
1157                         elsif (@lines && $lines[-1] !~ /\n\n$/) {
1158                             $lines[-1] .= "\n";
1159                         }
1160                         push @lines, "$key: $v\n\n";
1161                     }
1162                     elsif ($line &&
1163                            length($line)+length($v)-rindex($line, "\n") >= 70)
1164                     {
1165                         $line .= ",\n$sp$v";
1166                     }
1167                     else {
1168                         $line = $line ? "$line, $v" : "$key: $v";
1169                     }
1170                 }
1171
1172                 $line = "$key:" unless @values;
1173                 if ($line) {
1174                     if ($line =~ /\n/) {
1175                         if (@lines && $lines[-1] !~ /\n\n$/) {
1176                             $lines[-1] .= "\n";
1177                         }
1178                         $line .= "\n";
1179                     }
1180                     push @lines, "$line\n";
1181                 }
1182             }
1183
1184             $text .= join "", @lines;
1185         }
1186         else {
1187             chomp $text;
1188         }
1189         push @text, $text;
1190     }
1191
1192     return join "\n--\n\n", @text;
1193 }
1194
1195 # Configuration.
1196 # --------------
1197
1198 # Returns configuration information from the environment.
1199 sub config_from_env {
1200     my %env;
1201
1202     foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
1203         if (exists $ENV{"RT$k"}) {
1204             $env{lc $k} = $ENV{"RT$k"};
1205         }
1206     }
1207
1208     return %env;
1209 }
1210
1211 # Finds a suitable configuration file and returns information from it.
1212 sub config_from_file {
1213     my ($rc) = @_;
1214
1215     if ($rc =~ m#^/#) {
1216         # We'll use an absolute path if we were given one.
1217         return parse_config_file($rc);
1218     }
1219     else {
1220         # Otherwise we'll use the first file we can find in the current
1221         # directory, or in one of its (increasingly distant) ancestors.
1222
1223         my @dirs = split /\//, cwd;
1224         while (@dirs) {
1225             my $file = join('/', @dirs, $rc);
1226             if (-r $file) {
1227                 return parse_config_file($file);
1228             }
1229
1230             # Remove the last directory component each time.
1231             pop @dirs;
1232         }
1233
1234         # Still nothing? We'll fall back to some likely defaults.
1235         for ("$HOME/$rc", "/etc/rt.conf") {
1236             return parse_config_file($_) if (-r $_);
1237         }
1238     }
1239
1240     return ();
1241 }
1242
1243 # Makes a hash of the specified configuration file.
1244 sub parse_config_file {
1245     my %cfg;
1246     my ($file) = @_;
1247     local $_; # $_ may be aliased to a constant, from line 1163
1248
1249     open(CFG, $file) && do {
1250         while (<CFG>) {
1251             chomp;
1252             next if (/^#/ || /^\s*$/);
1253
1254             if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
1255                 $cfg{$1} = $2;
1256             }
1257             else {
1258                 die "rt: $file:$.: unknown configuration directive.\n";
1259             }
1260         }
1261     };
1262
1263     return %cfg;
1264 }
1265
1266 # Helper functions.
1267 # -----------------
1268
1269 sub whine {
1270     my $sub = (caller(1))[3];
1271     $sub =~ s/^main:://;
1272     warn "rt: $sub: @_\n";
1273     return;
1274 }
1275
1276 sub read_passwd {
1277     eval 'require Term::ReadKey';
1278     if ($@) {
1279         die "No password specified (and Term::ReadKey not installed).\n";
1280     }
1281
1282     print "Password: ";
1283     Term::ReadKey::ReadMode('noecho');
1284     chomp(my $passwd = Term::ReadKey::ReadLine(0));
1285     Term::ReadKey::ReadMode('restore');
1286     print "\n";
1287
1288     return $passwd;
1289 }
1290
1291 sub vi {
1292     my ($text) = @_;
1293     my $file = "/tmp/rt.form.$$";
1294     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1295
1296     local *F;
1297     local $/ = undef;
1298
1299     open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1300     system($editor, $file) && die "Couldn't run $editor.\n";
1301     open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1302     unlink($file);
1303
1304     return $text;
1305 }
1306
1307 # Add a value to a (possibly multi-valued) hash key.
1308 sub vpush {
1309     my ($hash, $key, $val) = @_;
1310     my @val = ref $val eq 'ARRAY' ? @$val : $val;
1311
1312     if (exists $hash->{$key}) {
1313         unless (ref $hash->{$key} eq 'ARRAY') {
1314             my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1315             $hash->{$key} = \@v;
1316         }
1317         push @{ $hash->{$key} }, @val;
1318     }
1319     else {
1320         $hash->{$key} = $val;
1321     }
1322 }
1323
1324 # "Normalise" a hash key that's known to be multi-valued.
1325 sub vsplit {
1326     my ($val) = @_;
1327     my ($word, @words);
1328     my @values = ref $val eq 'ARRAY' ? @$val : $val;
1329
1330     foreach my $line (map {split /\n/} @values) {
1331         # XXX: This should become a real parser, Ã  la Text::ParseWords.
1332         $line =~ s/^\s+//;
1333         $line =~ s/\s+$//;
1334         push @words, split /\s*,\s*/, $line;
1335     }
1336
1337     return \@words;
1338 }
1339
1340 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1341 # change both functions at once
1342 sub expand_list {
1343     my ($list) = @_;
1344
1345     my @elts;
1346     foreach (split /,/, $list) {
1347         push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1348     }
1349
1350     return map $_->[0], # schwartzian transform
1351         sort {
1352             defined $a->[1] && defined $b->[1]?
1353                 # both numbers
1354                 $a->[1] <=> $b->[1]
1355                 :!defined $a->[1] && !defined $b->[1]?
1356                     # both letters
1357                     $a->[2] cmp $b->[2]
1358                     # mix, number must be first
1359                     :defined $a->[1]? -1: 1
1360         }
1361         map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1362         @elts;
1363 }
1364
1365 sub get_type_argument {
1366     my $type;
1367
1368     if (@ARGV) {
1369         $type = shift @ARGV;
1370         unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1371             # We want whine to mention our caller, not us.
1372             @_ = ("Invalid type '$type' specified.");
1373             goto &whine;
1374         }
1375     }
1376     else {
1377         @_ = ("No type argument specified with -t.");
1378         goto &whine;
1379     }
1380
1381     $type =~ s/s$//; # "Plural". Ugh.
1382     return $type;
1383 }
1384
1385 sub get_var_argument {
1386     my ($data) = @_;
1387
1388     if (@ARGV) {
1389         my $kv = shift @ARGV;
1390         if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1391             push @{ $data->{$k} }, $v;
1392         }
1393         else {
1394             @_ = ("Invalid variable specification: '$kv'.");
1395             goto &whine;
1396         }
1397     }
1398     else {
1399         @_ = ("No variable argument specified with -S.");
1400         goto &whine;
1401     }
1402 }
1403
1404 sub is_object_spec {
1405     my ($spec, $type) = @_;
1406
1407     $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1408     return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1409     return;
1410 }
1411
1412 sub suggest_help {
1413     my ($action, $type) = @_;
1414
1415     print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1416     print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1417 }
1418
1419 __DATA__
1420
1421 Title: intro
1422 Title: introduction
1423 Text:
1424
1425      ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1426      ** PLEASE REPORT BUGS TO rt-bugs@bestpractical.com **
1427
1428     This is a command-line interface to RT 3.0 or newer
1429
1430     It allows you to interact with an RT server over HTTP, and offers an
1431     interface to RT's functionality that is better-suited to automation
1432     and integration with other tools.
1433
1434     In general, each invocation of this program should specify an action
1435     to perform on one or more objects, and any other arguments required
1436     to complete the desired action.
1437
1438     For more information:
1439
1440         - rt help usage         (syntax information)
1441         - rt help objects       (how to specify objects)
1442         - rt help actions       (a list of possible actions)
1443         - rt help types         (a list of object types)
1444
1445         - rt help config        (configuration details)
1446         - rt help examples      (a few useful examples)
1447         - rt help topics        (a list of help topics)
1448
1449 --
1450
1451 Title: usage
1452 Title: syntax
1453 Text:
1454
1455     Syntax:
1456
1457         rt <action> [options] [arguments]
1458       or
1459         rt shell
1460
1461     Each invocation of this program must specify an action (e.g. "edit",
1462     "create"), options to modify behaviour, and other arguments required
1463     by the specified action. (For example, most actions expect a list of
1464     numeric object IDs to act upon.)
1465
1466     The details of the syntax and arguments for each action are given by
1467     "rt help <action>". Some actions may be referred to by more than one
1468     name ("create" is the same as "new", for example).  
1469
1470     You may also call "rt shell", which will give you an 'rt>' prompt at
1471     which you can issue commands of the form "<action> [options] 
1472     [arguments]".  See "rt help shell" for details.
1473
1474     Objects are identified by a type and an ID (which can be a name or a
1475     number, depending on the type). For some actions, the object type is
1476     implied (you can only comment on tickets); for others, the user must
1477     specify it explicitly. See "rt help objects" for details.
1478
1479     In syntax descriptions, mandatory arguments that must be replaced by
1480     appropriate value are enclosed in <>, and optional arguments are
1481     indicated by [] (for example, <action> and [options] above).
1482
1483     For more information:
1484
1485         - rt help objects       (how to specify objects)
1486         - rt help actions       (a list of actions)
1487         - rt help types         (a list of object types)
1488         - rt help shell         (how to use the shell)
1489
1490 --
1491
1492 Title: conf
1493 Title: config
1494 Title: configuration
1495 Text:
1496
1497     This program has two major sources of configuration information: its
1498     configuration files, and the environment.
1499
1500     The program looks for configuration directives in a file named .rtrc
1501     (or $RTCONFIG; see below) in the current directory, and then in more
1502     distant ancestors, until it reaches /. If no suitable configuration
1503     files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1504
1505     Configuration directives:
1506
1507         The following directives may occur, one per line:
1508
1509         - server <URL>          URL to RT server.
1510         - user <username>       RT username.
1511         - passwd <passwd>       RT user's password.
1512         - query <RT Query>      Default RT Query for list action
1513         - orderby <order>       Default RT order for list action
1514
1515         Blank and #-commented lines are ignored.
1516
1517     Environment variables:
1518
1519         The following environment variables override any corresponding
1520         values defined in configuration files:
1521
1522         - RTUSER
1523         - RTPASSWD
1524         - RTSERVER
1525         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
1526         - RTCONFIG      Specifies a name other than ".rtrc" for the
1527                         configuration file.
1528         - RTQUERY       Default RT Query for rt list
1529         - RTORDERBY     Default order for rt list
1530
1531 --
1532
1533 Title: objects
1534 Text:
1535
1536     Syntax:
1537
1538         <type>/<id>[/<attributes>]
1539
1540     Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1541     ID. Some types of objects can also be identified by name (like users
1542     and queues). Furthermore, objects may have named attributes (such as
1543     "ticket/1/history").
1544
1545     An object specification is like a path in a virtual filesystem, with
1546     object types as top-level directories, object IDs as subdirectories,
1547     and named attributes as further subdirectories.
1548
1549     A comma-separated list of names, numeric IDs, or numeric ranges can
1550     be used to specify more than one object of the same type. Note that
1551     the list must be a single argument (i.e., no spaces). For example,
1552     "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1553     can also be written as "user/ams,root,1,2,3,5,7,8-10".
1554     
1555     Examples:
1556
1557         ticket/1
1558         ticket/1/attachments
1559         ticket/1/attachments/3
1560         ticket/1/attachments/3/content
1561         ticket/1-3/links
1562         ticket/1-3,5-7/history
1563
1564         user/ams
1565         user/ams/rights
1566         user/ams,rai,1/rights
1567
1568     For more information:
1569
1570         - rt help <action>      (action-specific details)
1571         - rt help <type>        (type-specific details)
1572
1573 --
1574
1575 Title: actions
1576 Title: commands
1577 Text:
1578
1579     You can currently perform the following actions on all objects:
1580
1581         - list          (list objects matching some condition)
1582         - show          (display object details)
1583         - edit          (edit object details)
1584         - create        (create a new object)
1585
1586     Each type may define actions specific to itself; these are listed in
1587     the help item about that type.
1588
1589     For more information:
1590
1591         - rt help <action>      (action-specific details)
1592         - rt help types         (a list of possible types)
1593
1594 --
1595
1596 Title: types
1597 Text:
1598
1599     You can currently operate on the following types of objects:
1600
1601         - tickets
1602         - users
1603         - groups
1604         - queues
1605
1606     For more information:
1607
1608         - rt help <type>        (type-specific details)
1609         - rt help objects       (how to specify objects)
1610         - rt help actions       (a list of possible actions)
1611
1612 --
1613
1614 Title: ticket
1615 Text:
1616
1617     Tickets are identified by a numeric ID.
1618
1619     The following generic operations may be performed upon tickets:
1620
1621         - list
1622         - show
1623         - edit
1624         - create
1625
1626     In addition, the following ticket-specific actions exist:
1627
1628         - link
1629         - merge
1630         - comment
1631         - correspond
1632
1633     Attributes:
1634
1635         The following attributes can be used with "rt show" or "rt edit"
1636         to retrieve or edit other information associated with tickets:
1637
1638         links                      A ticket's relationships with others.
1639         history                    All of a ticket's transactions.
1640         history/type/<type>        Only a particular type of transaction.
1641         history/id/<id>            Only the transaction of the specified id.
1642         attachments                A list of attachments.
1643         attachments/<id>           The metadata for an individual attachment.
1644         attachments/<id>/content   The content of an individual attachment.
1645
1646 --
1647
1648 Title: user
1649 Title: group
1650 Text:
1651
1652     Users and groups are identified by name or numeric ID.
1653
1654     The following generic operations may be performed upon them:
1655
1656         - list
1657         - show
1658         - edit
1659         - create
1660
1661     In addition, the following type-specific actions exist:
1662
1663         - grant
1664         - revoke
1665
1666     Attributes:
1667
1668         The following attributes can be used with "rt show" or "rt edit"
1669         to retrieve or edit other information associated with users and
1670         groups:
1671
1672         rights                  Global rights granted to this user.
1673         rights/<queue>          Queue rights for this user.
1674
1675 --
1676
1677 Title: queue
1678 Text:
1679
1680     Queues are identified by name or numeric ID.
1681
1682     Currently, they can be subjected to the following actions:
1683
1684         - show
1685         - edit
1686         - create
1687
1688 --
1689
1690 Title: logout
1691 Text:
1692
1693     Syntax:
1694
1695         rt logout
1696
1697     Terminates the currently established login session. You will need to
1698     provide authentication credentials before you can continue using the
1699     server. (See "rt help config" for details about authentication.)
1700
1701 --
1702
1703 Title: ls
1704 Title: list
1705 Title: search
1706 Text:
1707
1708     Syntax:
1709
1710         rt <ls|list|search> [options] "query string"
1711
1712     Displays a list of objects matching the specified conditions.
1713     ("ls", "list", and "search" are synonyms.)
1714
1715     Conditions are expressed in the SQL-like syntax used internally by
1716     RT3. (For more information, see "rt help query".) The query string
1717     must be supplied as one argument.
1718
1719     (Right now, the server doesn't support listing anything but tickets.
1720     Other types will be supported in future; this client will be able to
1721     take advantage of that support without any changes.)
1722
1723     Options:
1724
1725         The following options control how much information is displayed
1726         about each matching object:
1727
1728         -i      Numeric IDs only. (Useful for |rt edit -; see examples.)
1729         -s      Short description.
1730         -l      Longer description.
1731
1732         In addition,
1733         
1734         -o +/-<field>   Orders the returned list by the specified field.
1735         -S var=val      Submits the specified variable with the request.
1736         -t type         Specifies the type of object to look for. (The
1737                         default is "ticket".)
1738
1739     Examples:
1740
1741         rt ls "Priority > 5 and Status='new'"
1742         rt ls -o +Subject "Priority > 5 and Status='new'"
1743         rt ls -o -Created "Priority > 5 and Status='new'"
1744         rt ls -i "Priority > 5"|rt edit - set status=resolved
1745         rt ls -t ticket "Subject like '[PATCH]%'"
1746
1747 --
1748
1749 Title: show
1750 Text:
1751
1752     Syntax:
1753
1754         rt show [options] <object-ids>
1755
1756     Displays details of the specified objects.
1757
1758     For some types, object information is further classified into named
1759     attributes (for example, "1-3/links" is a valid ticket specification
1760     that refers to the links for tickets 1-3). Consult "rt help <type>"
1761     and "rt help objects" for further details.
1762
1763     This command writes a set of forms representing the requested object
1764     data to STDOUT.
1765
1766     Options:
1767
1768         -               Read IDs from STDIN instead of the command-line.
1769         -t type         Specifies object type.
1770         -f a,b,c        Restrict the display to the specified fields.
1771         -S var=val      Submits the specified variable with the request.
1772         -v              Verbose display
1773     Examples:
1774
1775         rt show -t ticket -f id,subject,status 1-3
1776         rt show ticket/3/attachments/29
1777         rt show ticket/3/attachments/29/content
1778         rt show ticket/1-3/links
1779         rt show ticket/3/history
1780         rt show -v ticket/3/history
1781         rt show -t user 2
1782
1783 --
1784
1785 Title: new
1786 Title: edit
1787 Title: create
1788 Text:
1789
1790     Syntax:
1791
1792         rt edit [options] <object-ids> set field=value [field=value] ...
1793                                        add field=value [field=value] ...
1794                                        del field=value [field=value] ...
1795
1796     Edits information corresponding to the specified objects.
1797
1798     If, instead of "edit", an action of "new" or "create" is specified,
1799     then a new object is created. In this case, no numeric object IDs
1800     may be specified, but the syntax and behaviour remain otherwise
1801     unchanged.
1802
1803     This command typically starts an editor to allow you to edit object
1804     data in a form for submission. If you specified enough information
1805     on the command-line, however, it will make the submission directly.
1806
1807     The command line may specify field-values in three different ways.
1808     "set" sets the named field to the given value, "add" adds a value
1809     to a multi-valued field, and "del" deletes the corresponding value.
1810     Each "field=value" specification must be given as a single argument.
1811
1812     For some types, object information is further classified into named
1813     attributes (for example, "1-3/links" is a valid ticket specification
1814     that refers to the links for tickets 1-3). These attributes may also
1815     be edited. Consult "rt help <type>" and "rt help object" for further
1816     details.
1817
1818     Options:
1819
1820         -       Read numeric IDs from STDIN instead of the command-line.
1821                 (Useful with rt ls ... | rt edit -; see examples below.)
1822         -i      Read a completed form from STDIN before submitting.
1823         -o      Dump the completed form to STDOUT instead of submitting.
1824         -e      Allows you to edit the form even if the command-line has
1825                 enough information to make a submission directly.
1826         -S var=val
1827                 Submits the specified variable with the request.
1828         -t type Specifies object type.
1829
1830     Examples:
1831
1832         # Interactive (starts $EDITOR with a form).
1833         rt edit ticket/3
1834         rt create -t ticket
1835
1836         # Non-interactive.
1837         rt edit ticket/1-3 add cc=foo@example.com set priority=3
1838         rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1839         rt edit ticket/4 set priority=3 owner=bar@example.com \
1840                          add cc=foo@example.com bcc=quux@example.net
1841         rt create -t ticket set subject='new ticket' priority=10 \
1842                             add cc=foo@example.com
1843
1844 --
1845
1846 Title: comment
1847 Title: correspond
1848 Text:
1849
1850     Syntax:
1851
1852         rt <comment|correspond> [options] <ticket-id>
1853
1854     Adds a comment (or correspondence) to the specified ticket (the only
1855     difference being that comments aren't sent to the requestors.)
1856
1857     This command will typically start an editor and allow you to type a
1858     comment into a form. If, however, you specified all the necessary
1859     information on the command line, it submits the comment directly.
1860
1861     (See "rt help forms" for more information about forms.)
1862
1863     Options:
1864
1865         -m <text>       Specify comment text.
1866         -a <file>       Attach a file to the comment. (May be used more
1867                         than once to attach multiple files.)
1868         -c <addrs>      A comma-separated list of Cc addresses.
1869         -b <addrs>      A comma-separated list of Bcc addresses.
1870         -w <time>       Specify the time spent working on this ticket.
1871         -e              Starts an editor before the submission, even if
1872                         arguments from the command line were sufficient.
1873
1874     Examples:
1875
1876         rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
1877
1878 --
1879
1880 Title: merge
1881 Text:
1882
1883     Syntax:
1884
1885         rt merge <from-id> <to-id>
1886
1887     Merges the first ticket specified into the second ticket specified.
1888
1889 --
1890
1891 Title: link
1892 Text:
1893
1894     Syntax:
1895
1896         rt link [-d] <id-A> <link> <id-B>
1897
1898     Creates (or, with -d, deletes) a link between the specified tickets.
1899     The link can (irrespective of case) be any of:
1900
1901         DependsOn/DependedOnBy:     A depends upon B (or vice versa).
1902         RefersTo/ReferredToBy:      A refers to B (or vice versa).
1903         MemberOf/HasMember:         A is a member of B (or vice versa).
1904
1905     To view a ticket's links, use "rt show ticket/3/links". (See
1906     "rt help ticket" and "rt help show".)
1907
1908     Options:
1909
1910         -d      Deletes the specified link.
1911
1912     Examples:
1913
1914         rt link 2 dependson 3
1915         rt link -d 4 referredtoby 6     # 6 no longer refers to 4
1916
1917 --
1918
1919 Title: grant
1920 Title: revoke
1921 Text:
1922
1923 --
1924
1925 Title: query
1926 Text:
1927
1928     RT3 uses an SQL-like syntax to specify object selection constraints.
1929     See the <RT:...> documentation for details.
1930     
1931     (XXX: I'm going to have to write it, aren't I?)
1932
1933 --
1934
1935 Title: form
1936 Title: forms
1937 Text:
1938
1939     This program uses RFC822 header-style forms to represent object data
1940     in a form that's suitable for processing both by humans and scripts.
1941
1942     A form is a set of (field, value) specifications, with some initial
1943     commented text and interspersed blank lines allowed for convenience.
1944     Field names may appear more than once in a form; a comma-separated
1945     list of multiple field values may also be specified directly.
1946     
1947     Field values can be wrapped as in RFC822, with leading whitespace.
1948     The longest sequence of leading whitespace common to all the lines
1949     is removed (preserving further indentation). There is no limit on
1950     the length of a value.
1951
1952     Multiple forms are separated by a line containing only "--\n".
1953
1954     (XXX: A more detailed specification will be provided soon. For now,
1955     the server-side syntax checking will suffice.)
1956
1957 --
1958
1959 Title: topics
1960 Text:
1961
1962     Syntax:
1963
1964         rt help <topic>
1965
1966     Get help on any of the following subjects:
1967
1968         - tickets, users, groups, queues.
1969         - show, edit, ls/list/search, new/create.
1970
1971         - query                                 (search query syntax)
1972         - forms                                 (form specification)
1973
1974         - objects                               (how to specify objects)
1975         - types                                 (a list of object types)
1976         - actions/commands                      (a list of actions)
1977         - usage/syntax                          (syntax details)
1978         - conf/config/configuration             (configuration details)
1979         - examples                              (a few useful examples)
1980
1981 --
1982
1983 Title: example
1984 Title: examples
1985 Text:
1986
1987     This section will be filled in with useful examples, once it becomes
1988     more clear what examples may be useful.
1989
1990     For the moment, please consult examples provided with each action.
1991
1992 --
1993
1994 Title: shell
1995 Text:
1996
1997     Syntax:
1998
1999         rt shell
2000
2001     Opens an interactive shell, at which you can issue commands of 
2002     the form "<action> [options] [arguments]".
2003
2004     To exit the shell, type "quit" or "exit".
2005
2006     Commands can be given at the shell in the same form as they would 
2007     be given at the command line without the leading 'rt' invocation.
2008
2009     Example:
2010         $ rt shell
2011         rt> create -t ticket set subject='new' add cc=foo@example.com
2012         # Ticket 8 created.
2013         rt> quit
2014         $
2015
2016 --
2017
2018 Title: take
2019 Title: untake
2020 Title: steal
2021 Text:
2022
2023     Syntax:
2024
2025         rt <take|untake|steal> <ticket-id>
2026
2027     Sets the owner of the specified ticket to the current user, 
2028     assuming said user has the bits to do so, or releases the 
2029     ticket.  
2030     
2031     'Take' is used on tickets which are not currently owned 
2032     (Owner: Nobody), 'steal' is used on tickets which *are* 
2033     currently owned, and 'untake' is used to "release" a ticket 
2034     (reset its Owner to Nobody).  'Take' cannot be used on
2035     tickets which are currently owned.
2036
2037     Example:
2038         alice$ rt create -t ticket set subject="New ticket"
2039         # Ticket 7 created.
2040         alice$ rt take 7
2041         # Owner changed from Nobody to alice
2042         alice$ su bob
2043         bob$ rt steal 7
2044         # Owner changed from alice to bob
2045         bob$ rt untake 7
2046         # Owner changed from bob to Nobody
2047
2048 --
2049
2050 Title: quit
2051 Title: exit
2052 Text:
2053
2054     Use "quit" or "exit" to leave the shell.  Only valid within shell 
2055     mode.
2056
2057     Example:
2058         $ rt shell
2059         rt> quit
2060         $