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