]> git.uio.no Git - usit-rt.git/blob - bin/rt
Upgrade to 4.0.8 with mod of ExternalAuth + absolute paths to ticket-menu.
[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-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 $_ =~ /\/links/ 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 (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
909             whine "Invalid link '$rel' for type $type specified.";
910             $bad = 1;
911         }
912         %data = (id => $from, rel => $rel, to => $to, del => $del);
913     }
914     else {
915         my $bad = @ARGV < 3 ? "few" : "many";
916         whine "Too $bad arguments specified.";
917         $bad = 1;
918     }
919     return suggest_help("link", $type, $bad) if $bad;
920  
921     my $r = submit("$REST/$type/link", \%data);
922     print $r->content;
923     return 0;
924 }
925
926 # Take/steal a ticket
927 sub take {
928     my ($cmd) = @_;
929     my ($bad, %data) = (0, ());
930
931     my $id;
932
933     # get the ticket id
934     if (@ARGV == 1) {
935         ($id) = @ARGV;
936         unless ($id =~ /^\d+$/) {
937             whine "Invalid ticket ID $id specified.";
938             $bad = 1;
939         }
940         my $form = [
941             "",
942             [ "Ticket", "Action" ],
943             {
944                 Ticket => $id,
945                 Action => $cmd,
946                 Status => '',
947             }
948         ];
949
950         my $text = Form::compose([ $form ]);
951         $data{content} = $text;
952     }
953     else {
954         $bad = @ARGV < 1 ? "few" : "many";
955         whine "Too $bad arguments specified.";
956         $bad = 1;
957     }
958     return suggest_help("take", "ticket", $bad) if $bad;
959
960     my $r = submit("$REST/ticket/$id/take", \%data);
961     print $r->content;
962     return 0;
963 }
964
965 # Grant/revoke a user's rights.
966
967 sub grant {
968     my ($cmd) = @_;
969
970     my $revoke = 0;
971     while (@ARGV) {
972     }
973
974     $revoke = 1 if $cmd->{action} eq 'revoke';
975     return 0;
976 }
977
978 # Client <-> Server communication.
979 # --------------------------------
980 #
981 # This function composes and sends an HTTP request to the RT server, and
982 # interprets the response. It takes a request URI, and optional request
983 # data (a string, or a reference to a set of key-value pairs).
984
985 sub submit {
986     my ($uri, $content) = @_;
987     my ($req, $data);
988     my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
989     my $h = HTTP::Headers->new;
990
991     # Did the caller specify any data to send with the request?
992     $data = [];
993     if (defined $content) {
994         unless (ref $content) {
995             # If it's just a string, make sure LWP handles it properly.
996             # (By pretending that it's a file!)
997             $content = [ content => [undef, "", Content => $content] ];
998         }
999         elsif (ref $content eq 'HASH') {
1000             my @data;
1001             foreach my $k (keys %$content) {
1002                 if (ref $content->{$k} eq 'ARRAY') {
1003                     foreach my $v (@{ $content->{$k} }) {
1004                         push @data, $k, $v;
1005                     }
1006                 }
1007                 else { push @data, $k, $content->{$k} }
1008             }
1009             $content = \@data;
1010         }
1011         $data = $content;
1012     }
1013
1014     # Should we send authentication information to start a new session?
1015     my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1016     (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1017     if ($config{externalauth}) {
1018         $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1019         print "   Password will be sent to $server $how\n",
1020               "   Press CTRL-C now if you do not want to continue\n"
1021             if ! $config{passwd};
1022     } elsif ( $no_strong_auth ) {
1023         if (!defined $session->cookie) {
1024             print "   Strong encryption not available, $no_strong_auth\n",
1025                   "   Password will be sent to $server $how\n",
1026                   "   Press CTRL-C now if you do not want to continue\n"
1027                 if ! $config{passwd};
1028             push @$data, ( user => $config{user} );
1029             push @$data, ( pass => $config{passwd} || read_passwd() );
1030         }
1031     }
1032
1033     # Now, we construct the request.
1034     if (@$data) {
1035         $req = POST($uri, $data, Content_Type => 'form-data');
1036     }
1037     else {
1038         $req = GET($uri);
1039     }
1040     $session->add_cookie_header($req);
1041     if ($config{externalauth}) {
1042         $req->header(%$h);
1043     }
1044
1045     # Then we send the request and parse the response.
1046     DEBUG(3, $req->as_string);
1047     my $res = $ua->request($req);
1048     DEBUG(3, $res->as_string);
1049
1050     if ($res->is_success) {
1051         # The content of the response we get from the RT server consists
1052         # of an HTTP-like status line followed by optional header lines,
1053         # a blank line, and arbitrary text.
1054
1055         my ($head, $text) = split /\n\n/, $res->content, 2;
1056         my ($status, @headers) = split /\n/, $head;
1057         $text =~ s/\n*$/\n/ if ($text);
1058
1059         # "RT/3.0.1 401 Credentials required"
1060         if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1061             warn "rt: Malformed RT response from $config{server}.\n";
1062             warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1063             exit -1;
1064         }
1065
1066         # Our caller can pretend that the server returned a custom HTTP
1067         # response code and message. (Doing that directly is apparently
1068         # not sufficiently portable and uncomplicated.)
1069         $res->code($1);
1070         $res->message($2);
1071         $res->content($text);
1072         $session->update($res) if ($res->is_success || $res->code != 401);
1073
1074         if (!$res->is_success) {
1075             # We can deal with authentication failures ourselves. Either
1076             # we sent invalid credentials, or our session has expired.
1077             if ($res->code == 401) {
1078                 my %d = @$data;
1079                 if (exists $d{user}) {
1080                     warn "rt: Incorrect username or password.\n";
1081                     exit -1;
1082                 }
1083                 elsif ($req->header("Cookie")) {
1084                     # We'll retry the request with credentials, unless
1085                     # we only wanted to logout in the first place.
1086                     $session->delete;
1087                     return submit(@_) unless $uri eq "$REST/logout";
1088                 }
1089             }
1090             # Conflicts should be dealt with by the handler and user.
1091             # For anything else, we just die.
1092             elsif ($res->code != 409) {
1093                 warn "rt: ", $res->content;
1094                 #exit;
1095             }
1096         }
1097     }
1098     else {
1099         warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1100         exit -1;
1101     }
1102
1103     return $res;
1104 }
1105
1106 # Session management.
1107 # -------------------
1108 #
1109 # Maintains a list of active sessions in the ~/.rt_sessions file.
1110 {
1111     package Session;
1112     my ($s, $u);
1113
1114     # Initialises the session cache.
1115     sub new {
1116         my ($class, $file) = @_;
1117         my $self = {
1118             file => $file || "$HOME/.rt_sessions",
1119             sids => { }
1120         };
1121        
1122         # The current session is identified by the currently configured
1123         # server and user.
1124         ($s, $u) = @config{"server", "user"};
1125
1126         bless $self, $class;
1127         $self->load();
1128
1129         return $self;
1130     }
1131
1132     # Returns the current session cookie.
1133     sub cookie {
1134         my ($self) = @_;
1135         my $cookie = $self->{sids}{$s}{$u};
1136         return defined $cookie ? "RT_SID_$cookie" : undef;
1137     }
1138
1139     # Deletes the current session cookie.
1140     sub delete {
1141         my ($self) = @_;
1142         delete $self->{sids}{$s}{$u};
1143     }
1144
1145     # Adds a Cookie header to an outgoing HTTP request.
1146     sub add_cookie_header {
1147         my ($self, $request) = @_;
1148         my $cookie = $self->cookie();
1149
1150         $request->header(Cookie => $cookie) if defined $cookie;
1151     }
1152
1153     # Extracts the Set-Cookie header from an HTTP response, and updates
1154     # session information accordingly.
1155     sub update {
1156         my ($self, $response) = @_;
1157         my $cookie = $response->header("Set-Cookie");
1158
1159         if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1160             $self->{sids}{$s}{$u} = $1;
1161         }
1162     }
1163
1164     # Loads the session cache from the specified file.
1165     sub load {
1166         my ($self, $file) = @_;
1167         $file ||= $self->{file};
1168
1169         open( my $handle, '<', $file ) or return 0;
1170
1171         $self->{file} = $file;
1172         my $sids = $self->{sids} = {};
1173         while (<$handle>) {
1174             chomp;
1175             next if /^$/ || /^#/;
1176             next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1177             my ($server, $user, $cookie) = split / /, $_;
1178             $sids->{$server}{$user} = $cookie;
1179         }
1180         return 1;
1181     }
1182
1183     # Writes the current session cache to the specified file.
1184     sub save {
1185         my ($self, $file) = shift;
1186         $file ||= $self->{file};
1187
1188         open( my $handle, '>', "$file" ) or return 0;
1189
1190         my $sids = $self->{sids};
1191         foreach my $server (keys %$sids) {
1192             foreach my $user (keys %{ $sids->{$server} }) {
1193                 my $sid = $sids->{$server}{$user};
1194                 if (defined $sid) {
1195                     print $handle "$server $user $sid\n";
1196                 }
1197             }
1198         }
1199         close($handle);
1200         chmod 0600, $file;
1201         return 1;
1202     }
1203
1204     sub DESTROY {
1205         my $self = shift;
1206         $self->save;
1207     }
1208 }
1209
1210 # Form handling.
1211 # --------------
1212 #
1213 # Forms are RFC822-style sets of (field, value) specifications with some
1214 # initial comments and interspersed blank lines allowed for convenience.
1215 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1216 #
1217 # Each form is parsed into an array with four elements: commented text
1218 # at the start of the form, an array with the order of keys, a hash with
1219 # key/value pairs, and optional error text if the form syntax was wrong.
1220
1221 # Returns a reference to an array of parsed forms.
1222 sub Form::parse {
1223     my $state = 0;
1224     my @forms = ();
1225     my @lines = split /\n/, $_[0] if $_[0];
1226     my ($c, $o, $k, $e) = ("", [], {}, "");
1227
1228     LINE:
1229     while (@lines) {
1230         my $line = shift @lines;
1231
1232         next LINE if $line eq '';
1233
1234         if ($line eq '--') {
1235             # We reached the end of one form. We'll ignore it if it was
1236             # empty, and store it otherwise, errors and all.
1237             if ($e || $c || @$o) {
1238                 push @forms, [ $c, $o, $k, $e ];
1239                 $c = ""; $o = []; $k = {}; $e = "";
1240             }
1241             $state = 0;
1242         }
1243         elsif ($state != -1) {
1244             if ($state == 0 && $line =~ /^#/) {
1245                 # Read an optional block of comments (only) at the start
1246                 # of the form.
1247                 $state = 1;
1248                 $c = $line;
1249                 while (@lines && $lines[0] =~ /^#/) {
1250                     $c .= "\n".shift @lines;
1251                 }
1252                 $c .= "\n";
1253             }
1254             elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1255                 # Read a field: value specification.
1256                 my $f  = $1;
1257                 my @v  = ($2 || ());
1258
1259                 # Read continuation lines, if any.
1260                 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1261                     push @v, shift @lines;
1262                 }
1263                 pop @v while (@v && $v[-1] eq '');
1264
1265                 # Strip longest common leading indent from text.
1266                 my $ws = "";
1267                 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1268                     $ws = $ls if (!$ws || length($ls) < length($ws));
1269                 }
1270                 s/^$ws// foreach @v;
1271
1272                 push(@$o, $f) unless exists $k->{$f};
1273                 vpush($k, $f, join("\n", @v));
1274
1275                 $state = 1;
1276             }
1277             elsif ($line !~ /^#/) {
1278                 # We've found a syntax error, so we'll reconstruct the
1279                 # form parsed thus far, and add an error marker. (>>)
1280                 $state = -1;
1281                 $e = Form::compose([[ "", $o, $k, "" ]]);
1282                 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1283             }
1284         }
1285         else {
1286             # We saw a syntax error earlier, so we'll accumulate the
1287             # contents of this form until the end.
1288             $e .= "$line\n";
1289         }
1290     }
1291     push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1292
1293     foreach my $l (keys %$k) {
1294         $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1295     }
1296
1297     return \@forms;
1298 }
1299
1300 # Returns text representing a set of forms.
1301 sub Form::compose {
1302     my ($forms) = @_;
1303     my @text;
1304
1305     foreach my $form (@$forms) {
1306         my ($c, $o, $k, $e) = @$form;
1307         my $text = "";
1308
1309         if ($c) {
1310             $c =~ s/\n*$/\n/;
1311             $text = "$c\n";
1312         }
1313         if ($e) {
1314             $text .= $e;
1315         }
1316         elsif ($o) {
1317             my @lines;
1318
1319             foreach my $key (@$o) {
1320                 my ($line, $sp);
1321                 my $v = $k->{$key};
1322                 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1323
1324                 $sp = " "x(length("$key: "));
1325                 $sp = " "x4 if length($sp) > 16;
1326
1327                 foreach $v (@values) {
1328                     if ($v =~ /\n/) {
1329                         $v =~ s/^/$sp/gm;
1330                         $v =~ s/^$sp//;
1331
1332                         if ($line) {
1333                             push @lines, "$line\n\n";
1334                             $line = "";
1335                         }
1336                         elsif (@lines && $lines[-1] !~ /\n\n$/) {
1337                             $lines[-1] .= "\n";
1338                         }
1339                         push @lines, "$key: $v\n\n";
1340                     }
1341                     elsif ($line &&
1342                            length($line)+length($v)-rindex($line, "\n") >= 70)
1343                     {
1344                         $line .= ",\n$sp$v";
1345                     }
1346                     else {
1347                         $line = $line ? "$line,$v" : "$key: $v";
1348                     }
1349                 }
1350
1351                 $line = "$key:" unless @values;
1352                 if ($line) {
1353                     if ($line =~ /\n/) {
1354                         if (@lines && $lines[-1] !~ /\n\n$/) {
1355                             $lines[-1] .= "\n";
1356                         }
1357                         $line .= "\n";
1358                     }
1359                     push @lines, "$line\n";
1360                 }
1361             }
1362
1363             $text .= join "", @lines;
1364         }
1365         else {
1366             chomp $text;
1367         }
1368         push @text, $text;
1369     }
1370
1371     return join "\n--\n\n", @text;
1372 }
1373
1374 # Configuration.
1375 # --------------
1376
1377 # Returns configuration information from the environment.
1378 sub config_from_env {
1379     my %env;
1380
1381     foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1382
1383         if (exists $ENV{"RT$k"}) {
1384             $env{lc $k} = $ENV{"RT$k"};
1385         }
1386     }
1387
1388     return %env;
1389 }
1390
1391 # Finds a suitable configuration file and returns information from it.
1392 sub config_from_file {
1393     my ($rc) = @_;
1394
1395     if ($rc =~ m#^/#) {
1396         # We'll use an absolute path if we were given one.
1397         return parse_config_file($rc);
1398     }
1399     else {
1400         # Otherwise we'll use the first file we can find in the current
1401         # directory, or in one of its (increasingly distant) ancestors.
1402
1403         my @dirs = split /\//, cwd;
1404         while (@dirs) {
1405             my $file = join('/', @dirs, $rc);
1406             if (-r $file) {
1407                 return parse_config_file($file);
1408             }
1409
1410             # Remove the last directory component each time.
1411             pop @dirs;
1412         }
1413
1414         # Still nothing? We'll fall back to some likely defaults.
1415         for ("$HOME/$rc", "local/etc/rt.conf", "/etc/rt.conf") {
1416             return parse_config_file($_) if (-r $_);
1417         }
1418     }
1419
1420     return ();
1421 }
1422
1423 # Makes a hash of the specified configuration file.
1424 sub parse_config_file {
1425     my %cfg;
1426     my ($file) = @_;
1427     local $_; # $_ may be aliased to a constant, from line 1163
1428
1429     open( my $handle, '<', $file ) or return;
1430
1431     while (<$handle>) {
1432         chomp;
1433         next if (/^#/ || /^\s*$/);
1434
1435         if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1436             $cfg{$1} = $2;
1437         }
1438         else {
1439             die "rt: $file:$.: unknown configuration directive.\n";
1440         }
1441     }
1442
1443     return %cfg;
1444 }
1445
1446 # Helper functions.
1447 # -----------------
1448
1449 sub whine {
1450     my $sub = (caller(1))[3];
1451     $sub =~ s/^main:://;
1452     warn "rt: $sub: @_\n";
1453     return 0;
1454 }
1455
1456 sub read_passwd {
1457     eval 'require Term::ReadKey';
1458     if ($@) {
1459         die "No password specified (and Term::ReadKey not installed).\n";
1460     }
1461
1462     print "Password: ";
1463     Term::ReadKey::ReadMode('noecho');
1464     chomp(my $passwd = Term::ReadKey::ReadLine(0));
1465     Term::ReadKey::ReadMode('restore');
1466     print "\n";
1467
1468     return $passwd;
1469 }
1470
1471 sub vi {
1472     my ($text) = @_;
1473     my $file = "/tmp/rt.form.$$";
1474     my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1475
1476     local $/ = undef;
1477
1478     open( my $handle, '>', $file ) or die "$file: $!\n";
1479     print $handle $text;
1480     close($handle);
1481
1482     system($editor, $file) && die "Couldn't run $editor.\n";
1483
1484     open( $handle, '<', $file ) or die "$file: $!\n";
1485     $text = <$handle>;
1486     close($handle);
1487
1488     unlink($file);
1489
1490     return $text;
1491 }
1492
1493 # Add a value to a (possibly multi-valued) hash key.
1494 sub vpush {
1495     my ($hash, $key, $val) = @_;
1496     my @val = ref $val eq 'ARRAY' ? @$val : $val;
1497
1498     if (exists $hash->{$key}) {
1499         unless (ref $hash->{$key} eq 'ARRAY') {
1500             my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1501             $hash->{$key} = \@v;
1502         }
1503         push @{ $hash->{$key} }, @val;
1504     }
1505     else {
1506         $hash->{$key} = $val;
1507     }
1508 }
1509
1510 # "Normalise" a hash key that's known to be multi-valued.
1511 sub vsplit {
1512     my ($val) = @_;
1513     my ($word, @words);
1514     my @values = ref $val eq 'ARRAY' ? @$val : $val;
1515
1516     foreach my $line (map {split /\n/} @values) {
1517         # XXX: This should become a real parser, Ã  la Text::ParseWords.
1518         $line =~ s/^\s+//;
1519         $line =~ s/\s+$//;
1520         my ( $a, $b ) = split /\s*,\s*/, $line, 2;
1521
1522         while ($a) {
1523             no warnings 'uninitialized';
1524             if ( $a =~ /^'/ ) {
1525                 my $s = $a;
1526                 while ( $a !~ /'$/ || (   $a !~ /(\\\\)+'$/
1527                             && $a =~ /(\\)+'$/ )) {
1528                     ( $a, $b ) = split /\s*,\s*/, $b, 2;
1529                     $s .= ',' . $a;
1530                 }
1531                 push @words, $s;
1532             }
1533             elsif ( $a =~ /^q{/ ) {
1534                 my $s = $a;
1535                 while ( $a !~ /}$/ ) {
1536                     ( $a, $b ) =
1537                       split /\s*,\s*/, $b, 2;
1538                     $s .= ',' . $a;
1539                 }
1540                 $s =~ s/^q{/'/;
1541                 $s =~ s/}/'/;
1542                 push @words, $s;
1543             }
1544             else {
1545                 push @words, $a;
1546             }
1547             ( $a, $b ) = split /\s*,\s*/, $b, 2;
1548         }
1549
1550
1551     }
1552
1553     return \@words;
1554 }
1555
1556 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1557 # change both functions at once
1558 sub expand_list {
1559     my ($list) = @_;
1560
1561     my @elts;
1562     foreach (split /\s*,\s*/, $list) {
1563         push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1564     }
1565
1566     return map $_->[0], # schwartzian transform
1567         sort {
1568             defined $a->[1] && defined $b->[1]?
1569                 # both numbers
1570                 $a->[1] <=> $b->[1]
1571                 :!defined $a->[1] && !defined $b->[1]?
1572                     # both letters
1573                     $a->[2] cmp $b->[2]
1574                     # mix, number must be first
1575                     :defined $a->[1]? -1: 1
1576         }
1577         map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1578         @elts;
1579 }
1580
1581 sub get_type_argument {
1582     my $type;
1583
1584     if (@ARGV) {
1585         $type = shift @ARGV;
1586         unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1587             # We want whine to mention our caller, not us.
1588             @_ = ("Invalid type '$type' specified.");
1589             goto &whine;
1590         }
1591     }
1592     else {
1593         @_ = ("No type argument specified with -t.");
1594         goto &whine;
1595     }
1596
1597     $type =~ s/s$//; # "Plural". Ugh.
1598     return $type;
1599 }
1600
1601 sub get_var_argument {
1602     my ($data) = @_;
1603
1604     if (@ARGV) {
1605         my $kv = shift @ARGV;
1606         if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1607             push @{ $data->{$k} }, $v;
1608         }
1609         else {
1610             @_ = ("Invalid variable specification: '$kv'.");
1611             goto &whine;
1612         }
1613     }
1614     else {
1615         @_ = ("No variable argument specified with -S.");
1616         goto &whine;
1617     }
1618 }
1619
1620 sub is_object_spec {
1621     my ($spec, $type) = @_;
1622
1623     $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1624     return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1625     return 0;
1626 }
1627
1628 sub suggest_help {
1629     my ($action, $type, $rv) = @_;
1630
1631     print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1632     print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1633     return $rv;
1634 }
1635
1636 sub str2time {
1637     # simplified procedure for parsing date, avoid loading Date::Parse
1638     my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May =>  4, Jun =>  5,
1639                  Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1640     $_ = shift;
1641     my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1642     if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1643         ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1644         $mon = $month{$monstr} if exists $month{$monstr};
1645     } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1646         ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1647     }
1648     if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1649         return timelocal($sec,$min,$hr,$day,$mon,$yr);
1650     } else {
1651         print "Unknown date format in parsedate: $_\n";
1652         return undef;
1653     }
1654 }
1655
1656 sub date_diff {
1657     my ($old, $new) = @_;
1658     $new = time() if ! $new;
1659     $old = str2time($old) if $old !~ /^\d+$/;
1660     $new = str2time($new) if $new !~ /^\d+$/;
1661     return "???" if ! $old or ! $new;
1662
1663     my %seconds = (min => 60,
1664                    hr  => 60*60,
1665                    day => 60*60*24,
1666                    wk  => 60*60*24*7,
1667                    mth => 60*60*24*30,
1668                    yr  => 60*60*24*365);
1669
1670     my $diff = $new - $old;
1671     my $what = 'sec';
1672     my $howmuch = $diff;
1673     for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1674         last if $diff < $seconds{$_};
1675         $what = $_;
1676         $howmuch = int($diff/$seconds{$_});
1677     }
1678     return "$howmuch $what";
1679 }
1680
1681 sub prettyshow {
1682     my $forms = shift;
1683     my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1684     my $k = $form->[2];
1685     # dates are in local time zone
1686     if ( $k ) {
1687         print "Date: $k->{Created}\n";
1688         print "From: $k->{Requestors}\n";
1689         print "Cc: $k->{Cc}\n" if $k->{Cc};
1690         print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1691         print "X-Queue: $k->{Queue}\n";
1692         print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1693     }
1694     # dates in these attributes are in GMT and will be converted
1695     foreach my $form (@$forms) {
1696         my ($c, $o, $k, $e) = @$form;
1697         next if ! $k->{id} or exists $k->{Queue};
1698         if ( exists $k->{Created} ) {
1699             my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1700             $m--;
1701             my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1702             if ( exists $k->{Description} ) {
1703                 print "===> $k->{Description} on $created\n";
1704             }
1705         }
1706         print "$k->{Content}\n" if exists $k->{Content} and
1707                                    $k->{Content} !~ /to have no content$/ and
1708                                    ($k->{Type}||'') ne 'EmailRecord';
1709         print "$k->{Attachments}\n" if exists $k->{Attachments} and
1710                                    $k->{Attachments};
1711     }
1712 }
1713
1714 sub prettylist {
1715     my $forms = shift;
1716     my $heading = "Ticket Owner Queue    Age   Told Status Requestor Subject\n";
1717     $heading .= '-' x 80 . "\n";
1718     my (@open, @me);
1719     foreach my $form (@$forms) {
1720         my ($c, $o, $k, $e) = @$form;
1721         next if ! $k->{id};
1722         print $heading if $heading;
1723         $heading = '';
1724         my $id = $k->{id};
1725         $id =~ s!^ticket/!!;
1726         my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1727         $owner = substr($owner, 0, 5);
1728         my $queue = substr($k->{Queue}, 0, 5);
1729         my $subject = substr($k->{Subject}, 0, 30);
1730         my $age = date_diff($k->{Created});
1731         my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1732         my $status = substr($k->{Status}, 0, 6);
1733         my $requestor = substr($k->{Requestors}, 0, 9);
1734         my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1735             $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1736         if ( $k->{Owner} eq 'Nobody' ) {
1737             push @open, $line;
1738         } elsif ($k->{Owner} eq $config{user} ) {
1739             push @me, $line;
1740         } else {
1741             print $line;
1742         }
1743     }
1744     print "No matches found\n" if $heading;
1745     printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1746     print @me if @me;
1747     printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1748     print @open if @open;
1749 }
1750
1751 __DATA__
1752
1753 Title: intro
1754 Title: introduction
1755 Text:
1756
1757     This is a command-line interface to RT 3.0 or newer.
1758
1759     It allows you to interact with an RT server over HTTP, and offers an
1760     interface to RT's functionality that is better-suited to automation
1761     and integration with other tools.
1762
1763     In general, each invocation of this program should specify an action
1764     to perform on one or more objects, and any other arguments required
1765     to complete the desired action.
1766
1767     For more information:
1768
1769         - rt help usage         (syntax information)
1770         - rt help objects       (how to specify objects)
1771         - rt help actions       (a list of possible actions)
1772         - rt help types         (a list of object types)
1773
1774         - rt help config        (configuration details)
1775         - rt help examples      (a few useful examples)
1776         - rt help topics        (a list of help topics)
1777
1778 --
1779
1780 Title: usage
1781 Title: syntax
1782 Text:
1783
1784     Syntax:
1785
1786         rt <action> [options] [arguments]
1787       or
1788         rt shell
1789
1790     Each invocation of this program must specify an action (e.g. "edit",
1791     "create"), options to modify behaviour, and other arguments required
1792     by the specified action. (For example, most actions expect a list of
1793     numeric object IDs to act upon.)
1794
1795     The details of the syntax and arguments for each action are given by
1796     "rt help <action>". Some actions may be referred to by more than one
1797     name ("create" is the same as "new", for example).  
1798
1799     You may also call "rt shell", which will give you an 'rt>' prompt at
1800     which you can issue commands of the form "<action> [options] 
1801     [arguments]".  See "rt help shell" for details.
1802
1803     Objects are identified by a type and an ID (which can be a name or a
1804     number, depending on the type). For some actions, the object type is
1805     implied (you can only comment on tickets); for others, the user must
1806     specify it explicitly. See "rt help objects" for details.
1807
1808     In syntax descriptions, mandatory arguments that must be replaced by
1809     appropriate value are enclosed in <>, and optional arguments are
1810     indicated by [] (for example, <action> and [options] above).
1811
1812     For more information:
1813
1814         - rt help objects       (how to specify objects)
1815         - rt help actions       (a list of actions)
1816         - rt help types         (a list of object types)
1817         - rt help shell         (how to use the shell)
1818
1819 --
1820
1821 Title: conf
1822 Title: config
1823 Title: configuration
1824 Text:
1825
1826     This program has two major sources of configuration information: its
1827     configuration files, and the environment.
1828
1829     The program looks for configuration directives in a file named .rtrc
1830     (or $RTCONFIG; see below) in the current directory, and then in more
1831     distant ancestors, until it reaches /. If no suitable configuration
1832     files are found, it will also check for ~/.rtrc, local/etc/rt.conf
1833     and /etc/rt.conf.
1834
1835     Configuration directives:
1836
1837         The following directives may occur, one per line:
1838
1839         - server <URL>          URL to RT server.
1840         - user <username>       RT username.
1841         - passwd <passwd>       RT user's password.
1842         - query <RT Query>      Default RT Query for list action
1843         - orderby <order>       Default RT order for list action
1844         - queue <queuename>     Default RT Queue for list action
1845         - externalauth <0|1>    Use HTTP Basic authentication
1846          explicitely setting externalauth to 0 inhibits also GSSAPI based
1847          authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1848
1849         Blank and #-commented lines are ignored.
1850
1851     Sample configuration file contents:
1852
1853          server  https://rt.somewhere.com/
1854          # more than one queue can be given (by adding a query expression)
1855          queue helpdesk or queue=support
1856          query Status != resolved and Owner=myaccount
1857
1858
1859     Environment variables:
1860
1861         The following environment variables override any corresponding
1862         values defined in configuration files:
1863
1864         - RTUSER
1865         - RTPASSWD
1866         - RTEXTERNALAUTH
1867         - RTSERVER
1868         - RTDEBUG       Numeric debug level. (Set to 3 for full logs.)
1869         - RTCONFIG      Specifies a name other than ".rtrc" for the
1870                         configuration file.
1871         - RTQUERY       Default RT Query for rt list
1872         - RTORDERBY     Default order for rt list
1873
1874 --
1875
1876 Title: objects
1877 Text:
1878
1879     Syntax:
1880
1881         <type>/<id>[/<attributes>]
1882
1883     Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1884     ID. Some types of objects can also be identified by name (like users
1885     and queues). Furthermore, objects may have named attributes (such as
1886     "ticket/1/history").
1887
1888     An object specification is like a path in a virtual filesystem, with
1889     object types as top-level directories, object IDs as subdirectories,
1890     and named attributes as further subdirectories.
1891
1892     A comma-separated list of names, numeric IDs, or numeric ranges can
1893     be used to specify more than one object of the same type. Note that
1894     the list must be a single argument (i.e., no spaces). For example,
1895     "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1896     can also be written as "user/ams,root,1,2,3,5,7,8-10".
1897     
1898     If just a number is given as object specification it will be
1899     interpreted as ticket/<number>
1900
1901     Examples:
1902
1903         1                   # the same as ticket/1
1904         ticket/1
1905         ticket/1/attachments
1906         ticket/1/attachments/3
1907         ticket/1/attachments/3/content
1908         ticket/1-3/links
1909         ticket/1-3,5-7/history
1910
1911         user/ams
1912         user/ams/rights
1913         user/ams,rai,1/rights
1914
1915     For more information:
1916
1917         - rt help <action>      (action-specific details)
1918         - rt help <type>        (type-specific details)
1919
1920 --
1921
1922 Title: actions
1923 Title: commands
1924 Text:
1925
1926     You can currently perform the following actions on all objects:
1927
1928         - list          (list objects matching some condition)
1929         - show          (display object details)
1930         - edit          (edit object details)
1931         - create        (create a new object)
1932
1933     Each type may define actions specific to itself; these are listed in
1934     the help item about that type.
1935
1936     For more information:
1937
1938         - rt help <action>      (action-specific details)
1939         - rt help types         (a list of possible types)
1940
1941     The following actions on tickets are also possible:
1942
1943         - comment       Add comments to a ticket
1944         - correspond    Add comments to a ticket
1945         - merge         Merge one ticket into another
1946         - link          Link one ticket to another
1947         - take          Take a ticket (steal and untake are possible as well)
1948
1949     For several edit set subcommands that are frequently used abbreviations
1950     have been introduced. These abbreviations are:
1951
1952         - delete or del  delete a ticket           (edit set status=deleted)
1953         - resolve or res resolve a ticket          (edit set status=resolved)
1954         - subject        change subject of ticket  (edit set subject=string)
1955         - give           give a ticket to somebody (edit set owner=user)
1956
1957 --
1958
1959 Title: types
1960 Text:
1961
1962     You can currently operate on the following types of objects:
1963
1964         - tickets
1965         - users
1966         - groups
1967         - queues
1968
1969     For more information:
1970
1971         - rt help <type>        (type-specific details)
1972         - rt help objects       (how to specify objects)
1973         - rt help actions       (a list of possible actions)
1974
1975 --
1976
1977 Title: ticket
1978 Text:
1979
1980     Tickets are identified by a numeric ID.
1981
1982     The following generic operations may be performed upon tickets:
1983
1984         - list
1985         - show
1986         - edit
1987         - create
1988
1989     In addition, the following ticket-specific actions exist:
1990
1991         - link
1992         - merge
1993         - comment
1994         - correspond
1995         - take
1996         - steal
1997         - untake
1998         - give
1999         - resolve
2000         - delete
2001         - subject
2002
2003     Attributes:
2004
2005         The following attributes can be used with "rt show" or "rt edit"
2006         to retrieve or edit other information associated with tickets:
2007
2008         links                      A ticket's relationships with others.
2009         history                    All of a ticket's transactions.
2010         history/type/<type>        Only a particular type of transaction.
2011         history/id/<id>            Only the transaction of the specified id.
2012         attachments                A list of attachments.
2013         attachments/<id>           The metadata for an individual attachment.
2014         attachments/<id>/content   The content of an individual attachment.
2015
2016 --
2017
2018 Title: user
2019 Title: group
2020 Text:
2021
2022     Users and groups are identified by name or numeric ID.
2023
2024     The following generic operations may be performed upon them:
2025
2026         - list
2027         - show
2028         - edit
2029         - create
2030
2031     In addition, the following type-specific actions exist:
2032
2033         - grant
2034         - revoke
2035
2036     Attributes:
2037
2038         The following attributes can be used with "rt show" or "rt edit"
2039         to retrieve or edit other information associated with users and
2040         groups:
2041
2042         rights                  Global rights granted to this user.
2043         rights/<queue>          Queue rights for this user.
2044
2045 --
2046
2047 Title: queue
2048 Text:
2049
2050     Queues are identified by name or numeric ID.
2051
2052     Currently, they can be subjected to the following actions:
2053
2054         - show
2055         - edit
2056         - create
2057
2058 --
2059
2060 Title: subject
2061 Text:
2062
2063     Syntax:
2064
2065         rt subject <id> <new subject text>
2066
2067     Change the subject of a ticket whose ticket id is given.
2068
2069 --
2070
2071 Title: give
2072 Text:
2073
2074     Syntax:
2075
2076         rt give <id> <accountname>
2077
2078     Give a ticket whose ticket id is given to another user.
2079
2080 --
2081
2082 Title: steal
2083 Text:
2084
2085         rt steal <id> 
2086
2087     Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2088
2089 --
2090
2091 Title: take
2092 Text:
2093
2094     Syntax:
2095
2096         rt take <id>
2097
2098     Take a ticket whose ticket id is given, i.e. set the owner to myself.
2099
2100 --
2101
2102 Title: untake
2103 Text:
2104
2105     Syntax:
2106
2107         rt untake <id>
2108
2109     Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2110
2111 --
2112
2113 Title: resolve
2114 Title: res
2115 Text:
2116
2117     Syntax:
2118
2119         rt resolve <id>
2120
2121     Resolves a ticket whose ticket id is given.
2122
2123 --
2124
2125 Title: delete
2126 Title: del
2127 Text:
2128
2129     Syntax:
2130
2131         rt delete <id>
2132
2133     Deletes a ticket whose ticket id is given.
2134
2135 --
2136
2137 Title: logout
2138 Text:
2139
2140     Syntax:
2141
2142         rt logout
2143
2144     Terminates the currently established login session. You will need to
2145     provide authentication credentials before you can continue using the
2146     server. (See "rt help config" for details about authentication.)
2147
2148 --
2149
2150 Title: ls
2151 Title: list
2152 Title: search
2153 Text:
2154
2155     Syntax:
2156
2157         rt <ls|list|search> [options] "query string"
2158
2159     Displays a list of objects matching the specified conditions.
2160     ("ls", "list", and "search" are synonyms.)
2161
2162     Conditions are expressed in the SQL-like syntax used internally by
2163     RT. (For more information, see "rt help query".) The query string
2164     must be supplied as one argument.
2165
2166     (Right now, the server doesn't support listing anything but tickets.
2167     Other types will be supported in future; this client will be able to
2168     take advantage of that support without any changes.)
2169
2170     Options:
2171
2172         The following options control how much information is displayed
2173         about each matching object:
2174
2175         -i             Numeric IDs only. (Useful for |rt edit -; see examples.)
2176         -s             Short description.
2177         -l             Longer description.
2178         -f <field[s]   Display only the fields listed and the ticket id
2179
2180         In addition,
2181         
2182         -o +/-<field>  Orders the returned list by the specified field.
2183         -r             reversed order (useful if a default was given)
2184         -q queue[s]    restricts the query to the queue[s] given
2185                        multiple queues are separated by comma
2186         -S var=val     Submits the specified variable with the request.
2187         -t type        Specifies the type of object to look for. (The
2188                        default is "ticket".)
2189
2190     Examples:
2191
2192         rt ls "Priority > 5 and Status=new"
2193         rt ls -o +Subject "Priority > 5 and Status=new"
2194         rt ls -o -Created "Priority > 5 and Status=new"
2195         rt ls -i "Priority > 5"|rt edit - set status=resolved
2196         rt ls -t ticket "Subject like '[PATCH]%'"
2197         rt ls -q systems
2198         rt ls -f owner,subject
2199
2200 --
2201
2202 Title: show
2203 Text:
2204
2205     Syntax:
2206
2207         rt show [options] <object-ids>
2208
2209     Displays details of the specified objects.
2210
2211     For some types, object information is further classified into named
2212     attributes (for example, "1-3/links" is a valid ticket specification
2213     that refers to the links for tickets 1-3). Consult "rt help <type>"
2214     and "rt help objects" for further details.
2215
2216     If only a number is given it will be interpreted as the objects
2217     ticket/number and ticket/number/history
2218
2219     This command writes a set of forms representing the requested object
2220     data to STDOUT.
2221
2222     Options:
2223
2224         The following options control how much information is displayed
2225         about each matching object:
2226
2227         Without any formatting options prettyprinted output is generated.
2228         Giving any of the two options below reverts to raw output.
2229         -s      Short description (history and attachments only).
2230         -l      Longer description (history and attachments only).
2231
2232         In addition,
2233         -               Read IDs from STDIN instead of the command-line.
2234         -t type         Specifies object type.
2235         -f a,b,c        Restrict the display to the specified fields.
2236         -S var=val      Submits the specified variable with the request.
2237
2238     Examples:
2239
2240         rt show -t ticket -f id,subject,status 1-3
2241         rt show ticket/3/attachments/29
2242         rt show ticket/3/attachments/29/content
2243         rt show ticket/1-3/links
2244         rt show ticket/3/history
2245         rt show -l ticket/3/history
2246         rt show -t user 2
2247         rt show 2
2248
2249 --
2250
2251 Title: new
2252 Title: edit
2253 Title: create
2254 Text:
2255
2256     Syntax:
2257
2258         rt edit [options] <object-ids> set field=value [field=value] ...
2259                                        add field=value [field=value] ...
2260                                        del field=value [field=value] ...
2261
2262     Edits information corresponding to the specified objects.
2263
2264     A purely numeric object id nnn is translated into ticket/nnn
2265
2266     If, instead of "edit", an action of "new" or "create" is specified,
2267     then a new object is created. In this case, no numeric object IDs
2268     may be specified, but the syntax and behaviour remain otherwise
2269     unchanged.
2270
2271     This command typically starts an editor to allow you to edit object
2272     data in a form for submission. If you specified enough information
2273     on the command-line, however, it will make the submission directly.
2274
2275     The command line may specify field-values in three different ways.
2276     "set" sets the named field to the given value, "add" adds a value
2277     to a multi-valued field, and "del" deletes the corresponding value.
2278     Each "field=value" specification must be given as a single argument.
2279
2280     For some types, object information is further classified into named
2281     attributes (for example, "1-3/links" is a valid ticket specification
2282     that refers to the links for tickets 1-3). These attributes may also
2283     be edited. Consult "rt help <type>" and "rt help object" for further
2284     details.
2285
2286     Options:
2287
2288         -       Read numeric IDs from STDIN instead of the command-line.
2289                 (Useful with rt ls ... | rt edit -; see examples below.)
2290         -i      Read a completed form from STDIN before submitting.
2291         -o      Dump the completed form to STDOUT instead of submitting.
2292         -e      Allows you to edit the form even if the command-line has
2293                 enough information to make a submission directly.
2294         -S var=val
2295                 Submits the specified variable with the request.
2296         -t type Specifies object type.
2297
2298     Examples:
2299
2300         # Interactive (starts $EDITOR with a form).
2301         rt edit ticket/3
2302         rt create -t ticket
2303
2304         # Non-interactive.
2305         rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2306         rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2307         rt edit ticket/4 set priority=3 owner=bar@example.com \
2308                          add cc=foo@example.com bcc=quux@example.net
2309         rt create -t ticket set subject='new ticket' priority=10 \
2310                             add cc=foo@example.com
2311
2312 --
2313
2314 Title: comment
2315 Title: correspond
2316 Text:
2317
2318     Syntax:
2319
2320         rt <comment|correspond> [options] <ticket-id>
2321
2322     Adds a comment (or correspondence) to the specified ticket (the only
2323     difference being that comments aren't sent to the requestors.)
2324
2325     This command will typically start an editor and allow you to type a
2326     comment into a form. If, however, you specified all the necessary
2327     information on the command line, it submits the comment directly.
2328
2329     (See "rt help forms" for more information about forms.)
2330
2331     Options:
2332
2333         -m <text>       Specify comment text.
2334         -a <file>       Attach a file to the comment. (May be used more
2335                         than once to attach multiple files.)
2336         -c <addrs>      A comma-separated list of Cc addresses.
2337         -b <addrs>      A comma-separated list of Bcc addresses.
2338         -w <time>       Specify the time spent working on this ticket.
2339         -e              Starts an editor before the submission, even if
2340                         arguments from the command line were sufficient.
2341
2342     Examples:
2343
2344         rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2345
2346 --
2347
2348 Title: merge
2349 Text:
2350
2351     Syntax:
2352
2353         rt merge <from-id> <to-id>
2354
2355     Merges the first ticket specified into the second ticket specified.
2356
2357 --
2358
2359 Title: link
2360 Text:
2361
2362     Syntax:
2363
2364         rt link [-d] <id-A> <link> <id-B>
2365
2366     Creates (or, with -d, deletes) a link between the specified tickets.
2367     The link can (irrespective of case) be any of:
2368
2369         DependsOn/DependedOnBy:     A depends upon B (or vice versa).
2370         RefersTo/ReferredToBy:      A refers to B (or vice versa).
2371         MemberOf/HasMember:         A is a member of B (or vice versa).
2372
2373     To view a ticket's links, use "rt show ticket/3/links". (See
2374     "rt help ticket" and "rt help show".)
2375
2376     Options:
2377
2378         -d      Deletes the specified link.
2379
2380     Examples:
2381
2382         rt link 2 dependson 3
2383         rt link -d 4 referredtoby 6     # 6 no longer refers to 4
2384
2385 --
2386
2387 Title: grant
2388 Title: revoke
2389 Text:
2390
2391 --
2392
2393 Title: query
2394 Text:
2395
2396     RT uses an SQL-like syntax to specify object selection constraints.
2397     See the <RT:...> documentation for details.
2398     
2399     (XXX: I'm going to have to write it, aren't I?)
2400
2401     Until it exists here a short description of important constructs:
2402
2403     The two simple forms of query expressions are the constructs
2404     Attribute like Value and
2405     Attribute = Value or Attribute != Value
2406
2407     Whether attributes can be matched using like or using = is built into RT.
2408     The attributes id, Queue, Owner Priority and Status require the = or !=
2409     tests.
2410
2411     If Value is a string it must be quoted and may contain the wildcard
2412     character %. If the string does not contain white space, the quoting
2413     may however be omitted, it will be added automatically when parsing
2414     the input.
2415
2416     Simple query expressions can be combined using and, or and parentheses
2417     can be used to group expressions.
2418
2419     As a special case a standalone string (which would not form a correct
2420     query) is transformed into (Owner='string' or Requestor like 'string%')
2421     and added to the default query, i.e. the query is narrowed down.
2422
2423     If no Queue=name clause is contained in the query, a default clause
2424     Queue=$config{queue} is added.
2425
2426     Examples:
2427     Status!='resolved' and Status!='rejected'
2428     (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2429
2430 --
2431
2432 Title: form
2433 Title: forms
2434 Text:
2435
2436     This program uses RFC822 header-style forms to represent object data
2437     in a form that's suitable for processing both by humans and scripts.
2438
2439     A form is a set of (field, value) specifications, with some initial
2440     commented text and interspersed blank lines allowed for convenience.
2441     Field names may appear more than once in a form; a comma-separated
2442     list of multiple field values may also be specified directly.
2443     
2444     Field values can be wrapped as in RFC822, with leading whitespace.
2445     The longest sequence of leading whitespace common to all the lines
2446     is removed (preserving further indentation). There is no limit on
2447     the length of a value.
2448
2449     Multiple forms are separated by a line containing only "--\n".
2450
2451     (XXX: A more detailed specification will be provided soon. For now,
2452     the server-side syntax checking will suffice.)
2453
2454 --
2455
2456 Title: topics
2457 Text:
2458
2459     Syntax:
2460
2461         rt help <topic>
2462
2463     Get help on any of the following subjects:
2464
2465         - tickets, users, groups, queues.
2466         - show, edit, ls/list/search, new/create.
2467
2468         - query                                 (search query syntax)
2469         - forms                                 (form specification)
2470
2471         - objects                               (how to specify objects)
2472         - types                                 (a list of object types)
2473         - actions/commands                      (a list of actions)
2474         - usage/syntax                          (syntax details)
2475         - conf/config/configuration             (configuration details)
2476         - examples                              (a few useful examples)
2477
2478 --
2479
2480 Title: example
2481 Title: examples
2482 Text:
2483
2484     some useful examples
2485
2486     All the following list requests will be restricted to the default queue.
2487     That can be changed by adding the option -q queuename
2488
2489     List all tickets that are not rejected/resolved
2490         rt ls
2491     List all tickets that are new and do not have an owner
2492         rt ls "status=new and owner=nobody"
2493     List all tickets which I have sent or of which I am the owner
2494         rt ls myaccount
2495     List all attributes for the ticket 6977 (ls -l instead of ls)
2496         rt ls -l 6977
2497     Show the content of ticket 6977
2498         rt show 6977
2499     Show all attributes in the ticket and in the history of the ticket
2500         rt show -l 6977
2501     Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2502         rt comment 6977
2503         This will open an editor and lets you add text (attribute Text:)
2504         Other attributes may be changed as well, but usually don't do that.
2505     Correspond a ticket (like comment, but mail is also sent to requestors)
2506         rt correspond 6977
2507     Edit a ticket (generic change, interactive using the editor)
2508         rt edit 6977
2509     Change the owner of a ticket non interactively
2510         rt edit 6977 set owner=myaccount
2511         or
2512         rt give 6977 account
2513         or
2514         rt take 6977
2515     Change the status of a ticket
2516         rt edit 6977 set status=resolved
2517         or
2518         rt resolve 6977
2519     Change the status of all tickets I own to resolved !!!
2520         rt ls -i owner=myaccount | rt edit - set status=resolved
2521
2522 --
2523
2524 Title: shell
2525 Text:
2526
2527     Syntax:
2528
2529         rt shell
2530
2531     Opens an interactive shell, at which you can issue commands of 
2532     the form "<action> [options] [arguments]".
2533
2534     To exit the shell, type "quit" or "exit".
2535
2536     Commands can be given at the shell in the same form as they would 
2537     be given at the command line without the leading 'rt' invocation.
2538
2539     Example:
2540         $ rt shell
2541         rt> create -t ticket set subject='new' add cc=foo@example.com
2542         # Ticket 8 created.
2543         rt> quit
2544         $
2545
2546 --
2547
2548 Title: take
2549 Title: untake
2550 Title: steal
2551 Text:
2552
2553     Syntax:
2554
2555         rt <take|untake|steal> <ticket-id>
2556
2557     Sets the owner of the specified ticket to the current user, 
2558     assuming said user has the bits to do so, or releases the 
2559     ticket.  
2560     
2561     'Take' is used on tickets which are not currently owned 
2562     (Owner: Nobody), 'steal' is used on tickets which *are* 
2563     currently owned, and 'untake' is used to "release" a ticket 
2564     (reset its Owner to Nobody).  'Take' cannot be used on
2565     tickets which are currently owned.
2566
2567     Example:
2568         alice$ rt create -t ticket set subject="New ticket"
2569         # Ticket 7 created.
2570         alice$ rt take 7
2571         # Owner changed from Nobody to alice
2572         alice$ su bob
2573         bob$ rt steal 7
2574         # Owner changed from alice to bob
2575         bob$ rt untake 7
2576         # Owner changed from bob to Nobody
2577
2578 --
2579
2580 Title: quit
2581 Title: exit
2582 Text:
2583
2584     Use "quit" or "exit" to leave the shell.  Only valid within shell 
2585     mode.
2586
2587     Example:
2588         $ rt shell
2589         rt> quit
2590         $
2591
2592 __END__
2593
2594 =head1 NAME
2595
2596 rt - command-line interface to RT 3.0 or newer
2597
2598 =head1 SYNOPSIS
2599
2600     rt help
2601
2602 =head1 DESCRIPTION
2603
2604 This script allows you to interact with an RT server over HTTP, and offers an
2605 interface to RT's functionality that is better-suited to automation and
2606 integration with other tools.
2607
2608 In general, each invocation of this program should specify an action to
2609 perform on one or more objects, and any other arguments required to complete
2610 the desired action.
2611