import rt 3.8.10
[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-2011 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
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", "@LOCAL_ETC_PATH@/rt.conf", "/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) or die "$file: $!\n"; print F $text; close(F);
1483     system($editor, $file) && die "Couldn't run $editor.\n";
1484     open(F, '<', $file) or 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 /\s*,\s*/, $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 /\s*,\s*/, $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 /\s*,\s*/, $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 /\s*,\s*/, $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 /\s*,\s*/, $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 a command-line interface to RT 3.0 or newer.
1755
1756     It allows you to interact with an RT server over HTTP, and offers an
1757     interface to RT's functionality that is better-suited to automation
1758     and integration with other tools.
1759
1760     In general, each invocation of this program should specify an action
1761     to perform on one or more objects, and any other arguments required
1762     to complete the desired action.
1763
1764     For more information:
1765
1766         - rt help usage         (syntax information)
1767         - rt help objects       (how to specify objects)
1768         - rt help actions       (a list of possible actions)
1769         - rt help types         (a list of object types)
1770
1771         - rt help config        (configuration details)
1772         - rt help examples      (a few useful examples)
1773         - rt help topics        (a list of help topics)
1774
1775 --
1776
1777 Title: usage
1778 Title: syntax
1779 Text:
1780
1781     Syntax:
1782
1783         rt <action> [options] [arguments]
1784       or
1785         rt shell
1786
1787     Each invocation of this program must specify an action (e.g. "edit",
1788     "create"), options to modify behaviour, and other arguments required
1789     by the specified action. (For example, most actions expect a list of
1790     numeric object IDs to act upon.)
1791
1792     The details of the syntax and arguments for each action are given by
1793     "rt help <action>". Some actions may be referred to by more than one
1794     name ("create" is the same as "new", for example).  
1795
1796     You may also call "rt shell", which will give you an 'rt>' prompt at
1797     which you can issue commands of the form "<action> [options] 
1798     [arguments]".  See "rt help shell" for details.
1799
1800     Objects are identified by a type and an ID (which can be a name or a
1801     number, depending on the type). For some actions, the object type is
1802     implied (you can only comment on tickets); for others, the user must
1803     specify it explicitly. See "rt help objects" for details.
1804
1805     In syntax descriptions, mandatory arguments that must be replaced by
1806     appropriate value are enclosed in <>, and optional arguments are
1807     indicated by [] (for example, <action> and [options] above).
1808
1809     For more information:
1810
1811         - rt help objects       (how to specify objects)
1812         - rt help actions       (a list of actions)
1813         - rt help types         (a list of object types)
1814         - rt help shell         (how to use the shell)
1815
1816 --
1817
1818 Title: conf
1819 Title: config
1820 Title: configuration
1821 Text:
1822
1823     This program has two major sources of configuration information: its
1824     configuration files, and the environment.
1825
1826     The program looks for configuration directives in a file named .rtrc
1827     (or $RTCONFIG; see below) in the current directory, and then in more
1828     distant ancestors, until it reaches /. If no suitable configuration
1829     files are found, it will also check for ~/.rtrc, @LOCAL_ETC_PATH@/rt.conf
1830     and /etc/rt.conf.
1831
1832     Configuration directives:
1833
1834         The following directives may occur, one per line:
1835
1836         - server <URL>          URL to RT server.
1837         - user <username>       RT username.
1838         - passwd <passwd>       RT user's password.
1839         - query <RT Query>      Default RT Query for list action
1840         - orderby <order>       Default RT order for list action
1841         - queue <queuename>     Default RT Queue for list action
1842         - externalauth <0|1>    Use HTTP Basic authentication
1843          explicitely setting externalauth to 0 inhibits also GSSAPI based
1844          authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1845
1846         Blank and #-commented lines are ignored.
1847
1848     Sample configuration file contents:
1849
1850          server  https://rt.somewhere.com/
1851          # more than one queue can be given (by adding a query expression)
1852          queue helpdesk or queue=support
1853          query Status != resolved and Owner=myaccount
1854
1855
1856     Environment variables:
1857
1858         The following environment variables override any corresponding
1859         values defined in configuration files:
1860
1861         - RTUSER
1862         - RTPASSWD
1863         - RTEXTERNALAUTH
1864         - RTSERVER
1865         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
1866         - RTCONFIG      Specifies a name other than ".rtrc" for the
1867                         configuration file.
1868         - RTQUERY       Default RT Query for rt list
1869         - RTORDERBY     Default order for rt list
1870
1871 --
1872
1873 Title: objects
1874 Text:
1875
1876     Syntax:
1877
1878         <type>/<id>[/<attributes>]
1879
1880     Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1881     ID. Some types of objects can also be identified by name (like users
1882     and queues). Furthermore, objects may have named attributes (such as
1883     "ticket/1/history").
1884
1885     An object specification is like a path in a virtual filesystem, with
1886     object types as top-level directories, object IDs as subdirectories,
1887     and named attributes as further subdirectories.
1888
1889     A comma-separated list of names, numeric IDs, or numeric ranges can
1890     be used to specify more than one object of the same type. Note that
1891     the list must be a single argument (i.e., no spaces). For example,
1892     "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1893     can also be written as "user/ams,root,1,2,3,5,7,8-10".
1894     
1895     If just a number is given as object specification it will be
1896     interpreted as ticket/<number>
1897
1898     Examples:
1899
1900         1                   # the same as ticket/1
1901         ticket/1
1902         ticket/1/attachments
1903         ticket/1/attachments/3
1904         ticket/1/attachments/3/content
1905         ticket/1-3/links
1906         ticket/1-3,5-7/history
1907
1908         user/ams
1909         user/ams/rights
1910         user/ams,rai,1/rights
1911
1912     For more information:
1913
1914         - rt help <action>      (action-specific details)
1915         - rt help <type>        (type-specific details)
1916
1917 --
1918
1919 Title: actions
1920 Title: commands
1921 Text:
1922
1923     You can currently perform the following actions on all objects:
1924
1925         - list          (list objects matching some condition)
1926         - show          (display object details)
1927         - edit          (edit object details)
1928         - create        (create a new object)
1929
1930     Each type may define actions specific to itself; these are listed in
1931     the help item about that type.
1932
1933     For more information:
1934
1935         - rt help <action>      (action-specific details)
1936         - rt help types         (a list of possible types)
1937
1938     The following actions on tickets are also possible:
1939
1940         - comment       Add comments to a ticket
1941         - correspond    Add comments to a ticket
1942         - merge         Merge one ticket into another
1943         - link          Link one ticket to another
1944         - take          Take a ticket (steal and untake are possible as well)
1945
1946     For several edit set subcommands that are frequently used abbreviations
1947     have been introduced. These abbreviations are:
1948
1949         - delete or del  delete a ticket           (edit set status=deleted)
1950         - resolve or res resolve a ticket          (edit set status=resolved)
1951         - subject        change subject of ticket  (edit set subject=string)
1952         - give           give a ticket to somebody (edit set owner=user)
1953
1954 --
1955
1956 Title: types
1957 Text:
1958
1959     You can currently operate on the following types of objects:
1960
1961         - tickets
1962         - users
1963         - groups
1964         - queues
1965
1966     For more information:
1967
1968         - rt help <type>        (type-specific details)
1969         - rt help objects       (how to specify objects)
1970         - rt help actions       (a list of possible actions)
1971
1972 --
1973
1974 Title: ticket
1975 Text:
1976
1977     Tickets are identified by a numeric ID.
1978
1979     The following generic operations may be performed upon tickets:
1980
1981         - list
1982         - show
1983         - edit
1984         - create
1985
1986     In addition, the following ticket-specific actions exist:
1987
1988         - link
1989         - merge
1990         - comment
1991         - correspond
1992         - take
1993         - steal
1994         - untake
1995         - give
1996         - resolve
1997         - delete
1998         - subject
1999
2000     Attributes:
2001
2002         The following attributes can be used with "rt show" or "rt edit"
2003         to retrieve or edit other information associated with tickets:
2004
2005         links                      A ticket's relationships with others.
2006         history                    All of a ticket's transactions.
2007         history/type/<type>        Only a particular type of transaction.
2008         history/id/<id>            Only the transaction of the specified id.
2009         attachments                A list of attachments.
2010         attachments/<id>           The metadata for an individual attachment.
2011         attachments/<id>/content   The content of an individual attachment.
2012
2013 --
2014
2015 Title: user
2016 Title: group
2017 Text:
2018
2019     Users and groups are identified by name or numeric ID.
2020
2021     The following generic operations may be performed upon them:
2022
2023         - list
2024         - show
2025         - edit
2026         - create
2027
2028     In addition, the following type-specific actions exist:
2029
2030         - grant
2031         - revoke
2032
2033     Attributes:
2034
2035         The following attributes can be used with "rt show" or "rt edit"
2036         to retrieve or edit other information associated with users and
2037         groups:
2038
2039         rights                  Global rights granted to this user.
2040         rights/<queue>          Queue rights for this user.
2041
2042 --
2043
2044 Title: queue
2045 Text:
2046
2047     Queues are identified by name or numeric ID.
2048
2049     Currently, they can be subjected to the following actions:
2050
2051         - show
2052         - edit
2053         - create
2054
2055 --
2056
2057 Title: subject
2058 Text:
2059
2060     Syntax:
2061
2062         rt subject <id> <new subject text>
2063
2064     Change the subject of a ticket whose ticket id is given.
2065
2066 --
2067
2068 Title: give
2069 Text:
2070
2071     Syntax:
2072
2073         rt give <id> <accountname>
2074
2075     Give a ticket whose ticket id is given to another user.
2076
2077 --
2078
2079 Title: steal
2080 Text:
2081
2082         rt steal <id> 
2083
2084     Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2085
2086 --
2087
2088 Title: take
2089 Text:
2090
2091     Syntax:
2092
2093         rt take <id>
2094
2095     Take a ticket whose ticket id is given, i.e. set the owner to myself.
2096
2097 --
2098
2099 Title: untake
2100 Text:
2101
2102     Syntax:
2103
2104         rt untake <id>
2105
2106     Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2107
2108 --
2109
2110 Title: resolve
2111 Title: res
2112 Text:
2113
2114     Syntax:
2115
2116         rt resolve <id>
2117
2118     Resolves a ticket whose ticket id is given.
2119
2120 --
2121
2122 Title: delete
2123 Title: del
2124 Text:
2125
2126     Syntax:
2127
2128         rt delete <id>
2129
2130     Deletes a ticket whose ticket id is given.
2131
2132 --
2133
2134 Title: logout
2135 Text:
2136
2137     Syntax:
2138
2139         rt logout
2140
2141     Terminates the currently established login session. You will need to
2142     provide authentication credentials before you can continue using the
2143     server. (See "rt help config" for details about authentication.)
2144
2145 --
2146
2147 Title: ls
2148 Title: list
2149 Title: search
2150 Text:
2151
2152     Syntax:
2153
2154         rt <ls|list|search> [options] "query string"
2155
2156     Displays a list of objects matching the specified conditions.
2157     ("ls", "list", and "search" are synonyms.)
2158
2159     Conditions are expressed in the SQL-like syntax used internally by
2160     RT3. (For more information, see "rt help query".) The query string
2161     must be supplied as one argument.
2162
2163     (Right now, the server doesn't support listing anything but tickets.
2164     Other types will be supported in future; this client will be able to
2165     take advantage of that support without any changes.)
2166
2167     Options:
2168
2169         The following options control how much information is displayed
2170         about each matching object:
2171
2172         -i             Numeric IDs only. (Useful for |rt edit -; see examples.)
2173         -s             Short description.
2174         -l             Longer description.
2175         -f <field[s]   Display only the fields listed and the ticket id
2176
2177         In addition,
2178         
2179         -o +/-<field>  Orders the returned list by the specified field.
2180         -r             reversed order (useful if a default was given)
2181         -q queue[s]    restricts the query to the queue[s] given
2182                        multiple queues are separated by comma
2183         -S var=val     Submits the specified variable with the request.
2184         -t type        Specifies the type of object to look for. (The
2185                        default is "ticket".)
2186
2187     Examples:
2188
2189         rt ls "Priority > 5 and Status=new"
2190         rt ls -o +Subject "Priority > 5 and Status=new"
2191         rt ls -o -Created "Priority > 5 and Status=new"
2192         rt ls -i "Priority > 5"|rt edit - set status=resolved
2193         rt ls -t ticket "Subject like '[PATCH]%'"
2194         rt ls -q systems
2195         rt ls -f owner,subject
2196
2197 --
2198
2199 Title: show
2200 Text:
2201
2202     Syntax:
2203
2204         rt show [options] <object-ids>
2205
2206     Displays details of the specified objects.
2207
2208     For some types, object information is further classified into named
2209     attributes (for example, "1-3/links" is a valid ticket specification
2210     that refers to the links for tickets 1-3). Consult "rt help <type>"
2211     and "rt help objects" for further details.
2212
2213     If only a number is given it will be interpreted as the objects
2214     ticket/number and ticket/number/history
2215
2216     This command writes a set of forms representing the requested object
2217     data to STDOUT.
2218
2219     Options:
2220
2221         The following options control how much information is displayed
2222         about each matching object:
2223
2224         Without any formatting options prettyprinted output is generated.
2225         Giving any of the two options below reverts to raw output.
2226         -s      Short description (history and attachments only).
2227         -l      Longer description (history and attachments only).
2228
2229         In addition,
2230         -               Read IDs from STDIN instead of the command-line.
2231         -t type         Specifies object type.
2232         -f a,b,c        Restrict the display to the specified fields.
2233         -S var=val      Submits the specified variable with the request.
2234
2235     Examples:
2236
2237         rt show -t ticket -f id,subject,status 1-3
2238         rt show ticket/3/attachments/29
2239         rt show ticket/3/attachments/29/content
2240         rt show ticket/1-3/links
2241         rt show ticket/3/history
2242         rt show -l ticket/3/history
2243         rt show -t user 2
2244         rt show 2
2245
2246 --
2247
2248 Title: new
2249 Title: edit
2250 Title: create
2251 Text:
2252
2253     Syntax:
2254
2255         rt edit [options] <object-ids> set field=value [field=value] ...
2256                                        add field=value [field=value] ...
2257                                        del field=value [field=value] ...
2258
2259     Edits information corresponding to the specified objects.
2260
2261     A purely numeric object id nnn is translated into ticket/nnn
2262
2263     If, instead of "edit", an action of "new" or "create" is specified,
2264     then a new object is created. In this case, no numeric object IDs
2265     may be specified, but the syntax and behaviour remain otherwise
2266     unchanged.
2267
2268     This command typically starts an editor to allow you to edit object
2269     data in a form for submission. If you specified enough information
2270     on the command-line, however, it will make the submission directly.
2271
2272     The command line may specify field-values in three different ways.
2273     "set" sets the named field to the given value, "add" adds a value
2274     to a multi-valued field, and "del" deletes the corresponding value.
2275     Each "field=value" specification must be given as a single argument.
2276
2277     For some types, object information is further classified into named
2278     attributes (for example, "1-3/links" is a valid ticket specification
2279     that refers to the links for tickets 1-3). These attributes may also
2280     be edited. Consult "rt help <type>" and "rt help object" for further
2281     details.
2282
2283     Options:
2284
2285         -       Read numeric IDs from STDIN instead of the command-line.
2286                 (Useful with rt ls ... | rt edit -; see examples below.)
2287         -i      Read a completed form from STDIN before submitting.
2288         -o      Dump the completed form to STDOUT instead of submitting.
2289         -e      Allows you to edit the form even if the command-line has
2290                 enough information to make a submission directly.
2291         -S var=val
2292                 Submits the specified variable with the request.
2293         -t type Specifies object type.
2294
2295     Examples:
2296
2297         # Interactive (starts $EDITOR with a form).
2298         rt edit ticket/3
2299         rt create -t ticket
2300
2301         # Non-interactive.
2302         rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2303         rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2304         rt edit ticket/4 set priority=3 owner=bar@example.com \
2305                          add cc=foo@example.com bcc=quux@example.net
2306         rt create -t ticket set subject='new ticket' priority=10 \
2307                             add cc=foo@example.com
2308
2309 --
2310
2311 Title: comment
2312 Title: correspond
2313 Text:
2314
2315     Syntax:
2316
2317         rt <comment|correspond> [options] <ticket-id>
2318
2319     Adds a comment (or correspondence) to the specified ticket (the only
2320     difference being that comments aren't sent to the requestors.)
2321
2322     This command will typically start an editor and allow you to type a
2323     comment into a form. If, however, you specified all the necessary
2324     information on the command line, it submits the comment directly.
2325
2326     (See "rt help forms" for more information about forms.)
2327
2328     Options:
2329
2330         -m <text>       Specify comment text.
2331         -a <file>       Attach a file to the comment. (May be used more
2332                         than once to attach multiple files.)
2333         -c <addrs>      A comma-separated list of Cc addresses.
2334         -b <addrs>      A comma-separated list of Bcc addresses.
2335         -w <time>       Specify the time spent working on this ticket.
2336         -e              Starts an editor before the submission, even if
2337                         arguments from the command line were sufficient.
2338
2339     Examples:
2340
2341         rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2342
2343 --
2344
2345 Title: merge
2346 Text:
2347
2348     Syntax:
2349
2350         rt merge <from-id> <to-id>
2351
2352     Merges the first ticket specified into the second ticket specified.
2353
2354 --
2355
2356 Title: link
2357 Text:
2358
2359     Syntax:
2360
2361         rt link [-d] <id-A> <link> <id-B>
2362
2363     Creates (or, with -d, deletes) a link between the specified tickets.
2364     The link can (irrespective of case) be any of:
2365
2366         DependsOn/DependedOnBy:     A depends upon B (or vice versa).
2367         RefersTo/ReferredToBy:      A refers to B (or vice versa).
2368         MemberOf/HasMember:         A is a member of B (or vice versa).
2369
2370     To view a ticket's links, use "rt show ticket/3/links". (See
2371     "rt help ticket" and "rt help show".)
2372
2373     Options:
2374
2375         -d      Deletes the specified link.
2376
2377     Examples:
2378
2379         rt link 2 dependson 3
2380         rt link -d 4 referredtoby 6     # 6 no longer refers to 4
2381
2382 --
2383
2384 Title: grant
2385 Title: revoke
2386 Text:
2387
2388 --
2389
2390 Title: query
2391 Text:
2392
2393     RT3 uses an SQL-like syntax to specify object selection constraints.
2394     See the <RT:...> documentation for details.
2395     
2396     (XXX: I'm going to have to write it, aren't I?)
2397
2398     Until it exists here a short description of important constructs:
2399
2400     The two simple forms of query expressions are the constructs
2401     Attribute like Value and
2402     Attribute = Value or Attribute != Value
2403
2404     Whether attributes can be matched using like or using = is built into RT.
2405     The attributes id, Queue, Owner Priority and Status require the = or !=
2406     tests.
2407
2408     If Value is a string it must be quoted and may contain the wildcard
2409     character %. If the string does not contain white space, the quoting
2410     may however be omitted, it will be added automatically when parsing
2411     the input.
2412
2413     Simple query expressions can be combined using and, or and parentheses
2414     can be used to group expressions.
2415
2416     As a special case a standalone string (which would not form a correct
2417     query) is transformed into (Owner='string' or Requestor like 'string%')
2418     and added to the default query, i.e. the query is narrowed down.
2419
2420     If no Queue=name clause is contained in the query, a default clause
2421     Queue=$config{queue} is added.
2422
2423     Examples:
2424     Status!='resolved' and Status!='rejected'
2425     (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2426
2427 --
2428
2429 Title: form
2430 Title: forms
2431 Text:
2432
2433     This program uses RFC822 header-style forms to represent object data
2434     in a form that's suitable for processing both by humans and scripts.
2435
2436     A form is a set of (field, value) specifications, with some initial
2437     commented text and interspersed blank lines allowed for convenience.
2438     Field names may appear more than once in a form; a comma-separated
2439     list of multiple field values may also be specified directly.
2440     
2441     Field values can be wrapped as in RFC822, with leading whitespace.
2442     The longest sequence of leading whitespace common to all the lines
2443     is removed (preserving further indentation). There is no limit on
2444     the length of a value.
2445
2446     Multiple forms are separated by a line containing only "--\n".
2447
2448     (XXX: A more detailed specification will be provided soon. For now,
2449     the server-side syntax checking will suffice.)
2450
2451 --
2452
2453 Title: topics
2454 Text:
2455
2456     Syntax:
2457
2458         rt help <topic>
2459
2460     Get help on any of the following subjects:
2461
2462         - tickets, users, groups, queues.
2463         - show, edit, ls/list/search, new/create.
2464
2465         - query                                 (search query syntax)
2466         - forms                                 (form specification)
2467
2468         - objects                               (how to specify objects)
2469         - types                                 (a list of object types)
2470         - actions/commands                      (a list of actions)
2471         - usage/syntax                          (syntax details)
2472         - conf/config/configuration             (configuration details)
2473         - examples                              (a few useful examples)
2474
2475 --
2476
2477 Title: example
2478 Title: examples
2479 Text:
2480
2481     some useful examples
2482
2483     All the following list requests will be restricted to the default queue.
2484     That can be changed by adding the option -q queuename
2485
2486     List all tickets that are not rejected/resolved
2487         rt ls
2488     List all tickets that are new and do not have an owner
2489         rt ls "status=new and owner=nobody"
2490     List all tickets which I have sent or of which I am the owner
2491         rt ls myaccount
2492     List all attributes for the ticket 6977 (ls -l instead of ls)
2493         rt ls -l 6977
2494     Show the content of ticket 6977
2495         rt show 6977
2496     Show all attributes in the ticket and in the history of the ticket
2497         rt show -l 6977
2498     Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2499         rt comment 6977
2500         This will open an editor and lets you add text (attribute Text:)
2501         Other attributes may be changed as well, but usually don't do that.
2502     Correspond a ticket (like comment, but mail is also sent to requestors)
2503         rt correspond 6977
2504     Edit a ticket (generic change, interactive using the editor)
2505         rt edit 6977
2506     Change the owner of a ticket non interactively
2507         rt edit 6977 set owner=myaccount
2508         or
2509         rt give 6977 account
2510         or
2511         rt take 6977
2512     Change the status of a ticket
2513         rt edit 6977 set status=resolved
2514         or
2515         rt resolve 6977
2516     Change the status of all tickets I own to resolved !!!
2517         rt ls -i owner=myaccount | rt edit - set status=resolved
2518
2519 --
2520
2521 Title: shell
2522 Text:
2523
2524     Syntax:
2525
2526         rt shell
2527
2528     Opens an interactive shell, at which you can issue commands of 
2529     the form "<action> [options] [arguments]".
2530
2531     To exit the shell, type "quit" or "exit".
2532
2533     Commands can be given at the shell in the same form as they would 
2534     be given at the command line without the leading 'rt' invocation.
2535
2536     Example:
2537         $ rt shell
2538         rt> create -t ticket set subject='new' add cc=foo@example.com
2539         # Ticket 8 created.
2540         rt> quit
2541         $
2542
2543 --
2544
2545 Title: take
2546 Title: untake
2547 Title: steal
2548 Text:
2549
2550     Syntax:
2551
2552         rt <take|untake|steal> <ticket-id>
2553
2554     Sets the owner of the specified ticket to the current user, 
2555     assuming said user has the bits to do so, or releases the 
2556     ticket.  
2557     
2558     'Take' is used on tickets which are not currently owned 
2559     (Owner: Nobody), 'steal' is used on tickets which *are* 
2560     currently owned, and 'untake' is used to "release" a ticket 
2561     (reset its Owner to Nobody).  'Take' cannot be used on
2562     tickets which are currently owned.
2563
2564     Example:
2565         alice$ rt create -t ticket set subject="New ticket"
2566         # Ticket 7 created.
2567         alice$ rt take 7
2568         # Owner changed from Nobody to alice
2569         alice$ su bob
2570         bob$ rt steal 7
2571         # Owner changed from alice to bob
2572         bob$ rt untake 7
2573         # Owner changed from bob to Nobody
2574
2575 --
2576
2577 Title: quit
2578 Title: exit
2579 Text:
2580
2581     Use "quit" or "exit" to leave the shell.  Only valid within shell 
2582     mode.
2583
2584     Example:
2585         $ rt shell
2586         rt> quit
2587         $