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