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