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