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