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