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