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