2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2015 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 # We derive configuration information from hardwired defaults, dotfiles,
74 # and the RT* environment variables (in increasing order of precedence).
75 # Session information is stored in ~/.rt_sessions.
78 my $HOME = eval{(getpwuid($<))[7]}
79 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
84 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
86 server => 'http://localhost/',
87 query => "Status!='resolved' and Status!='rejected'",
90 # to protect against unlimited searches a better choice would be
91 # queue => 'Unknown_Queue',
94 config_from_file($ENV{RTCONFIG} || ".rtrc"),
98 $config{auth} = "basic" if delete $config{externalauth};
100 my $session = Session->new("$HOME/.rt_sessions");
101 my $REST = "$config{server}/REST/1.0";
106 sub DEBUG { warn @_ if $config{debug} >= shift }
108 # These regexes are used by command handlers to parse arguments.
109 # (XXX: Ask Autrijus how i18n changes these definitions.)
111 my $name = '[\w.-]+';
112 my $CF_name = '[^,]+?';
113 my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
114 my $label = '[^,\\/]+';
115 my $labels = "(?:$label,)*$label";
116 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
118 # Our command line looks like this:
120 # rt <action> [options] [arguments]
122 # We'll parse just enough of it to decide upon an action to perform, and
123 # leave the rest to per-action handlers to interpret appropriately.
126 # handler => [ ...aliases... ],
127 version => ["version", "ver"],
129 logout => ["logout"],
130 help => ["help", "man"],
131 show => ["show", "cat"],
132 edit => ["create", "edit", "new", "ed"],
133 list => ["search", "list", "ls"],
134 comment => ["comment", "correspond"],
135 link => ["link", "ln"],
137 grant => ["grant", "revoke"],
138 take => ["take", "steal", "untake"],
139 quit => ["quit", "exit"],
140 setcommand => ["del", "delete", "give", "res", "resolve",
145 foreach my $fn (keys %handlers) {
146 foreach my $alias (@{ $handlers{$fn} }) {
147 $actions{$alias} = \&{"$fn"};
151 # Once we find and call an appropriate handler, we're done.
156 push @ARGV, 'shell' if (!@ARGV); # default to shell mode
157 shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
158 if (@ARGV && exists $actions{$ARGV[0]}) {
159 $action = shift @ARGV;
160 return $actions{$action}->($action);
163 print STDERR "rt: Unknown command '@ARGV'.\n";
164 print STDERR "rt: For help, run 'rt help'.\n";
174 # The following subs are handlers for each entry in %actions.
178 my $term = Term::ReadLine->new('RT CLI');
179 while ( defined ($_ = $term->readline($prompt)) ) {
180 next if /^#/ || /^\s*$/;
182 @ARGV = shellwords($_);
188 print "rt $VERSION\n";
193 submit("$REST/logout") if defined $session->cookie;
204 my ($action, $type, $rv) = @_;
205 $rv = defined $rv ? $rv : 0;
208 # What help topics do we know about?
211 foreach my $item (@{ Form::parse(<DATA>) }) {
212 my $title = $item->[2]{Title};
213 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
215 foreach $title (grep $_, @titles) {
216 $help{$title} = $item->[2]{Text};
221 # What does the user want help with?
222 undef $action if ($action && $actions{$action} eq \&help);
223 unless ($action || $type) {
224 # If we don't know, we'll look for clues in @ARGV.
226 if (exists $help{$_}) { $key = $_; last; }
229 # Tolerate possibly plural words.
231 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
236 if ($type && $action) {
237 $key = "$type.$action";
239 $key ||= $type || $action || "introduction";
241 # Find a suitable topic to display.
242 while (!exists $help{$key}) {
243 if ($type && $action) {
244 if ($key eq "$type.$action") { $key = $action; }
245 elsif ($key eq $action) { $key = $type; }
246 else { $key = "introduction"; }
249 $key = "introduction";
253 print STDERR $help{$key}, "\n\n";
257 # Displays a list of objects that match some specified condition.
260 my ($q, $type, %data);
261 my $orderby = $config{orderby};
263 if ($config{orderby}) {
264 $data{orderby} = $config{orderby};
268 my $reverse_sort = 0;
269 my $queue = $config{queue};
275 $bad = 1, last unless defined($type = get_type_argument());
278 $bad = 1, last unless get_var_argument(\%data);
281 $data{'orderby'} = shift @ARGV;
283 elsif (/^-([isl])$/) {
288 $queue = shift @ARGV;
294 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
295 whine "No valid field list in '-f $ARGV[0]'.";
298 $data{fields} = shift @ARGV;
299 $data{format} = 's' if ! $data{format};
302 elsif (!defined $q && !/^-/) {
306 my $datum = /^-/ ? "option" : "argument";
307 whine "Unrecognised $datum '$_'.";
311 if ( ! $rawprint and ! exists $data{format} ) {
313 $data{fields} = 'subject,status,queue,created,told,owner,requestors';
315 if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
316 $data{orderby} =~ s/^-/+/;
317 } elsif ($reverse_sort) {
318 $data{orderby} =~ s/^\+?(.*)/-$1/;
324 if ( $type eq 'ticket' ) {
332 if ( $type ne 'ticket' ) {
336 unless (defined $q) {
337 my $item = $type ? "query string" : "object type";
338 whine "No $item specified.";
342 $q =~ s/^#//; # get rid of leading hash
343 if ( $type eq 'ticket' ) {
344 if ( $q =~ /^\d+$/ ) {
346 # only digits, must be an id, formulate a correct query
347 $q = "id=$q" if $q =~ /^\d+$/;
351 # a string only, take it as an owner or requestor (quoting done later)
352 $q = "(Owner=$q or Requestor like $q) and $config{query}"
353 if $q =~ /^[\w\-]+$/;
355 # always add a query for a specific queue or (comma separated) queues
356 $queue =~ s/,/ or Queue=/g if $queue;
357 $q .= " and (Queue=$queue)"
360 and $q !~ /Queue\s*=/i
364 # correctly quote strings in a query
365 $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
368 #return help("list", $type) if $bad;
369 return suggest_help("list", $type, $bad) if $bad;
371 print "Query:$q\n" if ! $rawprint;
372 my $r = submit("$REST/search/$type", { query => $q, %data });
376 my $forms = Form::parse($r->content);
382 # Displays selected information about a single object.
385 my ($type, @objects, %data);
393 s/^#// if /^#\d+/; # get rid of leading hash
395 $bad = 1, last unless defined($type = get_type_argument());
398 $bad = 1, last unless get_var_argument(\%data);
400 elsif (/^-([isl])$/) {
404 elsif (/^-$/ && !$slurped) {
405 chomp(my @lines = <STDIN>);
407 unless (is_object_spec($_, $type)) {
408 whine "Invalid object on STDIN: '$_'.";
416 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
417 whine "No valid field list in '-f $ARGV[0]'.";
420 $data{fields} = shift @ARGV;
421 # option f requires short raw listing format
425 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
426 push @objects, $spc2;
427 $histspec = is_object_spec("ticket/$_/history", $type);
429 elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
430 push @objects, $spc3;
431 $rawprint = 1 if $_ =~ /\/content$/;
433 elsif (my $spec = is_object_spec($_, $type)) {
434 push @objects, $spec;
435 $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
438 my $datum = /^-/ ? "option" : "argument";
439 whine "Unrecognised $datum '$_'.";
444 push @objects, $histspec if $histspec;
445 $data{format} = 'l' if ! exists $data{format};
449 whine "No objects specified.";
452 #return help("show", $type) if $bad;
453 return suggest_help("show", $type, $bad) if $bad;
455 my $r = submit("$REST/show", { id => \@objects, %data });
457 # if this isn't a text reply, remove the trailing newline so we
458 # don't corrupt things like tarballs when people do
459 # show ticket/id/attachments/id/content > foo.tar.gz
460 if ($r->content_type !~ /^text\//) {
467 # I do not know how to get more than one form correctly returned
468 $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
469 my $forms = Form::parse($c);
475 # To create a new object, we ask the server for a form with the defaults
476 # filled in, allow the user to edit it, and send the form back.
478 # To edit an object, we must ask the server for a form representing that
479 # object, make changes requested by the user (either on the command line
480 # or interactively via $EDITOR), and send the form back.
484 my (%data, $type, @objects);
485 my ($cl, $text, $edit, $input, $output, $content_type);
487 use vars qw(%set %add %del);
488 %set = %add = %del = ();
494 s/^#// if /^#\d+/; # get rid of leading hash
496 if (/^-e$/) { $edit = 1 }
497 elsif (/^-i$/) { $input = 1 }
498 elsif (/^-o$/) { $output = 1 }
499 elsif (/^-ct$/) { $content_type = shift @ARGV }
501 $bad = 1, last unless defined($type = get_type_argument());
504 $bad = 1, last unless get_var_argument(\%data);
506 elsif (/^-$/ && !($slurped || $input)) {
507 chomp(my @lines = <STDIN>);
509 unless (is_object_spec($_, $type)) {
510 whine "Invalid object on STDIN: '$_'.";
520 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
521 my ($key, $op, $val) = ($1, $2, $3);
522 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
524 vpush($hash, lc $key, $val);
529 whine "No variables to set.";
534 elsif (/^(?:add|del)$/i) {
536 my $hash = ($_ eq "add") ? \%add : \%del;
538 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
539 my ($key, $val) = ($1, $2);
541 vpush($hash, lc $key, $val);
546 whine "No variables to set.";
551 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
552 push @objects, $spc2;
554 elsif (my $spec = is_object_spec($_, $type)) {
555 push @objects, $spec;
558 my $datum = /^-/ ? "option" : "argument";
559 whine "Unrecognised $datum '$_'.";
564 if ($action =~ /^ed(?:it)?$/) {
566 whine "No objects specified.";
572 whine "You shouldn't specify objects as arguments to $action.";
576 whine "What type of object do you want to create?";
579 @objects = ("$type/new") if defined($type);
581 #return help($action, $type) if $bad;
582 return suggest_help($action, $type, $bad) if $bad;
584 # We need a form to make changes to. We usually ask the server for
585 # one, but we can avoid that if we are fed one on STDIN, or if the
586 # user doesn't want to edit the form by hand, and the command line
587 # specifies only simple variable assignments. We *should* get a
588 # form if we're creating a new ticket, so that the default values
589 # get filled in properly.
591 my @new_objects = grep /\/new$/, @objects;
597 elsif ($edit || %add || %del || !$cl || @new_objects) {
598 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
602 # If any changes were specified on the command line, apply them.
605 # We're updating forms from the server.
606 my $forms = Form::parse($text);
608 foreach my $form (@$forms) {
609 my ($c, $o, $k, $e) = @$form;
612 next if ($e || !@$o);
618 # Make changes to existing fields.
620 if (exists $add{lc $key}) {
621 $val = delete $add{lc $key};
622 vpush($k, $key, $val);
623 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
625 if (exists $del{lc $key}) {
626 $val = delete $del{lc $key};
627 my %val = map {$_=>1} @{ vsplit($val) };
628 $k->{$key} = vsplit($k->{$key});
629 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
631 if (exists $set{lc $key}) {
632 $k->{$key} = delete $set{lc $key};
636 # Then update the others.
637 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
638 foreach $key (keys %add) {
639 vpush($k, $key, $add{$key});
640 $k->{$key} = vsplit($k->{$key});
642 push @$o, (keys %add, keys %set);
645 $text = Form::compose($forms);
648 # We're rolling our own set of forms.
651 my ($type, $ids, $args) =
652 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
655 foreach my $obj (expand_list($ids)) {
656 my %set = (%set, id => "$type/$obj$args");
657 push @forms, ["", [keys %set], \%set];
660 $text = Form::compose(\@forms);
670 @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
675 # We'll let the user edit the form before sending it to the server,
676 # unless we have enough information to submit it non-interactively.
677 if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
678 $text .= "Content-Type: $content_type\n"
679 if $content_type and $content_type ne "text/plain";
682 if ($edit || (!$input && !$cl)) {
683 my ($newtext) = vi_form_while(
686 my ($text, $form) = @_;
687 return 1 unless exists $form->[2]{'Attachment'};
689 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
690 return (0, "File '$f' doesn't exist") unless -f $f;
692 @files = @{ vsplit($form->[2]{'Attachment'}) };
696 return $newtext unless $newtext;
697 # We won't resubmit a bad form unless it was changed.
698 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
701 delete @data{ grep /^attachment_\d+$/, keys %data };
703 foreach my $file (@files) {
704 $data{"attachment_$i"} = bless([ $file ], "Attachment");
709 my $r = submit("$REST/edit", {content => $text, %data});
710 if ($r->code == 409) {
711 # If we submitted a bad form, we'll give the user a chance
712 # to correct it and resubmit.
713 if ($edit || (!$input && !$cl)) {
714 my $content = $r->content . "\n";
715 $content =~ s/^(?!#)/# /mg;
716 $text = $content . $text;
730 # handler for special edit commands. A valid edit command is constructed and
731 # further work is delegated to the edit handler
735 my ($id, $bad, $what);
738 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
742 whine "No ticket number specified.";
745 if ($action eq 'subject') {
746 my $subject = '"'.join (" ", @ARGV).'"';
748 $what = "subject=$subject";
749 } elsif ($action eq 'give') {
750 my $owner = shift @ARGV;
751 $what = "owner=$owner";
754 if ( $action eq 'delete' or $action eq 'del' ) {
755 $what = "status=deleted";
756 } elsif ($action eq 'resolve' or $action eq 'res' ) {
757 $what = "status=resolved";
758 } elsif ($action eq 'take' ) {
759 $what = "owner=$config{user}";
760 } elsif ($action eq 'untake') {
761 $what = "owner=Nobody";
766 whine "Extraneous arguments for action $action: @ARGV.";
770 whine "unrecognized action $action.";
772 return help("edit", undef, $bad) if $bad;
773 @ARGV = ( $id, "set", $what );
774 print "Executing: rt edit @ARGV\n";
778 # We roll "comment" and "correspond" into the same handler.
782 my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
792 elsif (/^-(?:[abcmws]|ct)$/) {
794 whine "No argument specified with $_.";
799 unless (-f $ARGV[0] && -r $ARGV[0]) {
800 whine "Cannot read attachment: '$ARGV[0]'.";
803 push @files, shift @ARGV;
806 $content_type = shift @ARGV;
809 $status = shift @ARGV;
812 my $a = $_ eq "-b" ? \@bcc : \@cc;
813 @$a = split /\s*,\s*/, shift @ARGV;
817 if ( $msg =~ /^-$/ ) {
819 while (<STDIN>) { $msg .= $_ }
822 elsif (/-w/) { $wtime = shift @ARGV }
824 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
828 my $datum = /^-/ ? "option" : "argument";
829 whine "Unrecognised $datum '$_'.";
835 whine "No object specified.";
838 #return help($action, "ticket") if $bad;
839 return suggest_help($action, "ticket") if $bad;
843 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
849 Attachment => [ @files ],
850 TimeWorked => $wtime || '',
851 'Content-Type' => $content_type || 'text/plain',
857 push(@{$form->[1]}, "Status");
860 my $text = Form::compose([ $form ]);
862 if ($edit || !$msg) {
863 my ($tmp) = vi_form_while(
866 my ($text, $form) = @_;
867 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
868 return (0, "File '$f' doesn't exist") unless -f $f;
870 @files = @{ vsplit($form->[2]{'Attachment'}) };
874 return $tmp unless $tmp;
879 foreach my $file (@files) {
880 $data{"attachment_$i"} = bless([ $file ], "Attachment");
883 $data{content} = $text;
885 my $r = submit("$REST/ticket/$id/comment", \%data);
890 # Merge one ticket into another.
898 s/^#// if /^#\d+/; # get rid of leading hash
904 whine "Unrecognised argument: '$_'.";
910 my $evil = @id > 2 ? "many" : "few";
911 whine "Too $evil arguments specified.";
914 #return help("merge", "ticket") if $bad;
915 return suggest_help("merge", "ticket", $bad) if $bad;
917 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
922 # Link one ticket to another.
925 my ($bad, $del, %data) = (0, 0, ());
928 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
929 ReferredToBy HasMember MemberOf);
931 while (@ARGV && $ARGV[0] =~ /^-/) {
938 $bad = 1, last unless defined($type = get_type_argument());
941 whine "Unrecognised option: '$_'.";
946 $type = "ticket" unless $type; # default type to tickets
949 my ($from, $rel, $to) = @ARGV;
950 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
951 whine "Invalid link '$rel' for type $type specified.";
954 %data = (id => $from, rel => $rel, to => $to, del => $del);
957 my $bad = @ARGV < 3 ? "few" : "many";
958 whine "Too $bad arguments specified.";
961 return suggest_help("link", $type, $bad) if $bad;
963 my $r = submit("$REST/$type/link", \%data);
968 # Take/steal a ticket
971 my ($bad, %data) = (0, ());
978 unless ($id =~ /^\d+$/) {
979 whine "Invalid ticket ID $id specified.";
984 [ "Ticket", "Action" ],
992 my $text = Form::compose([ $form ]);
993 $data{content} = $text;
996 $bad = @ARGV < 1 ? "few" : "many";
997 whine "Too $bad arguments specified.";
1000 return suggest_help("take", "ticket", $bad) if $bad;
1002 my $r = submit("$REST/ticket/$id/take", \%data);
1007 # Grant/revoke a user's rights.
1012 whine "$cmd is unimplemented.";
1016 # Client <-> Server communication.
1017 # --------------------------------
1019 # This function composes and sends an HTTP request to the RT server, and
1020 # interprets the response. It takes a request URI, and optional request
1021 # data (a string, or a reference to a set of key-value pairs).
1024 my ($uri, $content) = @_;
1026 my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
1027 my $h = HTTP::Headers->new;
1029 # Did the caller specify any data to send with the request?
1031 if (defined $content) {
1032 unless (ref $content) {
1033 # If it's just a string, make sure LWP handles it properly.
1034 # (By pretending that it's a file!)
1035 $content = [ content => [undef, "", Content => $content] ];
1037 elsif (ref $content eq 'HASH') {
1039 foreach my $k (keys %$content) {
1040 if (ref $content->{$k} eq 'ARRAY') {
1041 foreach my $v (@{ $content->{$k} }) {
1045 else { push @data, $k, $content->{$k} }
1052 # Should we send authentication information to start a new session?
1053 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1054 my($server) = $config{server} =~ m{^.*//([^/]+)};
1056 if ($config{auth} eq "gssapi") {
1057 die "GSSAPI support not available; failed to load perl module GSSAPI:\n$@\n"
1058 unless eval { require GSSAPI; 1 };
1059 die "GSSAPI support not available; failed to load perl module LWP::Authen::Negotiate:\n$@\n"
1060 unless eval { require LWP::Authen::Negotiate; 1 };
1061 } elsif ($config{auth} eq "basic") {
1062 print " Password will be sent to $server $how\n",
1063 " Press CTRL-C now if you do not want to continue\n"
1064 if ! $config{passwd};
1065 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1066 } elsif ( !defined $session->cookie ) {
1067 print " Password will be sent to $server $how\n",
1068 " Press CTRL-C now if you do not want to continue\n"
1069 if ! $config{passwd};
1070 push @$data, ( user => $config{user} );
1071 push @$data, ( pass => $config{passwd} || read_passwd() );
1074 # Now, we construct the request.
1076 $req = POST($uri, $data, Content_Type => 'form-data');
1081 $session->add_cookie_header($req);
1082 $req->header(%$h) if %$h;
1084 # Then we send the request and parse the response.
1085 DEBUG(3, $req->as_string);
1086 my $res = $ua->request($req);
1087 DEBUG(3, $res->as_string);
1089 if ($res->is_success) {
1090 # The content of the response we get from the RT server consists
1091 # of an HTTP-like status line followed by optional header lines,
1092 # a blank line, and arbitrary text.
1094 my ($head, $text) = split /\n\n/, $res->content, 2;
1095 my ($status, @headers) = split /\n/, $head;
1096 $text =~ s/\n*$/\n/ if ($text);
1098 # "RT/3.0.1 401 Credentials required"
1099 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1100 warn "rt: Malformed RT response from $server.\n";
1101 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1105 # Our caller can pretend that the server returned a custom HTTP
1106 # response code and message. (Doing that directly is apparently
1107 # not sufficiently portable and uncomplicated.)
1110 $res->content($text);
1111 $session->update($res) if ($res->is_success || $res->code != 401);
1113 if (!$res->is_success) {
1114 # We can deal with authentication failures ourselves. Either
1115 # we sent invalid credentials, or our session has expired.
1116 if ($res->code == 401) {
1118 if (exists $d{user}) {
1119 warn "rt: Incorrect username or password.\n";
1122 elsif ($req->header("Cookie")) {
1123 # We'll retry the request with credentials, unless
1124 # we only wanted to logout in the first place.
1126 return submit(@_) unless $uri eq "$REST/logout";
1129 # Conflicts should be dealt with by the handler and user.
1130 # For anything else, we just die.
1131 elsif ($res->code != 409) {
1132 warn "rt: ", $res->content;
1138 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1145 # Session management.
1146 # -------------------
1148 # Maintains a list of active sessions in the ~/.rt_sessions file.
1153 # Initialises the session cache.
1155 my ($class, $file) = @_;
1157 file => $file || "$HOME/.rt_sessions",
1161 # The current session is identified by the currently configured
1163 ($s, $u) = @config{"server", "user"};
1165 bless $self, $class;
1171 # Returns the current session cookie.
1174 my $cookie = $self->{sids}{$s}{$u};
1175 return defined $cookie ? "RT_SID_$cookie" : undef;
1178 # Deletes the current session cookie.
1181 delete $self->{sids}{$s}{$u};
1184 # Adds a Cookie header to an outgoing HTTP request.
1185 sub add_cookie_header {
1186 my ($self, $request) = @_;
1187 my $cookie = $self->cookie();
1189 $request->header(Cookie => $cookie) if defined $cookie;
1192 # Extracts the Set-Cookie header from an HTTP response, and updates
1193 # session information accordingly.
1195 my ($self, $response) = @_;
1196 my $cookie = $response->header("Set-Cookie");
1198 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1199 $self->{sids}{$s}{$u} = $1;
1203 # Loads the session cache from the specified file.
1205 my ($self, $file) = @_;
1206 $file ||= $self->{file};
1208 open( my $handle, '<', $file ) or return 0;
1210 $self->{file} = $file;
1211 my $sids = $self->{sids} = {};
1214 next if /^$/ || /^#/;
1215 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1216 my ($server, $user, $cookie) = split / /, $_;
1217 $sids->{$server}{$user} = $cookie;
1222 # Writes the current session cache to the specified file.
1224 my ($self, $file) = shift;
1225 $file ||= $self->{file};
1227 open( my $handle, '>', "$file" ) or return 0;
1229 my $sids = $self->{sids};
1230 foreach my $server (keys %$sids) {
1231 foreach my $user (keys %{ $sids->{$server} }) {
1232 my $sid = $sids->{$server}{$user};
1234 print $handle "$server $user $sid\n";
1252 # Forms are RFC822-style sets of (field, value) specifications with some
1253 # initial comments and interspersed blank lines allowed for convenience.
1254 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1256 # Each form is parsed into an array with four elements: commented text
1257 # at the start of the form, an array with the order of keys, a hash with
1258 # key/value pairs, and optional error text if the form syntax was wrong.
1260 # Returns a reference to an array of parsed forms.
1264 my @lines = split /\n/, $_[0] if $_[0];
1265 my ($c, $o, $k, $e) = ("", [], {}, "");
1269 my $line = shift @lines;
1271 next LINE if $line eq '';
1273 if ($line eq '--') {
1274 # We reached the end of one form. We'll ignore it if it was
1275 # empty, and store it otherwise, errors and all.
1276 if ($e || $c || @$o) {
1277 push @forms, [ $c, $o, $k, $e ];
1278 $c = ""; $o = []; $k = {}; $e = "";
1282 elsif ($state != -1) {
1283 if ($state == 0 && $line =~ /^#/) {
1284 # Read an optional block of comments (only) at the start
1288 while (@lines && $lines[0] =~ /^#/) {
1289 $c .= "\n".shift @lines;
1293 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1294 # Read a field: value specification.
1298 # Read continuation lines, if any.
1299 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1300 push @v, shift @lines;
1302 pop @v while (@v && $v[-1] eq '');
1304 # Strip longest common leading indent from text.
1306 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1307 $ws = $ls if (!$ws || length($ls) < length($ws));
1309 s/^$ws// foreach @v;
1311 push(@$o, $f) unless exists $k->{$f};
1312 vpush($k, $f, join("\n", @v));
1316 elsif ($line !~ /^#/) {
1317 # We've found a syntax error, so we'll reconstruct the
1318 # form parsed thus far, and add an error marker. (>>)
1320 $e = Form::compose([[ "", $o, $k, "" ]]);
1321 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1325 # We saw a syntax error earlier, so we'll accumulate the
1326 # contents of this form until the end.
1330 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1332 foreach my $l (keys %$k) {
1333 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1339 # Returns text representing a set of forms.
1344 foreach my $form (@$forms) {
1345 my ($c, $o, $k, $e) = @$form;
1358 foreach my $key (@$o) {
1361 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1363 $sp = " "x(length("$key: "));
1364 $sp = " "x4 if length($sp) > 16;
1366 foreach $v (@values) {
1372 push @lines, "$line\n\n";
1375 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1378 push @lines, "$key: $v\n\n";
1381 length($line)+length($v)-rindex($line, "\n") >= 70)
1383 $line .= ",\n$sp$v";
1386 $line = $line ? "$line,$v" : "$key: $v";
1390 $line = "$key:" unless @values;
1392 if ($line =~ /\n/) {
1393 if (@lines && $lines[-1] !~ /\n\n$/) {
1398 push @lines, "$line\n";
1402 $text .= join "", @lines;
1410 return join "\n--\n\n", @text;
1416 # Returns configuration information from the environment.
1417 sub config_from_env {
1420 foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1422 if (exists $ENV{"RT$k"}) {
1423 $env{lc $k} = $ENV{"RT$k"};
1430 # Finds a suitable configuration file and returns information from it.
1431 sub config_from_file {
1435 # We'll use an absolute path if we were given one.
1436 return parse_config_file($rc);
1439 # Otherwise we'll use the first file we can find in the current
1440 # directory, or in one of its (increasingly distant) ancestors.
1442 my @dirs = split /\//, cwd;
1444 my $file = join('/', @dirs, $rc);
1446 return parse_config_file($file);
1449 # Remove the last directory component each time.
1453 # Still nothing? We'll fall back to some likely defaults.
1454 for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
1455 return parse_config_file($_) if (-r $_);
1462 # Makes a hash of the specified configuration file.
1463 sub parse_config_file {
1466 local $_; # $_ may be aliased to a constant, from line 1163
1468 open( my $handle, '<', $file ) or return;
1472 next if (/^#/ || /^\s*$/);
1474 if (/^(externalauth|auth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1478 die "rt: $file:$.: unknown configuration directive.\n";
1489 my $sub = (caller(1))[3];
1490 $sub =~ s/^main:://;
1491 warn "rt: $sub: @_\n";
1496 eval 'require Term::ReadKey';
1498 die "No password specified (and Term::ReadKey not installed).\n";
1502 Term::ReadKey::ReadMode('noecho');
1503 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1504 Term::ReadKey::ReadMode('restore');
1515 my ($c, $o, $k, $e);
1517 my $ntext = vi($text);
1518 return undef if ($error && $ntext eq $text);
1522 my $form = Form::parse($text);
1524 ($c, $o, $k, $e) = @{ $form->[0] };
1527 $c = "# Syntax error.";
1534 my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
1535 unless ( $status ) {
1541 $text = Form::compose([[$c, $o, $k, $e]]);
1549 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1553 my $handle = File::Temp->new;
1554 print $handle $text;
1557 system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1559 open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1566 # Add a value to a (possibly multi-valued) hash key.
1568 my ($hash, $key, $val) = @_;
1569 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1571 if (exists $hash->{$key}) {
1572 unless (ref $hash->{$key} eq 'ARRAY') {
1573 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1574 $hash->{$key} = \@v;
1576 push @{ $hash->{$key} }, @val;
1579 $hash->{$key} = $val;
1583 # WARNING: this code is duplicated in lib/RT/Interface/REST.pm
1584 # If you change one, change both functions at once
1585 # "Normalise" a hash key that's known to be multi-valued.
1587 my ($val, $strip) = @_;
1589 my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val);
1591 foreach my $line (@values) {
1592 while ($line =~ /\S/) {
1594 \s* # Trim leading whitespace
1597 ((?>[^\\"]*(?:\\.[^\\"]*)*))"
1599 (') # Single-quoted string
1600 ((?>[^\\']*(?:\\.[^\\']*)*))'
1602 q\{(.*?)\} # A perl-ish q{} string; this does
1603 # no paren balancing, however, and
1604 # only exists for back-compat
1606 (.*?) # Anything else, until the next comma
1608 \s* # Trim trailing whitespace
1610 \Z # Finish at end-of-line
1614 //xs or last; # There should be no way this match
1615 # fails, but add a failsafe to
1616 # prevent infinite-looping if it
1618 my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6));
1619 # Only unquote the quote character, or the backslash -- and
1620 # only if we were originally quoted..
1622 $quoted =~ s/([\\'])/\\$1/g;
1626 $quoted =~ s/\\([\\$quote])/$1/g if $quote;
1627 push @words, $quoted;
1629 push @words, "$quote$quoted$quote";
1636 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1637 # change both functions at once
1642 foreach (split /\s*,\s*/, $list) {
1643 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1646 return map $_->[0], # schwartzian transform
1648 defined $a->[1] && defined $b->[1]?
1651 :!defined $a->[1] && !defined $b->[1]?
1654 # mix, number must be first
1655 :defined $a->[1]? -1: 1
1657 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1661 sub get_type_argument {
1665 $type = shift @ARGV;
1666 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1667 # We want whine to mention our caller, not us.
1668 @_ = ("Invalid type '$type' specified.");
1673 @_ = ("No type argument specified with -t.");
1677 $type =~ s/s$//; # "Plural". Ugh.
1681 sub get_var_argument {
1685 my $kv = shift @ARGV;
1686 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1687 push @{ $data->{$k} }, $v;
1690 @_ = ("Invalid variable specification: '$kv'.");
1695 @_ = ("No variable argument specified with -S.");
1700 sub is_object_spec {
1701 my ($spec, $type) = @_;
1703 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1704 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1709 my ($action, $type, $rv) = @_;
1711 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1712 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1717 # simplified procedure for parsing date, avoid loading Date::Parse
1718 my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1719 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1721 my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1722 if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1723 ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1724 $mon = $month{$monstr} if exists $month{$monstr};
1725 } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1726 ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1728 if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1729 return timelocal($sec,$min,$hr,$day,$mon,$yr);
1731 print "Unknown date format in parsedate: $_\n";
1737 my ($old, $new) = @_;
1738 $new = time() if ! $new;
1739 $old = str2time($old) if $old !~ /^\d+$/;
1740 $new = str2time($new) if $new !~ /^\d+$/;
1741 return "???" if ! $old or ! $new;
1743 my %seconds = (min => 60,
1748 yr => 60*60*24*365);
1750 my $diff = $new - $old;
1752 my $howmuch = $diff;
1753 for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1754 last if $diff < $seconds{$_};
1756 $howmuch = int($diff/$seconds{$_});
1758 return "$howmuch $what";
1763 my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1765 # dates are in local time zone
1767 print "Date: $k->{Created}\n";
1768 print "From: $k->{Requestors}\n";
1769 print "Cc: $k->{Cc}\n" if $k->{Cc};
1770 print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1771 print "X-Queue: $k->{Queue}\n";
1772 print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1774 # dates in these attributes are in GMT and will be converted
1775 foreach my $form (@$forms) {
1776 my ($c, $o, $k, $e) = @$form;
1777 next if ! $k->{id} or exists $k->{Queue};
1778 if ( exists $k->{Created} ) {
1779 my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1781 my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1782 if ( exists $k->{Description} ) {
1783 print "===> $k->{Description} on $created\n";
1786 print "$k->{Content}\n" if exists $k->{Content} and
1787 $k->{Content} !~ /to have no content$/ and
1788 ($k->{Type}||'') ne 'EmailRecord';
1789 print "$k->{Attachments}\n" if exists $k->{Attachments} and
1796 my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1797 $heading .= '-' x 80 . "\n";
1799 foreach my $form (@$forms) {
1800 my ($c, $o, $k, $e) = @$form;
1802 print $heading if $heading;
1805 $id =~ s!^ticket/!!;
1806 my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1807 $owner = substr($owner, 0, 5);
1808 my $queue = substr($k->{Queue}, 0, 5);
1809 my $subject = substr($k->{Subject}, 0, 30);
1810 my $age = date_diff($k->{Created});
1811 my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1812 my $status = substr($k->{Status}, 0, 6);
1813 my $requestor = substr($k->{Requestors}, 0, 9);
1814 my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1815 $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1816 if ( $k->{Owner} eq 'Nobody' ) {
1818 } elsif ($k->{Owner} eq $config{user} ) {
1824 print "No matches found\n" if $heading;
1825 printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1827 printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1828 print @open if @open;
1837 This is a command-line interface to RT 3.0 or newer.
1839 It allows you to interact with an RT server over HTTP, and offers an
1840 interface to RT's functionality that is better-suited to automation
1841 and integration with other tools.
1843 In general, each invocation of this program should specify an action
1844 to perform on one or more objects, and any other arguments required
1845 to complete the desired action.
1847 For more information:
1849 - rt help usage (syntax information)
1850 - rt help objects (how to specify objects)
1851 - rt help actions (a list of possible actions)
1852 - rt help types (a list of object types)
1854 - rt help config (configuration details)
1855 - rt help examples (a few useful examples)
1856 - rt help topics (a list of help topics)
1866 rt <action> [options] [arguments]
1870 Each invocation of this program must specify an action (e.g. "edit",
1871 "create"), options to modify behaviour, and other arguments required
1872 by the specified action. (For example, most actions expect a list of
1873 numeric object IDs to act upon.)
1875 The details of the syntax and arguments for each action are given by
1876 "rt help <action>". Some actions may be referred to by more than one
1877 name ("create" is the same as "new", for example).
1879 You may also call "rt shell", which will give you an 'rt>' prompt at
1880 which you can issue commands of the form "<action> [options]
1881 [arguments]". See "rt help shell" for details.
1883 Objects are identified by a type and an ID (which can be a name or a
1884 number, depending on the type). For some actions, the object type is
1885 implied (you can only comment on tickets); for others, the user must
1886 specify it explicitly. See "rt help objects" for details.
1888 In syntax descriptions, mandatory arguments that must be replaced by
1889 appropriate value are enclosed in <>, and optional arguments are
1890 indicated by [] (for example, <action> and [options] above).
1892 For more information:
1894 - rt help objects (how to specify objects)
1895 - rt help actions (a list of actions)
1896 - rt help types (a list of object types)
1897 - rt help shell (how to use the shell)
1903 Title: configuration
1906 This program has two major sources of configuration information: its
1907 configuration files, and the environment.
1909 The program looks for configuration directives in a file named .rtrc
1910 (or $RTCONFIG; see below) in the current directory, and then in more
1911 distant ancestors, until it reaches /. If no suitable configuration
1912 files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
1915 Configuration directives:
1917 The following directives may occur, one per line:
1919 - server <URL> URL to RT server.
1920 - user <username> RT username.
1921 - passwd <passwd> RT user's password.
1922 - query <RT Query> Default RT Query for list action
1923 - orderby <order> Default RT order for list action
1924 - queue <queuename> Default RT Queue for list action
1925 - auth <rt|basic|gssapi> Method to authenticate via; "basic"
1926 means HTTP Basic authentication, "gssapi" means
1927 Kerberos credentials, if your RT is configured
1928 with $WebRemoteUserAuth. For backwards
1929 compatibility, "externalauth 1" means "auth basic"
1931 Blank and #-commented lines are ignored.
1933 Sample configuration file contents:
1935 server https://rt.somewhere.com/
1936 # more than one queue can be given (by adding a query expression)
1937 queue helpdesk or queue=support
1938 query Status != resolved and Owner=myaccount
1941 Environment variables:
1943 The following environment variables override any corresponding
1944 values defined in configuration files:
1950 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1951 - RTCONFIG Specifies a name other than ".rtrc" for the
1953 - RTQUERY Default RT Query for rt list
1954 - RTORDERBY Default order for rt list
1963 <type>/<id>[/<attributes>]
1965 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1966 ID. Some types of objects can also be identified by name (like users
1967 and queues). Furthermore, objects may have named attributes (such as
1968 "ticket/1/history").
1970 An object specification is like a path in a virtual filesystem, with
1971 object types as top-level directories, object IDs as subdirectories,
1972 and named attributes as further subdirectories.
1974 A comma-separated list of names, numeric IDs, or numeric ranges can
1975 be used to specify more than one object of the same type. Note that
1976 the list must be a single argument (i.e., no spaces). For example,
1977 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1978 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1980 If just a number is given as object specification it will be
1981 interpreted as ticket/<number>
1985 1 # the same as ticket/1
1987 ticket/1/attachments
1988 ticket/1/attachments/3
1989 ticket/1/attachments/3/content
1991 ticket/1-3,5-7/history
1995 For more information:
1997 - rt help <action> (action-specific details)
1998 - rt help <type> (type-specific details)
2006 You can currently perform the following actions on all objects:
2008 - list (list objects matching some condition)
2009 - show (display object details)
2010 - edit (edit object details)
2011 - create (create a new object)
2013 Each type may define actions specific to itself; these are listed in
2014 the help item about that type.
2016 For more information:
2018 - rt help <action> (action-specific details)
2019 - rt help types (a list of possible types)
2021 The following actions on tickets are also possible:
2023 - comment Add comments to a ticket
2024 - correspond Add comments to a ticket
2025 - merge Merge one ticket into another
2026 - link Link one ticket to another
2027 - take Take a ticket (steal and untake are possible as well)
2029 For several edit set subcommands that are frequently used abbreviations
2030 have been introduced. These abbreviations are:
2032 - delete or del delete a ticket (edit set status=deleted)
2033 - resolve or res resolve a ticket (edit set status=resolved)
2034 - subject change subject of ticket (edit set subject=string)
2035 - give give a ticket to somebody (edit set owner=user)
2042 You can currently operate on the following types of objects:
2049 For more information:
2051 - rt help <type> (type-specific details)
2052 - rt help objects (how to specify objects)
2053 - rt help actions (a list of possible actions)
2060 Tickets are identified by a numeric ID.
2062 The following generic operations may be performed upon tickets:
2069 In addition, the following ticket-specific actions exist:
2085 The following attributes can be used with "rt show" or "rt edit"
2086 to retrieve or edit other information associated with tickets:
2088 links A ticket's relationships with others.
2089 history All of a ticket's transactions.
2090 history/type/<type> Only a particular type of transaction.
2091 history/id/<id> Only the transaction of the specified id.
2092 attachments A list of attachments.
2093 attachments/<id> The metadata for an individual attachment.
2094 attachments/<id>/content The content of an individual attachment.
2102 Users and groups are identified by name or numeric ID.
2104 The following generic operations may be performed upon them:
2116 Queues are identified by name or numeric ID.
2118 Currently, they can be subjected to the following actions:
2131 rt subject <id> <new subject text>
2133 Change the subject of a ticket whose ticket id is given.
2142 rt give <id> <accountname>
2144 Give a ticket whose ticket id is given to another user.
2153 Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2164 Take a ticket whose ticket id is given, i.e. set the owner to myself.
2175 Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2187 Resolves a ticket whose ticket id is given.
2199 Deletes a ticket whose ticket id is given.
2210 Terminates the currently established login session. You will need to
2211 provide authentication credentials before you can continue using the
2212 server. (See "rt help config" for details about authentication.)
2223 rt <ls|list|search> [options] "query string"
2225 Displays a list of objects matching the specified conditions.
2226 ("ls", "list", and "search" are synonyms.)
2228 The query string must be supplied as one argument.
2230 if on tickets, query is in the SQL-like syntax used internally by
2231 RT. (For more information, see "rt help query".), otherwise, query
2232 is plain string with format "FIELD OP VALUE", e.g. "Name = General".
2234 if query string is absent, we limit to privileged ones on users and
2235 user defined ones on groups automatically.
2239 The following options control how much information is displayed
2240 about each matching object:
2242 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
2243 -s Short description.
2244 -l Longer description.
2245 -f <field[s] Display only the fields listed and the ticket id
2249 -o +/-<field> Orders the returned list by the specified field.
2250 -r reversed order (useful if a default was given)
2251 -q queue[s] restricts the query to the queue[s] given
2252 multiple queues are separated by comma
2253 -S var=val Submits the specified variable with the request.
2254 -t type Specifies the type of object to look for. (The
2255 default is "ticket".)
2259 rt ls "Priority > 5 and Status=new"
2260 rt ls -o +Subject "Priority > 5 and Status=new"
2261 rt ls -o -Created "Priority > 5 and Status=new"
2262 rt ls -i "Priority > 5"|rt edit - set status=resolved
2263 rt ls -t ticket "Subject like '[PATCH]%'"
2265 rt ls -f owner,subject
2266 rt ls -t queue 'Name = General'
2267 rt ls -t user 'EmailAddress like foo@bar.com'
2268 rt ls -t group 'Name like foo'
2277 rt show [options] <object-ids>
2279 Displays details of the specified objects.
2281 For some types, object information is further classified into named
2282 attributes (for example, "1-3/links" is a valid ticket specification
2283 that refers to the links for tickets 1-3). Consult "rt help <type>"
2284 and "rt help objects" for further details.
2286 If only a number is given it will be interpreted as the objects
2287 ticket/number and ticket/number/history
2289 This command writes a set of forms representing the requested object
2294 The following options control how much information is displayed
2295 about each matching object:
2297 Without any formatting options prettyprinted output is generated.
2298 Giving any of the two options below reverts to raw output.
2299 -s Short description (history and attachments only).
2300 -l Longer description (history and attachments only).
2303 - Read IDs from STDIN instead of the command-line.
2304 -t type Specifies object type.
2305 -f a,b,c Restrict the display to the specified fields.
2306 -S var=val Submits the specified variable with the request.
2310 rt show -t ticket -f id,subject,status 1-3
2311 rt show ticket/3/attachments/29
2312 rt show ticket/3/attachments/29/content
2313 rt show ticket/1-3/links
2314 rt show ticket/3/history
2315 rt show -l ticket/3/history
2328 rt edit [options] <object-ids> set field=value [field=value] ...
2329 add field=value [field=value] ...
2330 del field=value [field=value] ...
2332 Edits information corresponding to the specified objects.
2334 A purely numeric object id nnn is translated into ticket/nnn
2336 If, instead of "edit", an action of "new" or "create" is specified,
2337 then a new object is created. In this case, no numeric object IDs
2338 may be specified, but the syntax and behaviour remain otherwise
2341 This command typically starts an editor to allow you to edit object
2342 data in a form for submission. If you specified enough information
2343 on the command-line, however, it will make the submission directly.
2345 The command line may specify field-values in three different ways.
2346 "set" sets the named field to the given value, "add" adds a value
2347 to a multi-valued field, and "del" deletes the corresponding value.
2348 Each "field=value" specification must be given as a single argument.
2350 For some types, object information is further classified into named
2351 attributes (for example, "1-3/links" is a valid ticket specification
2352 that refers to the links for tickets 1-3). These attributes may also
2353 be edited. Consult "rt help <type>" and "rt help object" for further
2358 - Read numeric IDs from STDIN instead of the command-line.
2359 (Useful with rt ls ... | rt edit -; see examples below.)
2360 -i Read a completed form from STDIN before submitting.
2361 -o Dump the completed form to STDOUT instead of submitting.
2362 -e Allows you to edit the form even if the command-line has
2363 enough information to make a submission directly.
2365 Submits the specified variable with the request.
2366 -t type Specifies object type.
2367 -ct content-type Specifies content type of message(tickets only).
2371 # Interactive (starts $EDITOR with a form).
2374 rt create -t ticket -ct text/html
2377 rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2378 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2379 rt edit ticket/4 set priority=3 owner=bar@example.com \
2380 add cc=foo@example.com bcc=quux@example.net
2381 rt create -t ticket set subject='new ticket' priority=10 \
2382 add cc=foo@example.com
2392 rt <comment|correspond> [options] <ticket-id>
2394 Adds a comment (or correspondence) to the specified ticket (the only
2395 difference being that comments aren't sent to the requestors.)
2397 This command will typically start an editor and allow you to type a
2398 comment into a form. If, however, you specified all the necessary
2399 information on the command line, it submits the comment directly.
2401 (See "rt help forms" for more information about forms.)
2405 -m <text> Specify comment text.
2406 -ct <content-type> Specify content-type of comment text.
2407 -a <file> Attach a file to the comment. (May be used more
2408 than once to attach multiple files.)
2409 -c <addrs> A comma-separated list of Cc addresses.
2410 -b <addrs> A comma-separated list of Bcc addresses.
2411 -s <status> Set a new status for the ticket (default will
2412 leave the status unchanged)
2413 -w <time> Specify the time spent working on this ticket.
2414 -e Starts an editor before the submission, even if
2415 arguments from the command line were sufficient.
2419 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2428 rt merge <from-id> <to-id>
2430 Merges the first ticket specified into the second ticket specified.
2439 rt link [-d] <id-A> <link> <id-B>
2441 Creates (or, with -d, deletes) a link between the specified tickets.
2442 The link can (irrespective of case) be any of:
2444 DependsOn/DependedOnBy: A depends upon B (or vice versa).
2445 RefersTo/ReferredToBy: A refers to B (or vice versa).
2446 MemberOf/HasMember: A is a member of B (or vice versa).
2448 To view a ticket's links, use "rt show ticket/3/links". (See
2449 "rt help ticket" and "rt help show".)
2453 -d Deletes the specified link.
2457 rt link 2 dependson 3
2458 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
2465 RT uses an SQL-like syntax to specify object selection constraints.
2466 See the <RT:...> documentation for details.
2468 (XXX: I'm going to have to write it, aren't I?)
2470 Until it exists here a short description of important constructs:
2472 The two simple forms of query expressions are the constructs
2473 Attribute like Value and
2474 Attribute = Value or Attribute != Value
2476 Whether attributes can be matched using like or using = is built into RT.
2477 The attributes id, Queue, Owner Priority and Status require the = or !=
2480 If Value is a string it must be quoted and may contain the wildcard
2481 character %. If the string does not contain white space, the quoting
2482 may however be omitted, it will be added automatically when parsing
2485 Simple query expressions can be combined using and, or and parentheses
2486 can be used to group expressions.
2488 As a special case a standalone string (which would not form a correct
2489 query) is transformed into (Owner='string' or Requestor like 'string%')
2490 and added to the default query, i.e. the query is narrowed down.
2492 If no Queue=name clause is contained in the query, a default clause
2493 Queue=$config{queue} is added.
2496 Status!='resolved' and Status!='rejected'
2497 (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2505 This program uses RFC822 header-style forms to represent object data
2506 in a form that's suitable for processing both by humans and scripts.
2508 A form is a set of (field, value) specifications, with some initial
2509 commented text and interspersed blank lines allowed for convenience.
2510 Field names may appear more than once in a form; a comma-separated
2511 list of multiple field values may also be specified directly.
2513 Field values can be wrapped as in RFC822, with leading whitespace.
2514 The longest sequence of leading whitespace common to all the lines
2515 is removed (preserving further indentation). There is no limit on
2516 the length of a value.
2518 Multiple forms are separated by a line containing only "--\n".
2520 (XXX: A more detailed specification will be provided soon. For now,
2521 the server-side syntax checking will suffice.)
2532 Get help on any of the following subjects:
2534 - tickets, users, groups, queues.
2535 - show, edit, ls/list/search, new/create.
2537 - query (search query syntax)
2538 - forms (form specification)
2540 - objects (how to specify objects)
2541 - types (a list of object types)
2542 - actions/commands (a list of actions)
2543 - usage/syntax (syntax details)
2544 - conf/config/configuration (configuration details)
2545 - examples (a few useful examples)
2553 some useful examples
2555 All the following list requests will be restricted to the default queue.
2556 That can be changed by adding the option -q queuename
2558 List all tickets that are not rejected/resolved
2560 List all tickets that are new and do not have an owner
2561 rt ls "status=new and owner=nobody"
2562 List all tickets which I have sent or of which I am the owner
2564 List all attributes for the ticket 6977 (ls -l instead of ls)
2566 Show the content of ticket 6977
2568 Show all attributes in the ticket and in the history of the ticket
2570 Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2572 This will open an editor and lets you add text (attribute Text:)
2573 Other attributes may be changed as well, but usually don't do that.
2574 Correspond a ticket (like comment, but mail is also sent to requestors)
2576 Edit a ticket (generic change, interactive using the editor)
2578 Change the owner of a ticket non interactively
2579 rt edit 6977 set owner=myaccount
2581 rt give 6977 account
2584 Change the status of a ticket
2585 rt edit 6977 set status=resolved
2588 Change the status of all tickets I own to resolved !!!
2589 rt ls -i owner=myaccount | rt edit - set status=resolved
2600 Opens an interactive shell, at which you can issue commands of
2601 the form "<action> [options] [arguments]".
2603 To exit the shell, type "quit" or "exit".
2605 Commands can be given at the shell in the same form as they would
2606 be given at the command line without the leading 'rt' invocation.
2610 rt> create -t ticket set subject='new' add cc=foo@example.com
2624 rt <take|untake|steal> <ticket-id>
2626 Sets the owner of the specified ticket to the current user,
2627 assuming said user has the bits to do so, or releases the
2630 'Take' is used on tickets which are not currently owned
2631 (Owner: Nobody), 'steal' is used on tickets which *are*
2632 currently owned, and 'untake' is used to "release" a ticket
2633 (reset its Owner to Nobody). 'Take' cannot be used on
2634 tickets which are currently owned.
2637 alice$ rt create -t ticket set subject="New ticket"
2640 # Owner changed from Nobody to alice
2643 # Owner changed from alice to bob
2645 # Owner changed from bob to Nobody
2653 Use "quit" or "exit" to leave the shell. Only valid within shell
2665 rt - command-line interface to RT 3.0 or newer
2673 This script allows you to interact with an RT server over HTTP, and offers an
2674 interface to RT's functionality that is better-suited to automation and
2675 integration with other tools.
2677 In general, each invocation of this program should specify an action to
2678 perform on one or more objects, and any other arguments required to complete