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