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