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