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