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