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