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