2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
7 # <sales@bestpractical.com>
9 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
31 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
48 # END BPS TAGGED BLOCK }}}
49 # Designed and implemented for Best Practical Solutions, LLC by
50 # Abhijit Menon-Sen <ams@wiw.org>
55 if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
57 print Pod::Usage::pod2usage( { verbose => 2 } );
61 # This program is intentionally written to have as few non-core module
62 # dependencies as possible. It should stay that way.
67 use HTTP::Request::Common;
70 use Time::Local; # used in prettyshow
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)
76 eval { require GSSAPI };
77 my $no_strong_auth = 'missing perl module GSSAPI';
79 eval {require LWP::Authen::Negotiate};
80 $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
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.
88 my $HOME = eval{(getpwuid($<))[7]}
89 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
94 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
96 server => 'http://localhost/',
97 query => "Status!='resolved' and Status!='rejected'",
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
106 config_from_file($ENV{RTCONFIG} || ".rtrc"),
109 my $session = Session->new("$HOME/.rt_sessions");
110 my $REST = "$config{server}/REST/1.0";
111 $no_strong_auth = 'switched off by externalauth=0'
112 if defined $config{externalauth};
118 sub DEBUG { warn @_ if $config{debug} >= shift }
120 # These regexes are used by command handlers to parse arguments.
121 # (XXX: Ask Autrijus how i18n changes these definitions.)
123 my $name = '[\w.-]+';
124 my $CF_name = '[^,]+?';
125 my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
126 my $label = '[^,\\/]+';
127 my $labels = "(?:$label,)*$label";
128 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
130 # Our command line looks like this:
132 # rt <action> [options] [arguments]
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.
138 # handler => [ ...aliases... ],
139 version => ["version", "ver"],
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"],
149 grant => ["grant", "revoke"],
150 take => ["take", "steal", "untake"],
151 quit => ["quit", "exit"],
152 setcommand => ["del", "delete", "give", "res", "resolve",
157 foreach my $fn (keys %handlers) {
158 foreach my $alias (@{ $handlers{$fn} }) {
159 $actions{$alias} = \&{"$fn"};
163 # Once we find and call an appropriate handler, we're done.
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);
175 print STDERR "rt: Unknown command '@ARGV'.\n";
176 print STDERR "rt: For help, run 'rt help'.\n";
186 # The following subs are handlers for each entry in %actions.
190 my $term = Term::ReadLine->new('RT CLI');
191 while ( defined ($_ = $term->readline($prompt)) ) {
192 next if /^#/ || /^\s*$/;
194 @ARGV = shellwords($_);
200 print "rt $VERSION\n";
205 submit("$REST/logout") if defined $session->cookie;
216 my ($action, $type, $rv) = @_;
217 $rv = defined $rv ? $rv : 0;
220 # What help topics do we know about?
223 foreach my $item (@{ Form::parse(<DATA>) }) {
224 my $title = $item->[2]{Title};
225 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
227 foreach $title (grep $_, @titles) {
228 $help{$title} = $item->[2]{Text};
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.
238 if (exists $help{$_}) { $key = $_; last; }
241 # Tolerate possibly plural words.
243 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
248 if ($type && $action) {
249 $key = "$type.$action";
251 $key ||= $type || $action || "introduction";
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"; }
261 $key = "introduction";
265 print STDERR $help{$key}, "\n\n";
269 # Displays a list of objects that match some specified condition.
272 my ($q, $type, %data);
273 my $orderby = $config{orderby};
275 if ($config{orderby}) {
276 $data{orderby} = $config{orderby};
280 my $reverse_sort = 0;
281 my $queue = $config{queue};
287 $bad = 1, last unless defined($type = get_type_argument());
290 $bad = 1, last unless get_var_argument(\%data);
293 $data{'orderby'} = shift @ARGV;
295 elsif (/^-([isl])$/) {
300 $queue = shift @ARGV;
306 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
307 whine "No valid field list in '-f $ARGV[0]'.";
310 $data{fields} = shift @ARGV;
311 $data{format} = 's' if ! $data{format};
314 elsif (!defined $q && !/^-/) {
318 my $datum = /^-/ ? "option" : "argument";
319 whine "Unrecognised $datum '$_'.";
323 if ( ! $rawprint and ! exists $data{format} ) {
326 if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
327 $data{orderby} =~ s/^-/+/;
328 } elsif ($reverse_sort) {
329 $data{orderby} =~ s/^\+?(.*)/-$1/;
336 $q =~ s/^#//; # get rid of leading hash
338 # only digits, must be an id, formulate a correct query
339 $q = "id=$q" if $q =~ /^\d+$/;
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
349 # correctly quote strings in a query
350 $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
353 unless ($type && defined $q) {
354 my $item = $type ? "query string" : "object type";
355 whine "No $item specified.";
358 #return help("list", $type) if $bad;
359 return suggest_help("list", $type, $bad) if $bad;
361 print "Query:$q\n" if ! $rawprint;
362 my $r = submit("$REST/search/$type", { query => $q, %data });
366 my $forms = Form::parse($r->content);
372 # Displays selected information about a single object.
375 my ($type, @objects, %data);
383 s/^#// if /^#\d+/; # get rid of leading hash
385 $bad = 1, last unless defined($type = get_type_argument());
388 $bad = 1, last unless get_var_argument(\%data);
390 elsif (/^-([isl])$/) {
394 elsif (/^-$/ && !$slurped) {
395 chomp(my @lines = <STDIN>);
397 unless (is_object_spec($_, $type)) {
398 whine "Invalid object on STDIN: '$_'.";
406 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
407 whine "No valid field list in '-f $ARGV[0]'.";
410 $data{fields} = shift @ARGV;
411 # option f requires short raw listing format
415 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
416 push @objects, $spc2;
417 $histspec = is_object_spec("ticket/$_/history", $type);
419 elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
420 push @objects, $spc3;
421 $rawprint = 1 if $_ =~ /\/content$/;
423 elsif (my $spec = is_object_spec($_, $type)) {
424 push @objects, $spec;
425 $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
428 my $datum = /^-/ ? "option" : "argument";
429 whine "Unrecognised $datum '$_'.";
434 push @objects, $histspec if $histspec;
435 $data{format} = 'l' if ! exists $data{format};
439 whine "No objects specified.";
442 #return help("show", $type) if $bad;
443 return suggest_help("show", $type, $bad) if $bad;
445 my $r = submit("$REST/show", { id => \@objects, %data });
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\//) {
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);
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.
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.
474 my (%data, $type, @objects);
475 my ($cl, $text, $edit, $input, $output);
477 use vars qw(%set %add %del);
478 %set = %add = %del = ();
484 s/^#// if /^#\d+/; # get rid of leading hash
486 if (/^-e$/) { $edit = 1 }
487 elsif (/^-i$/) { $input = 1 }
488 elsif (/^-o$/) { $output = 1 }
490 $bad = 1, last unless defined($type = get_type_argument());
493 $bad = 1, last unless get_var_argument(\%data);
495 elsif (/^-$/ && !($slurped || $input)) {
496 chomp(my @lines = <STDIN>);
498 unless (is_object_spec($_, $type)) {
499 whine "Invalid object on STDIN: '$_'.";
509 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
510 my ($key, $op, $val) = ($1, $2, $3);
511 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
513 vpush($hash, lc $key, $val);
518 whine "No variables to set.";
523 elsif (/^(?:add|del)$/i) {
525 my $hash = ($_ eq "add") ? \%add : \%del;
527 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
528 my ($key, $val) = ($1, $2);
530 vpush($hash, lc $key, $val);
535 whine "No variables to set.";
540 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
541 push @objects, $spc2;
543 elsif (my $spec = is_object_spec($_, $type)) {
544 push @objects, $spec;
547 my $datum = /^-/ ? "option" : "argument";
548 whine "Unrecognised $datum '$_'.";
553 if ($action =~ /^ed(?:it)?$/) {
555 whine "No objects specified.";
561 whine "You shouldn't specify objects as arguments to $action.";
565 whine "What type of object do you want to create?";
568 @objects = ("$type/new") if defined($type);
570 #return help($action, $type) if $bad;
571 return suggest_help($action, $type, $bad) if $bad;
573 # We need a form to make changes to. We usually ask the server for
574 # one, but we can avoid that if we are fed one on STDIN, or if the
575 # user doesn't want to edit the form by hand, and the command line
576 # specifies only simple variable assignments. We *should* get a
577 # form if we're creating a new ticket, so that the default values
578 # get filled in properly.
580 my @new_objects = grep /\/new$/, @objects;
586 elsif ($edit || %add || %del || !$cl || @new_objects) {
587 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
591 # If any changes were specified on the command line, apply them.
594 # We're updating forms from the server.
595 my $forms = Form::parse($text);
597 foreach my $form (@$forms) {
598 my ($c, $o, $k, $e) = @$form;
601 next if ($e || !@$o);
607 # Make changes to existing fields.
609 if (exists $add{lc $key}) {
610 $val = delete $add{lc $key};
611 vpush($k, $key, $val);
612 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
614 if (exists $del{lc $key}) {
615 $val = delete $del{lc $key};
616 my %val = map {$_=>1} @{ vsplit($val) };
617 $k->{$key} = vsplit($k->{$key});
618 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
620 if (exists $set{lc $key}) {
621 $k->{$key} = delete $set{lc $key};
625 # Then update the others.
626 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
627 foreach $key (keys %add) {
628 vpush($k, $key, $add{$key});
629 $k->{$key} = vsplit($k->{$key});
631 push @$o, (keys %add, keys %set);
634 $text = Form::compose($forms);
637 # We're rolling our own set of forms.
640 my ($type, $ids, $args) =
641 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
644 foreach my $obj (expand_list($ids)) {
645 my %set = (%set, id => "$type/$obj$args");
646 push @forms, ["", [keys %set], \%set];
649 $text = Form::compose(\@forms);
661 # We'll let the user edit the form before sending it to the server,
662 # unless we have enough information to submit it non-interactively.
663 if ($edit || (!$input && !$cl)) {
664 my $newtext = vi($text);
665 # We won't resubmit a bad form unless it was changed.
666 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
670 my $r = submit("$REST/edit", {content => $text, %data});
671 if ($r->code == 409) {
672 # If we submitted a bad form, we'll give the user a chance
673 # to correct it and resubmit.
674 if ($edit || (!$input && !$cl)) {
689 # handler for special edit commands. A valid edit command is constructed and
690 # further work is delegated to the edit handler
694 my ($id, $bad, $what);
697 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
701 whine "No ticket number specified.";
704 if ($action eq 'subject') {
705 my $subject = '"'.join (" ", @ARGV).'"';
707 $what = "subject=$subject";
708 } elsif ($action eq 'give') {
709 my $owner = shift @ARGV;
710 $what = "owner=$owner";
713 if ( $action eq 'delete' or $action eq 'del' ) {
714 $what = "status=deleted";
715 } elsif ($action eq 'resolve' or $action eq 'res' ) {
716 $what = "status=resolved";
717 } elsif ($action eq 'take' ) {
718 $what = "owner=$config{user}";
719 } elsif ($action eq 'untake') {
720 $what = "owner=Nobody";
725 whine "Extraneous arguments for action $action: @ARGV.";
729 whine "unrecognized action $action.";
731 return help("edit", undef, $bad) if $bad;
732 @ARGV = ( $id, "set", $what );
733 print "Executing: rt edit @ARGV\n";
737 # We roll "comment" and "correspond" into the same handler.
741 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
750 elsif (/^-[abcmw]$/) {
752 whine "No argument specified with $_.";
757 unless (-f $ARGV[0] && -r $ARGV[0]) {
758 whine "Cannot read attachment: '$ARGV[0]'.";
761 push @files, shift @ARGV;
764 my $a = $_ eq "-b" ? \@bcc : \@cc;
765 @$a = split /\s*,\s*/, shift @ARGV;
769 if ( $msg =~ /^-$/ ) {
771 while (<STDIN>) { $msg .= $_ }
775 elsif (/-w/) { $wtime = shift @ARGV }
777 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
781 my $datum = /^-/ ? "option" : "argument";
782 whine "Unrecognised $datum '$_'.";
788 whine "No object specified.";
791 #return help($action, "ticket") if $bad;
792 return suggest_help($action, "ticket") if $bad;
796 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
802 Attachment => [ @files ],
803 TimeWorked => $wtime || '',
809 my $text = Form::compose([ $form ]);
811 if ($edit || !$msg) {
816 my $ntext = vi($text);
817 return if ($error && $ntext eq $text);
819 $form = Form::parse($text);
822 ($c, $o, $k, $e) = @{ $form->[0] };
825 $c = "# Syntax error.";
831 @files = @{ vsplit($k->{Attachment}) };
834 $text = Form::compose([[$c, $o, $k, $e]]);
839 foreach my $file (@files) {
840 $data{"attachment_$i"} = bless([ $file ], "Attachment");
843 $data{content} = $text;
845 my $r = submit("$REST/ticket/$id/comment", \%data);
850 # Merge one ticket into another.
858 s/^#// if /^#\d+/; # get rid of leading hash
864 whine "Unrecognised argument: '$_'.";
870 my $evil = @id > 2 ? "many" : "few";
871 whine "Too $evil arguments specified.";
874 #return help("merge", "ticket") if $bad;
875 return suggest_help("merge", "ticket", $bad) if $bad;
877 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
882 # Link one ticket to another.
885 my ($bad, $del, %data) = (0, 0, ());
888 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
889 ReferredToBy HasMember MemberOf);
891 while (@ARGV && $ARGV[0] =~ /^-/) {
898 $bad = 1, last unless defined($type = get_type_argument());
901 whine "Unrecognised option: '$_'.";
906 $type = "ticket" unless $type; # default type to tickets
909 my ($from, $rel, $to) = @ARGV;
910 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
911 whine "Invalid link '$rel' for type $type specified.";
914 %data = (id => $from, rel => $rel, to => $to, del => $del);
917 my $bad = @ARGV < 3 ? "few" : "many";
918 whine "Too $bad arguments specified.";
921 return suggest_help("link", $type, $bad) if $bad;
923 my $r = submit("$REST/$type/link", \%data);
928 # Take/steal a ticket
931 my ($bad, %data) = (0, ());
938 unless ($id =~ /^\d+$/) {
939 whine "Invalid ticket ID $id specified.";
944 [ "Ticket", "Action" ],
952 my $text = Form::compose([ $form ]);
953 $data{content} = $text;
956 $bad = @ARGV < 1 ? "few" : "many";
957 whine "Too $bad arguments specified.";
960 return suggest_help("take", "ticket", $bad) if $bad;
962 my $r = submit("$REST/ticket/$id/take", \%data);
967 # Grant/revoke a user's rights.
972 whine "$cmd is unimplemented.";
976 # Client <-> Server communication.
977 # --------------------------------
979 # This function composes and sends an HTTP request to the RT server, and
980 # interprets the response. It takes a request URI, and optional request
981 # data (a string, or a reference to a set of key-value pairs).
984 my ($uri, $content) = @_;
986 my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
987 my $h = HTTP::Headers->new;
989 # Did the caller specify any data to send with the request?
991 if (defined $content) {
992 unless (ref $content) {
993 # If it's just a string, make sure LWP handles it properly.
994 # (By pretending that it's a file!)
995 $content = [ content => [undef, "", Content => $content] ];
997 elsif (ref $content eq 'HASH') {
999 foreach my $k (keys %$content) {
1000 if (ref $content->{$k} eq 'ARRAY') {
1001 foreach my $v (@{ $content->{$k} }) {
1005 else { push @data, $k, $content->{$k} }
1012 # Should we send authentication information to start a new session?
1013 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1014 (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1015 if ($config{externalauth}) {
1016 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1017 print " Password will be sent to $server $how\n",
1018 " Press CTRL-C now if you do not want to continue\n"
1019 if ! $config{passwd};
1020 } elsif ( $no_strong_auth ) {
1021 if (!defined $session->cookie) {
1022 print " Strong encryption not available, $no_strong_auth\n",
1023 " Password will be sent to $server $how\n",
1024 " Press CTRL-C now if you do not want to continue\n"
1025 if ! $config{passwd};
1026 push @$data, ( user => $config{user} );
1027 push @$data, ( pass => $config{passwd} || read_passwd() );
1031 # Now, we construct the request.
1033 $req = POST($uri, $data, Content_Type => 'form-data');
1038 $session->add_cookie_header($req);
1039 if ($config{externalauth}) {
1043 # Then we send the request and parse the response.
1044 DEBUG(3, $req->as_string);
1045 my $res = $ua->request($req);
1046 DEBUG(3, $res->as_string);
1048 if ($res->is_success) {
1049 # The content of the response we get from the RT server consists
1050 # of an HTTP-like status line followed by optional header lines,
1051 # a blank line, and arbitrary text.
1053 my ($head, $text) = split /\n\n/, $res->content, 2;
1054 my ($status, @headers) = split /\n/, $head;
1055 $text =~ s/\n*$/\n/ if ($text);
1057 # "RT/3.0.1 401 Credentials required"
1058 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1059 warn "rt: Malformed RT response from $config{server}.\n";
1060 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1064 # Our caller can pretend that the server returned a custom HTTP
1065 # response code and message. (Doing that directly is apparently
1066 # not sufficiently portable and uncomplicated.)
1069 $res->content($text);
1070 $session->update($res) if ($res->is_success || $res->code != 401);
1072 if (!$res->is_success) {
1073 # We can deal with authentication failures ourselves. Either
1074 # we sent invalid credentials, or our session has expired.
1075 if ($res->code == 401) {
1077 if (exists $d{user}) {
1078 warn "rt: Incorrect username or password.\n";
1081 elsif ($req->header("Cookie")) {
1082 # We'll retry the request with credentials, unless
1083 # we only wanted to logout in the first place.
1085 return submit(@_) unless $uri eq "$REST/logout";
1088 # Conflicts should be dealt with by the handler and user.
1089 # For anything else, we just die.
1090 elsif ($res->code != 409) {
1091 warn "rt: ", $res->content;
1097 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1104 # Session management.
1105 # -------------------
1107 # Maintains a list of active sessions in the ~/.rt_sessions file.
1112 # Initialises the session cache.
1114 my ($class, $file) = @_;
1116 file => $file || "$HOME/.rt_sessions",
1120 # The current session is identified by the currently configured
1122 ($s, $u) = @config{"server", "user"};
1124 bless $self, $class;
1130 # Returns the current session cookie.
1133 my $cookie = $self->{sids}{$s}{$u};
1134 return defined $cookie ? "RT_SID_$cookie" : undef;
1137 # Deletes the current session cookie.
1140 delete $self->{sids}{$s}{$u};
1143 # Adds a Cookie header to an outgoing HTTP request.
1144 sub add_cookie_header {
1145 my ($self, $request) = @_;
1146 my $cookie = $self->cookie();
1148 $request->header(Cookie => $cookie) if defined $cookie;
1151 # Extracts the Set-Cookie header from an HTTP response, and updates
1152 # session information accordingly.
1154 my ($self, $response) = @_;
1155 my $cookie = $response->header("Set-Cookie");
1157 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1158 $self->{sids}{$s}{$u} = $1;
1162 # Loads the session cache from the specified file.
1164 my ($self, $file) = @_;
1165 $file ||= $self->{file};
1167 open( my $handle, '<', $file ) or return 0;
1169 $self->{file} = $file;
1170 my $sids = $self->{sids} = {};
1173 next if /^$/ || /^#/;
1174 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1175 my ($server, $user, $cookie) = split / /, $_;
1176 $sids->{$server}{$user} = $cookie;
1181 # Writes the current session cache to the specified file.
1183 my ($self, $file) = shift;
1184 $file ||= $self->{file};
1186 open( my $handle, '>', "$file" ) or return 0;
1188 my $sids = $self->{sids};
1189 foreach my $server (keys %$sids) {
1190 foreach my $user (keys %{ $sids->{$server} }) {
1191 my $sid = $sids->{$server}{$user};
1193 print $handle "$server $user $sid\n";
1211 # Forms are RFC822-style sets of (field, value) specifications with some
1212 # initial comments and interspersed blank lines allowed for convenience.
1213 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1215 # Each form is parsed into an array with four elements: commented text
1216 # at the start of the form, an array with the order of keys, a hash with
1217 # key/value pairs, and optional error text if the form syntax was wrong.
1219 # Returns a reference to an array of parsed forms.
1223 my @lines = split /\n/, $_[0] if $_[0];
1224 my ($c, $o, $k, $e) = ("", [], {}, "");
1228 my $line = shift @lines;
1230 next LINE if $line eq '';
1232 if ($line eq '--') {
1233 # We reached the end of one form. We'll ignore it if it was
1234 # empty, and store it otherwise, errors and all.
1235 if ($e || $c || @$o) {
1236 push @forms, [ $c, $o, $k, $e ];
1237 $c = ""; $o = []; $k = {}; $e = "";
1241 elsif ($state != -1) {
1242 if ($state == 0 && $line =~ /^#/) {
1243 # Read an optional block of comments (only) at the start
1247 while (@lines && $lines[0] =~ /^#/) {
1248 $c .= "\n".shift @lines;
1252 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1253 # Read a field: value specification.
1257 # Read continuation lines, if any.
1258 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1259 push @v, shift @lines;
1261 pop @v while (@v && $v[-1] eq '');
1263 # Strip longest common leading indent from text.
1265 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1266 $ws = $ls if (!$ws || length($ls) < length($ws));
1268 s/^$ws// foreach @v;
1270 push(@$o, $f) unless exists $k->{$f};
1271 vpush($k, $f, join("\n", @v));
1275 elsif ($line !~ /^#/) {
1276 # We've found a syntax error, so we'll reconstruct the
1277 # form parsed thus far, and add an error marker. (>>)
1279 $e = Form::compose([[ "", $o, $k, "" ]]);
1280 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1284 # We saw a syntax error earlier, so we'll accumulate the
1285 # contents of this form until the end.
1289 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1291 foreach my $l (keys %$k) {
1292 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1298 # Returns text representing a set of forms.
1303 foreach my $form (@$forms) {
1304 my ($c, $o, $k, $e) = @$form;
1317 foreach my $key (@$o) {
1320 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1322 $sp = " "x(length("$key: "));
1323 $sp = " "x4 if length($sp) > 16;
1325 foreach $v (@values) {
1331 push @lines, "$line\n\n";
1334 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1337 push @lines, "$key: $v\n\n";
1340 length($line)+length($v)-rindex($line, "\n") >= 70)
1342 $line .= ",\n$sp$v";
1345 $line = $line ? "$line,$v" : "$key: $v";
1349 $line = "$key:" unless @values;
1351 if ($line =~ /\n/) {
1352 if (@lines && $lines[-1] !~ /\n\n$/) {
1357 push @lines, "$line\n";
1361 $text .= join "", @lines;
1369 return join "\n--\n\n", @text;
1375 # Returns configuration information from the environment.
1376 sub config_from_env {
1379 foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1381 if (exists $ENV{"RT$k"}) {
1382 $env{lc $k} = $ENV{"RT$k"};
1389 # Finds a suitable configuration file and returns information from it.
1390 sub config_from_file {
1394 # We'll use an absolute path if we were given one.
1395 return parse_config_file($rc);
1398 # Otherwise we'll use the first file we can find in the current
1399 # directory, or in one of its (increasingly distant) ancestors.
1401 my @dirs = split /\//, cwd;
1403 my $file = join('/', @dirs, $rc);
1405 return parse_config_file($file);
1408 # Remove the last directory component each time.
1412 # Still nothing? We'll fall back to some likely defaults.
1413 for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
1414 return parse_config_file($_) if (-r $_);
1421 # Makes a hash of the specified configuration file.
1422 sub parse_config_file {
1425 local $_; # $_ may be aliased to a constant, from line 1163
1427 open( my $handle, '<', $file ) or return;
1431 next if (/^#/ || /^\s*$/);
1433 if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1437 die "rt: $file:$.: unknown configuration directive.\n";
1448 my $sub = (caller(1))[3];
1449 $sub =~ s/^main:://;
1450 warn "rt: $sub: @_\n";
1455 eval 'require Term::ReadKey';
1457 die "No password specified (and Term::ReadKey not installed).\n";
1461 Term::ReadKey::ReadMode('noecho');
1462 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1463 Term::ReadKey::ReadMode('restore');
1471 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1475 my $handle = File::Temp->new;
1476 print $handle $text;
1479 system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1481 open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1488 # Add a value to a (possibly multi-valued) hash key.
1490 my ($hash, $key, $val) = @_;
1491 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1493 if (exists $hash->{$key}) {
1494 unless (ref $hash->{$key} eq 'ARRAY') {
1495 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1496 $hash->{$key} = \@v;
1498 push @{ $hash->{$key} }, @val;
1501 $hash->{$key} = $val;
1505 # "Normalise" a hash key that's known to be multi-valued.
1509 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1511 foreach my $line (map {split /\n/} @values) {
1512 # XXX: This should become a real parser, Ã la Text::ParseWords.
1515 my ( $a, $b ) = split /\s*,\s*/, $line, 2;
1518 no warnings 'uninitialized';
1521 while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/
1522 && $a =~ /(\\)+'$/ )) {
1523 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1528 elsif ( $a =~ /^q{/ ) {
1530 while ( $a !~ /}$/ ) {
1532 split /\s*,\s*/, $b, 2;
1542 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1551 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1552 # change both functions at once
1557 foreach (split /\s*,\s*/, $list) {
1558 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1561 return map $_->[0], # schwartzian transform
1563 defined $a->[1] && defined $b->[1]?
1566 :!defined $a->[1] && !defined $b->[1]?
1569 # mix, number must be first
1570 :defined $a->[1]? -1: 1
1572 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1576 sub get_type_argument {
1580 $type = shift @ARGV;
1581 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1582 # We want whine to mention our caller, not us.
1583 @_ = ("Invalid type '$type' specified.");
1588 @_ = ("No type argument specified with -t.");
1592 $type =~ s/s$//; # "Plural". Ugh.
1596 sub get_var_argument {
1600 my $kv = shift @ARGV;
1601 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1602 push @{ $data->{$k} }, $v;
1605 @_ = ("Invalid variable specification: '$kv'.");
1610 @_ = ("No variable argument specified with -S.");
1615 sub is_object_spec {
1616 my ($spec, $type) = @_;
1618 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1619 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1624 my ($action, $type, $rv) = @_;
1626 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1627 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1632 # simplified procedure for parsing date, avoid loading Date::Parse
1633 my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1634 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1636 my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1637 if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1638 ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1639 $mon = $month{$monstr} if exists $month{$monstr};
1640 } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1641 ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1643 if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1644 return timelocal($sec,$min,$hr,$day,$mon,$yr);
1646 print "Unknown date format in parsedate: $_\n";
1652 my ($old, $new) = @_;
1653 $new = time() if ! $new;
1654 $old = str2time($old) if $old !~ /^\d+$/;
1655 $new = str2time($new) if $new !~ /^\d+$/;
1656 return "???" if ! $old or ! $new;
1658 my %seconds = (min => 60,
1663 yr => 60*60*24*365);
1665 my $diff = $new - $old;
1667 my $howmuch = $diff;
1668 for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1669 last if $diff < $seconds{$_};
1671 $howmuch = int($diff/$seconds{$_});
1673 return "$howmuch $what";
1678 my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1680 # dates are in local time zone
1682 print "Date: $k->{Created}\n";
1683 print "From: $k->{Requestors}\n";
1684 print "Cc: $k->{Cc}\n" if $k->{Cc};
1685 print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1686 print "X-Queue: $k->{Queue}\n";
1687 print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1689 # dates in these attributes are in GMT and will be converted
1690 foreach my $form (@$forms) {
1691 my ($c, $o, $k, $e) = @$form;
1692 next if ! $k->{id} or exists $k->{Queue};
1693 if ( exists $k->{Created} ) {
1694 my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1696 my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1697 if ( exists $k->{Description} ) {
1698 print "===> $k->{Description} on $created\n";
1701 print "$k->{Content}\n" if exists $k->{Content} and
1702 $k->{Content} !~ /to have no content$/ and
1703 ($k->{Type}||'') ne 'EmailRecord';
1704 print "$k->{Attachments}\n" if exists $k->{Attachments} and
1711 my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1712 $heading .= '-' x 80 . "\n";
1714 foreach my $form (@$forms) {
1715 my ($c, $o, $k, $e) = @$form;
1717 print $heading if $heading;
1720 $id =~ s!^ticket/!!;
1721 my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1722 $owner = substr($owner, 0, 5);
1723 my $queue = substr($k->{Queue}, 0, 5);
1724 my $subject = substr($k->{Subject}, 0, 30);
1725 my $age = date_diff($k->{Created});
1726 my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1727 my $status = substr($k->{Status}, 0, 6);
1728 my $requestor = substr($k->{Requestors}, 0, 9);
1729 my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1730 $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1731 if ( $k->{Owner} eq 'Nobody' ) {
1733 } elsif ($k->{Owner} eq $config{user} ) {
1739 print "No matches found\n" if $heading;
1740 printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1742 printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1743 print @open if @open;
1752 This is a command-line interface to RT 3.0 or newer.
1754 It allows you to interact with an RT server over HTTP, and offers an
1755 interface to RT's functionality that is better-suited to automation
1756 and integration with other tools.
1758 In general, each invocation of this program should specify an action
1759 to perform on one or more objects, and any other arguments required
1760 to complete the desired action.
1762 For more information:
1764 - rt help usage (syntax information)
1765 - rt help objects (how to specify objects)
1766 - rt help actions (a list of possible actions)
1767 - rt help types (a list of object types)
1769 - rt help config (configuration details)
1770 - rt help examples (a few useful examples)
1771 - rt help topics (a list of help topics)
1781 rt <action> [options] [arguments]
1785 Each invocation of this program must specify an action (e.g. "edit",
1786 "create"), options to modify behaviour, and other arguments required
1787 by the specified action. (For example, most actions expect a list of
1788 numeric object IDs to act upon.)
1790 The details of the syntax and arguments for each action are given by
1791 "rt help <action>". Some actions may be referred to by more than one
1792 name ("create" is the same as "new", for example).
1794 You may also call "rt shell", which will give you an 'rt>' prompt at
1795 which you can issue commands of the form "<action> [options]
1796 [arguments]". See "rt help shell" for details.
1798 Objects are identified by a type and an ID (which can be a name or a
1799 number, depending on the type). For some actions, the object type is
1800 implied (you can only comment on tickets); for others, the user must
1801 specify it explicitly. See "rt help objects" for details.
1803 In syntax descriptions, mandatory arguments that must be replaced by
1804 appropriate value are enclosed in <>, and optional arguments are
1805 indicated by [] (for example, <action> and [options] above).
1807 For more information:
1809 - rt help objects (how to specify objects)
1810 - rt help actions (a list of actions)
1811 - rt help types (a list of object types)
1812 - rt help shell (how to use the shell)
1818 Title: configuration
1821 This program has two major sources of configuration information: its
1822 configuration files, and the environment.
1824 The program looks for configuration directives in a file named .rtrc
1825 (or $RTCONFIG; see below) in the current directory, and then in more
1826 distant ancestors, until it reaches /. If no suitable configuration
1827 files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
1830 Configuration directives:
1832 The following directives may occur, one per line:
1834 - server <URL> URL to RT server.
1835 - user <username> RT username.
1836 - passwd <passwd> RT user's password.
1837 - query <RT Query> Default RT Query for list action
1838 - orderby <order> Default RT order for list action
1839 - queue <queuename> Default RT Queue for list action
1840 - externalauth <0|1> Use HTTP Basic authentication
1841 explicitely setting externalauth to 0 inhibits also GSSAPI based
1842 authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1844 Blank and #-commented lines are ignored.
1846 Sample configuration file contents:
1848 server https://rt.somewhere.com/
1849 # more than one queue can be given (by adding a query expression)
1850 queue helpdesk or queue=support
1851 query Status != resolved and Owner=myaccount
1854 Environment variables:
1856 The following environment variables override any corresponding
1857 values defined in configuration files:
1863 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1864 - RTCONFIG Specifies a name other than ".rtrc" for the
1866 - RTQUERY Default RT Query for rt list
1867 - RTORDERBY Default order for rt list
1876 <type>/<id>[/<attributes>]
1878 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1879 ID. Some types of objects can also be identified by name (like users
1880 and queues). Furthermore, objects may have named attributes (such as
1881 "ticket/1/history").
1883 An object specification is like a path in a virtual filesystem, with
1884 object types as top-level directories, object IDs as subdirectories,
1885 and named attributes as further subdirectories.
1887 A comma-separated list of names, numeric IDs, or numeric ranges can
1888 be used to specify more than one object of the same type. Note that
1889 the list must be a single argument (i.e., no spaces). For example,
1890 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1891 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1893 If just a number is given as object specification it will be
1894 interpreted as ticket/<number>
1898 1 # the same as ticket/1
1900 ticket/1/attachments
1901 ticket/1/attachments/3
1902 ticket/1/attachments/3/content
1904 ticket/1-3,5-7/history
1908 For more information:
1910 - rt help <action> (action-specific details)
1911 - rt help <type> (type-specific details)
1919 You can currently perform the following actions on all objects:
1921 - list (list objects matching some condition)
1922 - show (display object details)
1923 - edit (edit object details)
1924 - create (create a new object)
1926 Each type may define actions specific to itself; these are listed in
1927 the help item about that type.
1929 For more information:
1931 - rt help <action> (action-specific details)
1932 - rt help types (a list of possible types)
1934 The following actions on tickets are also possible:
1936 - comment Add comments to a ticket
1937 - correspond Add comments to a ticket
1938 - merge Merge one ticket into another
1939 - link Link one ticket to another
1940 - take Take a ticket (steal and untake are possible as well)
1942 For several edit set subcommands that are frequently used abbreviations
1943 have been introduced. These abbreviations are:
1945 - delete or del delete a ticket (edit set status=deleted)
1946 - resolve or res resolve a ticket (edit set status=resolved)
1947 - subject change subject of ticket (edit set subject=string)
1948 - give give a ticket to somebody (edit set owner=user)
1955 You can currently operate on the following types of objects:
1962 For more information:
1964 - rt help <type> (type-specific details)
1965 - rt help objects (how to specify objects)
1966 - rt help actions (a list of possible actions)
1973 Tickets are identified by a numeric ID.
1975 The following generic operations may be performed upon tickets:
1982 In addition, the following ticket-specific actions exist:
1998 The following attributes can be used with "rt show" or "rt edit"
1999 to retrieve or edit other information associated with tickets:
2001 links A ticket's relationships with others.
2002 history All of a ticket's transactions.
2003 history/type/<type> Only a particular type of transaction.
2004 history/id/<id> Only the transaction of the specified id.
2005 attachments A list of attachments.
2006 attachments/<id> The metadata for an individual attachment.
2007 attachments/<id>/content The content of an individual attachment.
2015 Users and groups are identified by name or numeric ID.
2017 The following generic operations may be performed upon them:
2029 Queues are identified by name or numeric ID.
2031 Currently, they can be subjected to the following actions:
2044 rt subject <id> <new subject text>
2046 Change the subject of a ticket whose ticket id is given.
2055 rt give <id> <accountname>
2057 Give a ticket whose ticket id is given to another user.
2066 Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2077 Take a ticket whose ticket id is given, i.e. set the owner to myself.
2088 Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2100 Resolves a ticket whose ticket id is given.
2112 Deletes a ticket whose ticket id is given.
2123 Terminates the currently established login session. You will need to
2124 provide authentication credentials before you can continue using the
2125 server. (See "rt help config" for details about authentication.)
2136 rt <ls|list|search> [options] "query string"
2138 Displays a list of objects matching the specified conditions.
2139 ("ls", "list", and "search" are synonyms.)
2141 Conditions are expressed in the SQL-like syntax used internally by
2142 RT. (For more information, see "rt help query".) The query string
2143 must be supplied as one argument.
2145 (Right now, the server doesn't support listing anything but tickets.
2146 Other types will be supported in future; this client will be able to
2147 take advantage of that support without any changes.)
2151 The following options control how much information is displayed
2152 about each matching object:
2154 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
2155 -s Short description.
2156 -l Longer description.
2157 -f <field[s] Display only the fields listed and the ticket id
2161 -o +/-<field> Orders the returned list by the specified field.
2162 -r reversed order (useful if a default was given)
2163 -q queue[s] restricts the query to the queue[s] given
2164 multiple queues are separated by comma
2165 -S var=val Submits the specified variable with the request.
2166 -t type Specifies the type of object to look for. (The
2167 default is "ticket".)
2171 rt ls "Priority > 5 and Status=new"
2172 rt ls -o +Subject "Priority > 5 and Status=new"
2173 rt ls -o -Created "Priority > 5 and Status=new"
2174 rt ls -i "Priority > 5"|rt edit - set status=resolved
2175 rt ls -t ticket "Subject like '[PATCH]%'"
2177 rt ls -f owner,subject
2186 rt show [options] <object-ids>
2188 Displays details of the specified objects.
2190 For some types, object information is further classified into named
2191 attributes (for example, "1-3/links" is a valid ticket specification
2192 that refers to the links for tickets 1-3). Consult "rt help <type>"
2193 and "rt help objects" for further details.
2195 If only a number is given it will be interpreted as the objects
2196 ticket/number and ticket/number/history
2198 This command writes a set of forms representing the requested object
2203 The following options control how much information is displayed
2204 about each matching object:
2206 Without any formatting options prettyprinted output is generated.
2207 Giving any of the two options below reverts to raw output.
2208 -s Short description (history and attachments only).
2209 -l Longer description (history and attachments only).
2212 - Read IDs from STDIN instead of the command-line.
2213 -t type Specifies object type.
2214 -f a,b,c Restrict the display to the specified fields.
2215 -S var=val Submits the specified variable with the request.
2219 rt show -t ticket -f id,subject,status 1-3
2220 rt show ticket/3/attachments/29
2221 rt show ticket/3/attachments/29/content
2222 rt show ticket/1-3/links
2223 rt show ticket/3/history
2224 rt show -l ticket/3/history
2237 rt edit [options] <object-ids> set field=value [field=value] ...
2238 add field=value [field=value] ...
2239 del field=value [field=value] ...
2241 Edits information corresponding to the specified objects.
2243 A purely numeric object id nnn is translated into ticket/nnn
2245 If, instead of "edit", an action of "new" or "create" is specified,
2246 then a new object is created. In this case, no numeric object IDs
2247 may be specified, but the syntax and behaviour remain otherwise
2250 This command typically starts an editor to allow you to edit object
2251 data in a form for submission. If you specified enough information
2252 on the command-line, however, it will make the submission directly.
2254 The command line may specify field-values in three different ways.
2255 "set" sets the named field to the given value, "add" adds a value
2256 to a multi-valued field, and "del" deletes the corresponding value.
2257 Each "field=value" specification must be given as a single argument.
2259 For some types, object information is further classified into named
2260 attributes (for example, "1-3/links" is a valid ticket specification
2261 that refers to the links for tickets 1-3). These attributes may also
2262 be edited. Consult "rt help <type>" and "rt help object" for further
2267 - Read numeric IDs from STDIN instead of the command-line.
2268 (Useful with rt ls ... | rt edit -; see examples below.)
2269 -i Read a completed form from STDIN before submitting.
2270 -o Dump the completed form to STDOUT instead of submitting.
2271 -e Allows you to edit the form even if the command-line has
2272 enough information to make a submission directly.
2274 Submits the specified variable with the request.
2275 -t type Specifies object type.
2279 # Interactive (starts $EDITOR with a form).
2284 rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2285 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2286 rt edit ticket/4 set priority=3 owner=bar@example.com \
2287 add cc=foo@example.com bcc=quux@example.net
2288 rt create -t ticket set subject='new ticket' priority=10 \
2289 add cc=foo@example.com
2299 rt <comment|correspond> [options] <ticket-id>
2301 Adds a comment (or correspondence) to the specified ticket (the only
2302 difference being that comments aren't sent to the requestors.)
2304 This command will typically start an editor and allow you to type a
2305 comment into a form. If, however, you specified all the necessary
2306 information on the command line, it submits the comment directly.
2308 (See "rt help forms" for more information about forms.)
2312 -m <text> Specify comment text.
2313 -a <file> Attach a file to the comment. (May be used more
2314 than once to attach multiple files.)
2315 -c <addrs> A comma-separated list of Cc addresses.
2316 -b <addrs> A comma-separated list of Bcc addresses.
2317 -w <time> Specify the time spent working on this ticket.
2318 -e Starts an editor before the submission, even if
2319 arguments from the command line were sufficient.
2323 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2332 rt merge <from-id> <to-id>
2334 Merges the first ticket specified into the second ticket specified.
2343 rt link [-d] <id-A> <link> <id-B>
2345 Creates (or, with -d, deletes) a link between the specified tickets.
2346 The link can (irrespective of case) be any of:
2348 DependsOn/DependedOnBy: A depends upon B (or vice versa).
2349 RefersTo/ReferredToBy: A refers to B (or vice versa).
2350 MemberOf/HasMember: A is a member of B (or vice versa).
2352 To view a ticket's links, use "rt show ticket/3/links". (See
2353 "rt help ticket" and "rt help show".)
2357 -d Deletes the specified link.
2361 rt link 2 dependson 3
2362 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
2369 RT uses an SQL-like syntax to specify object selection constraints.
2370 See the <RT:...> documentation for details.
2372 (XXX: I'm going to have to write it, aren't I?)
2374 Until it exists here a short description of important constructs:
2376 The two simple forms of query expressions are the constructs
2377 Attribute like Value and
2378 Attribute = Value or Attribute != Value
2380 Whether attributes can be matched using like or using = is built into RT.
2381 The attributes id, Queue, Owner Priority and Status require the = or !=
2384 If Value is a string it must be quoted and may contain the wildcard
2385 character %. If the string does not contain white space, the quoting
2386 may however be omitted, it will be added automatically when parsing
2389 Simple query expressions can be combined using and, or and parentheses
2390 can be used to group expressions.
2392 As a special case a standalone string (which would not form a correct
2393 query) is transformed into (Owner='string' or Requestor like 'string%')
2394 and added to the default query, i.e. the query is narrowed down.
2396 If no Queue=name clause is contained in the query, a default clause
2397 Queue=$config{queue} is added.
2400 Status!='resolved' and Status!='rejected'
2401 (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2409 This program uses RFC822 header-style forms to represent object data
2410 in a form that's suitable for processing both by humans and scripts.
2412 A form is a set of (field, value) specifications, with some initial
2413 commented text and interspersed blank lines allowed for convenience.
2414 Field names may appear more than once in a form; a comma-separated
2415 list of multiple field values may also be specified directly.
2417 Field values can be wrapped as in RFC822, with leading whitespace.
2418 The longest sequence of leading whitespace common to all the lines
2419 is removed (preserving further indentation). There is no limit on
2420 the length of a value.
2422 Multiple forms are separated by a line containing only "--\n".
2424 (XXX: A more detailed specification will be provided soon. For now,
2425 the server-side syntax checking will suffice.)
2436 Get help on any of the following subjects:
2438 - tickets, users, groups, queues.
2439 - show, edit, ls/list/search, new/create.
2441 - query (search query syntax)
2442 - forms (form specification)
2444 - objects (how to specify objects)
2445 - types (a list of object types)
2446 - actions/commands (a list of actions)
2447 - usage/syntax (syntax details)
2448 - conf/config/configuration (configuration details)
2449 - examples (a few useful examples)
2457 some useful examples
2459 All the following list requests will be restricted to the default queue.
2460 That can be changed by adding the option -q queuename
2462 List all tickets that are not rejected/resolved
2464 List all tickets that are new and do not have an owner
2465 rt ls "status=new and owner=nobody"
2466 List all tickets which I have sent or of which I am the owner
2468 List all attributes for the ticket 6977 (ls -l instead of ls)
2470 Show the content of ticket 6977
2472 Show all attributes in the ticket and in the history of the ticket
2474 Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2476 This will open an editor and lets you add text (attribute Text:)
2477 Other attributes may be changed as well, but usually don't do that.
2478 Correspond a ticket (like comment, but mail is also sent to requestors)
2480 Edit a ticket (generic change, interactive using the editor)
2482 Change the owner of a ticket non interactively
2483 rt edit 6977 set owner=myaccount
2485 rt give 6977 account
2488 Change the status of a ticket
2489 rt edit 6977 set status=resolved
2492 Change the status of all tickets I own to resolved !!!
2493 rt ls -i owner=myaccount | rt edit - set status=resolved
2504 Opens an interactive shell, at which you can issue commands of
2505 the form "<action> [options] [arguments]".
2507 To exit the shell, type "quit" or "exit".
2509 Commands can be given at the shell in the same form as they would
2510 be given at the command line without the leading 'rt' invocation.
2514 rt> create -t ticket set subject='new' add cc=foo@example.com
2528 rt <take|untake|steal> <ticket-id>
2530 Sets the owner of the specified ticket to the current user,
2531 assuming said user has the bits to do so, or releases the
2534 'Take' is used on tickets which are not currently owned
2535 (Owner: Nobody), 'steal' is used on tickets which *are*
2536 currently owned, and 'untake' is used to "release" a ticket
2537 (reset its Owner to Nobody). 'Take' cannot be used on
2538 tickets which are currently owned.
2541 alice$ rt create -t ticket set subject="New ticket"
2544 # Owner changed from Nobody to alice
2547 # Owner changed from alice to bob
2549 # Owner changed from bob to Nobody
2557 Use "quit" or "exit" to leave the shell. Only valid within shell
2569 rt - command-line interface to RT 3.0 or newer
2577 This script allows you to interact with an RT server over HTTP, and offers an
2578 interface to RT's functionality that is better-suited to automation and
2579 integration with other tools.
2581 In general, each invocation of this program should specify an action to
2582 perform on one or more objects, and any other arguments required to complete