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, $content_type);
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 }
489 elsif (/^-ct$/) { $content_type = shift @ARGV }
491 $bad = 1, last unless defined($type = get_type_argument());
494 $bad = 1, last unless get_var_argument(\%data);
496 elsif (/^-$/ && !($slurped || $input)) {
497 chomp(my @lines = <STDIN>);
499 unless (is_object_spec($_, $type)) {
500 whine "Invalid object on STDIN: '$_'.";
510 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
511 my ($key, $op, $val) = ($1, $2, $3);
512 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
514 vpush($hash, lc $key, $val);
519 whine "No variables to set.";
524 elsif (/^(?:add|del)$/i) {
526 my $hash = ($_ eq "add") ? \%add : \%del;
528 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
529 my ($key, $val) = ($1, $2);
531 vpush($hash, lc $key, $val);
536 whine "No variables to set.";
541 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
542 push @objects, $spc2;
544 elsif (my $spec = is_object_spec($_, $type)) {
545 push @objects, $spec;
548 my $datum = /^-/ ? "option" : "argument";
549 whine "Unrecognised $datum '$_'.";
554 if ($action =~ /^ed(?:it)?$/) {
556 whine "No objects specified.";
562 whine "You shouldn't specify objects as arguments to $action.";
566 whine "What type of object do you want to create?";
569 @objects = ("$type/new") if defined($type);
571 #return help($action, $type) if $bad;
572 return suggest_help($action, $type, $bad) if $bad;
574 # We need a form to make changes to. We usually ask the server for
575 # one, but we can avoid that if we are fed one on STDIN, or if the
576 # user doesn't want to edit the form by hand, and the command line
577 # specifies only simple variable assignments. We *should* get a
578 # form if we're creating a new ticket, so that the default values
579 # get filled in properly.
581 my @new_objects = grep /\/new$/, @objects;
587 elsif ($edit || %add || %del || !$cl || @new_objects) {
588 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
592 # If any changes were specified on the command line, apply them.
595 # We're updating forms from the server.
596 my $forms = Form::parse($text);
598 foreach my $form (@$forms) {
599 my ($c, $o, $k, $e) = @$form;
602 next if ($e || !@$o);
608 # Make changes to existing fields.
610 if (exists $add{lc $key}) {
611 $val = delete $add{lc $key};
612 vpush($k, $key, $val);
613 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
615 if (exists $del{lc $key}) {
616 $val = delete $del{lc $key};
617 my %val = map {$_=>1} @{ vsplit($val) };
618 $k->{$key} = vsplit($k->{$key});
619 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
621 if (exists $set{lc $key}) {
622 $k->{$key} = delete $set{lc $key};
626 # Then update the others.
627 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
628 foreach $key (keys %add) {
629 vpush($k, $key, $add{$key});
630 $k->{$key} = vsplit($k->{$key});
632 push @$o, (keys %add, keys %set);
635 $text = Form::compose($forms);
638 # We're rolling our own set of forms.
641 my ($type, $ids, $args) =
642 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
645 foreach my $obj (expand_list($ids)) {
646 my %set = (%set, id => "$type/$obj$args");
647 push @forms, ["", [keys %set], \%set];
650 $text = Form::compose(\@forms);
660 @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
665 # We'll let the user edit the form before sending it to the server,
666 # unless we have enough information to submit it non-interactively.
667 if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
668 $text .= "Content-Type: $content_type\n"
669 if $content_type and $content_type ne "text/plain";
672 if ($edit || (!$input && !$cl)) {
673 my ($newtext) = vi_form_while(
676 my ($text, $form) = @_;
677 return 1 unless exists $form->[2]{'Attachment'};
679 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
680 return (0, "File '$f' doesn't exist") unless -f $f;
682 @files = @{ vsplit($form->[2]{'Attachment'}) };
686 return $newtext unless $newtext;
687 # We won't resubmit a bad form unless it was changed.
688 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
691 delete @data{ grep /^attachment_\d+$/, keys %data };
693 foreach my $file (@files) {
694 $data{"attachment_$i"} = bless([ $file ], "Attachment");
699 my $r = submit("$REST/edit", {content => $text, %data});
700 if ($r->code == 409) {
701 # If we submitted a bad form, we'll give the user a chance
702 # to correct it and resubmit.
703 if ($edit || (!$input && !$cl)) {
718 # handler for special edit commands. A valid edit command is constructed and
719 # further work is delegated to the edit handler
723 my ($id, $bad, $what);
726 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
730 whine "No ticket number specified.";
733 if ($action eq 'subject') {
734 my $subject = '"'.join (" ", @ARGV).'"';
736 $what = "subject=$subject";
737 } elsif ($action eq 'give') {
738 my $owner = shift @ARGV;
739 $what = "owner=$owner";
742 if ( $action eq 'delete' or $action eq 'del' ) {
743 $what = "status=deleted";
744 } elsif ($action eq 'resolve' or $action eq 'res' ) {
745 $what = "status=resolved";
746 } elsif ($action eq 'take' ) {
747 $what = "owner=$config{user}";
748 } elsif ($action eq 'untake') {
749 $what = "owner=Nobody";
754 whine "Extraneous arguments for action $action: @ARGV.";
758 whine "unrecognized action $action.";
760 return help("edit", undef, $bad) if $bad;
761 @ARGV = ( $id, "set", $what );
762 print "Executing: rt edit @ARGV\n";
766 # We roll "comment" and "correspond" into the same handler.
770 my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
779 elsif (/^-(?:[abcmw]|ct)$/) {
781 whine "No argument specified with $_.";
786 unless (-f $ARGV[0] && -r $ARGV[0]) {
787 whine "Cannot read attachment: '$ARGV[0]'.";
790 push @files, shift @ARGV;
793 $content_type = shift @ARGV;
796 my $a = $_ eq "-b" ? \@bcc : \@cc;
797 @$a = split /\s*,\s*/, shift @ARGV;
801 if ( $msg =~ /^-$/ ) {
803 while (<STDIN>) { $msg .= $_ }
806 elsif (/-w/) { $wtime = shift @ARGV }
808 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
812 my $datum = /^-/ ? "option" : "argument";
813 whine "Unrecognised $datum '$_'.";
819 whine "No object specified.";
822 #return help($action, "ticket") if $bad;
823 return suggest_help($action, "ticket") if $bad;
827 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
833 Attachment => [ @files ],
834 TimeWorked => $wtime || '',
835 'Content-Type' => $content_type || 'text/plain',
841 my $text = Form::compose([ $form ]);
843 if ($edit || !$msg) {
844 my ($tmp) = vi_form_while(
847 my ($text, $form) = @_;
848 foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
849 return (0, "File '$f' doesn't exist") unless -f $f;
851 @files = @{ vsplit($form->[2]{'Attachment'}) };
855 return $tmp unless $tmp;
860 foreach my $file (@files) {
861 $data{"attachment_$i"} = bless([ $file ], "Attachment");
864 $data{content} = $text;
866 my $r = submit("$REST/ticket/$id/comment", \%data);
871 # Merge one ticket into another.
879 s/^#// if /^#\d+/; # get rid of leading hash
885 whine "Unrecognised argument: '$_'.";
891 my $evil = @id > 2 ? "many" : "few";
892 whine "Too $evil arguments specified.";
895 #return help("merge", "ticket") if $bad;
896 return suggest_help("merge", "ticket", $bad) if $bad;
898 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
903 # Link one ticket to another.
906 my ($bad, $del, %data) = (0, 0, ());
909 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
910 ReferredToBy HasMember MemberOf);
912 while (@ARGV && $ARGV[0] =~ /^-/) {
919 $bad = 1, last unless defined($type = get_type_argument());
922 whine "Unrecognised option: '$_'.";
927 $type = "ticket" unless $type; # default type to tickets
930 my ($from, $rel, $to) = @ARGV;
931 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
932 whine "Invalid link '$rel' for type $type specified.";
935 %data = (id => $from, rel => $rel, to => $to, del => $del);
938 my $bad = @ARGV < 3 ? "few" : "many";
939 whine "Too $bad arguments specified.";
942 return suggest_help("link", $type, $bad) if $bad;
944 my $r = submit("$REST/$type/link", \%data);
949 # Take/steal a ticket
952 my ($bad, %data) = (0, ());
959 unless ($id =~ /^\d+$/) {
960 whine "Invalid ticket ID $id specified.";
965 [ "Ticket", "Action" ],
973 my $text = Form::compose([ $form ]);
974 $data{content} = $text;
977 $bad = @ARGV < 1 ? "few" : "many";
978 whine "Too $bad arguments specified.";
981 return suggest_help("take", "ticket", $bad) if $bad;
983 my $r = submit("$REST/ticket/$id/take", \%data);
988 # Grant/revoke a user's rights.
993 whine "$cmd is unimplemented.";
997 # Client <-> Server communication.
998 # --------------------------------
1000 # This function composes and sends an HTTP request to the RT server, and
1001 # interprets the response. It takes a request URI, and optional request
1002 # data (a string, or a reference to a set of key-value pairs).
1005 my ($uri, $content) = @_;
1007 my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
1008 my $h = HTTP::Headers->new;
1010 # Did the caller specify any data to send with the request?
1012 if (defined $content) {
1013 unless (ref $content) {
1014 # If it's just a string, make sure LWP handles it properly.
1015 # (By pretending that it's a file!)
1016 $content = [ content => [undef, "", Content => $content] ];
1018 elsif (ref $content eq 'HASH') {
1020 foreach my $k (keys %$content) {
1021 if (ref $content->{$k} eq 'ARRAY') {
1022 foreach my $v (@{ $content->{$k} }) {
1026 else { push @data, $k, $content->{$k} }
1033 # Should we send authentication information to start a new session?
1034 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1035 (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1036 if ($config{externalauth}) {
1037 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1038 print " Password will be sent to $server $how\n",
1039 " Press CTRL-C now if you do not want to continue\n"
1040 if ! $config{passwd};
1041 } elsif ( $no_strong_auth ) {
1042 if (!defined $session->cookie) {
1043 print " Strong encryption not available, $no_strong_auth\n",
1044 " Password will be sent to $server $how\n",
1045 " Press CTRL-C now if you do not want to continue\n"
1046 if ! $config{passwd};
1047 push @$data, ( user => $config{user} );
1048 push @$data, ( pass => $config{passwd} || read_passwd() );
1052 # Now, we construct the request.
1054 $req = POST($uri, $data, Content_Type => 'form-data');
1059 $session->add_cookie_header($req);
1060 if ($config{externalauth}) {
1064 # Then we send the request and parse the response.
1065 DEBUG(3, $req->as_string);
1066 my $res = $ua->request($req);
1067 DEBUG(3, $res->as_string);
1069 if ($res->is_success) {
1070 # The content of the response we get from the RT server consists
1071 # of an HTTP-like status line followed by optional header lines,
1072 # a blank line, and arbitrary text.
1074 my ($head, $text) = split /\n\n/, $res->content, 2;
1075 my ($status, @headers) = split /\n/, $head;
1076 $text =~ s/\n*$/\n/ if ($text);
1078 # "RT/3.0.1 401 Credentials required"
1079 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1080 warn "rt: Malformed RT response from $config{server}.\n";
1081 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1085 # Our caller can pretend that the server returned a custom HTTP
1086 # response code and message. (Doing that directly is apparently
1087 # not sufficiently portable and uncomplicated.)
1090 $res->content($text);
1091 $session->update($res) if ($res->is_success || $res->code != 401);
1093 if (!$res->is_success) {
1094 # We can deal with authentication failures ourselves. Either
1095 # we sent invalid credentials, or our session has expired.
1096 if ($res->code == 401) {
1098 if (exists $d{user}) {
1099 warn "rt: Incorrect username or password.\n";
1102 elsif ($req->header("Cookie")) {
1103 # We'll retry the request with credentials, unless
1104 # we only wanted to logout in the first place.
1106 return submit(@_) unless $uri eq "$REST/logout";
1109 # Conflicts should be dealt with by the handler and user.
1110 # For anything else, we just die.
1111 elsif ($res->code != 409) {
1112 warn "rt: ", $res->content;
1118 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1125 # Session management.
1126 # -------------------
1128 # Maintains a list of active sessions in the ~/.rt_sessions file.
1133 # Initialises the session cache.
1135 my ($class, $file) = @_;
1137 file => $file || "$HOME/.rt_sessions",
1141 # The current session is identified by the currently configured
1143 ($s, $u) = @config{"server", "user"};
1145 bless $self, $class;
1151 # Returns the current session cookie.
1154 my $cookie = $self->{sids}{$s}{$u};
1155 return defined $cookie ? "RT_SID_$cookie" : undef;
1158 # Deletes the current session cookie.
1161 delete $self->{sids}{$s}{$u};
1164 # Adds a Cookie header to an outgoing HTTP request.
1165 sub add_cookie_header {
1166 my ($self, $request) = @_;
1167 my $cookie = $self->cookie();
1169 $request->header(Cookie => $cookie) if defined $cookie;
1172 # Extracts the Set-Cookie header from an HTTP response, and updates
1173 # session information accordingly.
1175 my ($self, $response) = @_;
1176 my $cookie = $response->header("Set-Cookie");
1178 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1179 $self->{sids}{$s}{$u} = $1;
1183 # Loads the session cache from the specified file.
1185 my ($self, $file) = @_;
1186 $file ||= $self->{file};
1188 open( my $handle, '<', $file ) or return 0;
1190 $self->{file} = $file;
1191 my $sids = $self->{sids} = {};
1194 next if /^$/ || /^#/;
1195 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1196 my ($server, $user, $cookie) = split / /, $_;
1197 $sids->{$server}{$user} = $cookie;
1202 # Writes the current session cache to the specified file.
1204 my ($self, $file) = shift;
1205 $file ||= $self->{file};
1207 open( my $handle, '>', "$file" ) or return 0;
1209 my $sids = $self->{sids};
1210 foreach my $server (keys %$sids) {
1211 foreach my $user (keys %{ $sids->{$server} }) {
1212 my $sid = $sids->{$server}{$user};
1214 print $handle "$server $user $sid\n";
1232 # Forms are RFC822-style sets of (field, value) specifications with some
1233 # initial comments and interspersed blank lines allowed for convenience.
1234 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1236 # Each form is parsed into an array with four elements: commented text
1237 # at the start of the form, an array with the order of keys, a hash with
1238 # key/value pairs, and optional error text if the form syntax was wrong.
1240 # Returns a reference to an array of parsed forms.
1244 my @lines = split /\n/, $_[0] if $_[0];
1245 my ($c, $o, $k, $e) = ("", [], {}, "");
1249 my $line = shift @lines;
1251 next LINE if $line eq '';
1253 if ($line eq '--') {
1254 # We reached the end of one form. We'll ignore it if it was
1255 # empty, and store it otherwise, errors and all.
1256 if ($e || $c || @$o) {
1257 push @forms, [ $c, $o, $k, $e ];
1258 $c = ""; $o = []; $k = {}; $e = "";
1262 elsif ($state != -1) {
1263 if ($state == 0 && $line =~ /^#/) {
1264 # Read an optional block of comments (only) at the start
1268 while (@lines && $lines[0] =~ /^#/) {
1269 $c .= "\n".shift @lines;
1273 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1274 # Read a field: value specification.
1278 # Read continuation lines, if any.
1279 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1280 push @v, shift @lines;
1282 pop @v while (@v && $v[-1] eq '');
1284 # Strip longest common leading indent from text.
1286 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1287 $ws = $ls if (!$ws || length($ls) < length($ws));
1289 s/^$ws// foreach @v;
1291 push(@$o, $f) unless exists $k->{$f};
1292 vpush($k, $f, join("\n", @v));
1296 elsif ($line !~ /^#/) {
1297 # We've found a syntax error, so we'll reconstruct the
1298 # form parsed thus far, and add an error marker. (>>)
1300 $e = Form::compose([[ "", $o, $k, "" ]]);
1301 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1305 # We saw a syntax error earlier, so we'll accumulate the
1306 # contents of this form until the end.
1310 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1312 foreach my $l (keys %$k) {
1313 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1319 # Returns text representing a set of forms.
1324 foreach my $form (@$forms) {
1325 my ($c, $o, $k, $e) = @$form;
1338 foreach my $key (@$o) {
1341 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1343 $sp = " "x(length("$key: "));
1344 $sp = " "x4 if length($sp) > 16;
1346 foreach $v (@values) {
1352 push @lines, "$line\n\n";
1355 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1358 push @lines, "$key: $v\n\n";
1361 length($line)+length($v)-rindex($line, "\n") >= 70)
1363 $line .= ",\n$sp$v";
1366 $line = $line ? "$line,$v" : "$key: $v";
1370 $line = "$key:" unless @values;
1372 if ($line =~ /\n/) {
1373 if (@lines && $lines[-1] !~ /\n\n$/) {
1378 push @lines, "$line\n";
1382 $text .= join "", @lines;
1390 return join "\n--\n\n", @text;
1396 # Returns configuration information from the environment.
1397 sub config_from_env {
1400 foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1402 if (exists $ENV{"RT$k"}) {
1403 $env{lc $k} = $ENV{"RT$k"};
1410 # Finds a suitable configuration file and returns information from it.
1411 sub config_from_file {
1415 # We'll use an absolute path if we were given one.
1416 return parse_config_file($rc);
1419 # Otherwise we'll use the first file we can find in the current
1420 # directory, or in one of its (increasingly distant) ancestors.
1422 my @dirs = split /\//, cwd;
1424 my $file = join('/', @dirs, $rc);
1426 return parse_config_file($file);
1429 # Remove the last directory component each time.
1433 # Still nothing? We'll fall back to some likely defaults.
1434 for ("$HOME/$rc", "local/etc/rt.conf", "/etc/rt.conf") {
1435 return parse_config_file($_) if (-r $_);
1442 # Makes a hash of the specified configuration file.
1443 sub parse_config_file {
1446 local $_; # $_ may be aliased to a constant, from line 1163
1448 open( my $handle, '<', $file ) or return;
1452 next if (/^#/ || /^\s*$/);
1454 if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1458 die "rt: $file:$.: unknown configuration directive.\n";
1469 my $sub = (caller(1))[3];
1470 $sub =~ s/^main:://;
1471 warn "rt: $sub: @_\n";
1476 eval 'require Term::ReadKey';
1478 die "No password specified (and Term::ReadKey not installed).\n";
1482 Term::ReadKey::ReadMode('noecho');
1483 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1484 Term::ReadKey::ReadMode('restore');
1495 my ($c, $o, $k, $e);
1497 my $ntext = vi($text);
1498 return undef if ($error && $ntext eq $text);
1502 my $form = Form::parse($text);
1504 ($c, $o, $k, $e) = @{ $form->[0] };
1507 $c = "# Syntax error.";
1514 my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
1515 unless ( $status ) {
1521 $text = Form::compose([[$c, $o, $k, $e]]);
1529 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1533 my $handle = File::Temp->new;
1534 print $handle $text;
1537 system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1539 open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1546 # Add a value to a (possibly multi-valued) hash key.
1548 my ($hash, $key, $val) = @_;
1549 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1551 if (exists $hash->{$key}) {
1552 unless (ref $hash->{$key} eq 'ARRAY') {
1553 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1554 $hash->{$key} = \@v;
1556 push @{ $hash->{$key} }, @val;
1559 $hash->{$key} = $val;
1563 # "Normalise" a hash key that's known to be multi-valued.
1567 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1569 foreach my $line (map {split /\n/} @values) {
1570 # XXX: This should become a real parser, Ã la Text::ParseWords.
1573 my ( $a, $b ) = split /\s*,\s*/, $line, 2;
1576 no warnings 'uninitialized';
1579 while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/
1580 && $a =~ /(\\)+'$/ )) {
1581 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1586 elsif ( $a =~ /^q\{/ ) {
1588 while ( $a !~ /\}$/ ) {
1590 split /\s*,\s*/, $b, 2;
1600 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1609 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1610 # change both functions at once
1615 foreach (split /\s*,\s*/, $list) {
1616 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1619 return map $_->[0], # schwartzian transform
1621 defined $a->[1] && defined $b->[1]?
1624 :!defined $a->[1] && !defined $b->[1]?
1627 # mix, number must be first
1628 :defined $a->[1]? -1: 1
1630 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1634 sub get_type_argument {
1638 $type = shift @ARGV;
1639 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1640 # We want whine to mention our caller, not us.
1641 @_ = ("Invalid type '$type' specified.");
1646 @_ = ("No type argument specified with -t.");
1650 $type =~ s/s$//; # "Plural". Ugh.
1654 sub get_var_argument {
1658 my $kv = shift @ARGV;
1659 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1660 push @{ $data->{$k} }, $v;
1663 @_ = ("Invalid variable specification: '$kv'.");
1668 @_ = ("No variable argument specified with -S.");
1673 sub is_object_spec {
1674 my ($spec, $type) = @_;
1676 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1677 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1682 my ($action, $type, $rv) = @_;
1684 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1685 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1690 # simplified procedure for parsing date, avoid loading Date::Parse
1691 my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1692 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1694 my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1695 if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1696 ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1697 $mon = $month{$monstr} if exists $month{$monstr};
1698 } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1699 ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1701 if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1702 return timelocal($sec,$min,$hr,$day,$mon,$yr);
1704 print "Unknown date format in parsedate: $_\n";
1710 my ($old, $new) = @_;
1711 $new = time() if ! $new;
1712 $old = str2time($old) if $old !~ /^\d+$/;
1713 $new = str2time($new) if $new !~ /^\d+$/;
1714 return "???" if ! $old or ! $new;
1716 my %seconds = (min => 60,
1721 yr => 60*60*24*365);
1723 my $diff = $new - $old;
1725 my $howmuch = $diff;
1726 for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1727 last if $diff < $seconds{$_};
1729 $howmuch = int($diff/$seconds{$_});
1731 return "$howmuch $what";
1736 my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1738 # dates are in local time zone
1740 print "Date: $k->{Created}\n";
1741 print "From: $k->{Requestors}\n";
1742 print "Cc: $k->{Cc}\n" if $k->{Cc};
1743 print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1744 print "X-Queue: $k->{Queue}\n";
1745 print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1747 # dates in these attributes are in GMT and will be converted
1748 foreach my $form (@$forms) {
1749 my ($c, $o, $k, $e) = @$form;
1750 next if ! $k->{id} or exists $k->{Queue};
1751 if ( exists $k->{Created} ) {
1752 my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1754 my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1755 if ( exists $k->{Description} ) {
1756 print "===> $k->{Description} on $created\n";
1759 print "$k->{Content}\n" if exists $k->{Content} and
1760 $k->{Content} !~ /to have no content$/ and
1761 ($k->{Type}||'') ne 'EmailRecord';
1762 print "$k->{Attachments}\n" if exists $k->{Attachments} and
1769 my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1770 $heading .= '-' x 80 . "\n";
1772 foreach my $form (@$forms) {
1773 my ($c, $o, $k, $e) = @$form;
1775 print $heading if $heading;
1778 $id =~ s!^ticket/!!;
1779 my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1780 $owner = substr($owner, 0, 5);
1781 my $queue = substr($k->{Queue}, 0, 5);
1782 my $subject = substr($k->{Subject}, 0, 30);
1783 my $age = date_diff($k->{Created});
1784 my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1785 my $status = substr($k->{Status}, 0, 6);
1786 my $requestor = substr($k->{Requestors}, 0, 9);
1787 my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1788 $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1789 if ( $k->{Owner} eq 'Nobody' ) {
1791 } elsif ($k->{Owner} eq $config{user} ) {
1797 print "No matches found\n" if $heading;
1798 printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1800 printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1801 print @open if @open;
1810 This is a command-line interface to RT 3.0 or newer.
1812 It allows you to interact with an RT server over HTTP, and offers an
1813 interface to RT's functionality that is better-suited to automation
1814 and integration with other tools.
1816 In general, each invocation of this program should specify an action
1817 to perform on one or more objects, and any other arguments required
1818 to complete the desired action.
1820 For more information:
1822 - rt help usage (syntax information)
1823 - rt help objects (how to specify objects)
1824 - rt help actions (a list of possible actions)
1825 - rt help types (a list of object types)
1827 - rt help config (configuration details)
1828 - rt help examples (a few useful examples)
1829 - rt help topics (a list of help topics)
1839 rt <action> [options] [arguments]
1843 Each invocation of this program must specify an action (e.g. "edit",
1844 "create"), options to modify behaviour, and other arguments required
1845 by the specified action. (For example, most actions expect a list of
1846 numeric object IDs to act upon.)
1848 The details of the syntax and arguments for each action are given by
1849 "rt help <action>". Some actions may be referred to by more than one
1850 name ("create" is the same as "new", for example).
1852 You may also call "rt shell", which will give you an 'rt>' prompt at
1853 which you can issue commands of the form "<action> [options]
1854 [arguments]". See "rt help shell" for details.
1856 Objects are identified by a type and an ID (which can be a name or a
1857 number, depending on the type). For some actions, the object type is
1858 implied (you can only comment on tickets); for others, the user must
1859 specify it explicitly. See "rt help objects" for details.
1861 In syntax descriptions, mandatory arguments that must be replaced by
1862 appropriate value are enclosed in <>, and optional arguments are
1863 indicated by [] (for example, <action> and [options] above).
1865 For more information:
1867 - rt help objects (how to specify objects)
1868 - rt help actions (a list of actions)
1869 - rt help types (a list of object types)
1870 - rt help shell (how to use the shell)
1876 Title: configuration
1879 This program has two major sources of configuration information: its
1880 configuration files, and the environment.
1882 The program looks for configuration directives in a file named .rtrc
1883 (or $RTCONFIG; see below) in the current directory, and then in more
1884 distant ancestors, until it reaches /. If no suitable configuration
1885 files are found, it will also check for ~/.rtrc, local/etc/rt.conf
1888 Configuration directives:
1890 The following directives may occur, one per line:
1892 - server <URL> URL to RT server.
1893 - user <username> RT username.
1894 - passwd <passwd> RT user's password.
1895 - query <RT Query> Default RT Query for list action
1896 - orderby <order> Default RT order for list action
1897 - queue <queuename> Default RT Queue for list action
1898 - externalauth <0|1> Use HTTP Basic authentication
1899 explicitely setting externalauth to 0 inhibits also GSSAPI based
1900 authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1902 Blank and #-commented lines are ignored.
1904 Sample configuration file contents:
1906 server https://rt.somewhere.com/
1907 # more than one queue can be given (by adding a query expression)
1908 queue helpdesk or queue=support
1909 query Status != resolved and Owner=myaccount
1912 Environment variables:
1914 The following environment variables override any corresponding
1915 values defined in configuration files:
1921 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1922 - RTCONFIG Specifies a name other than ".rtrc" for the
1924 - RTQUERY Default RT Query for rt list
1925 - RTORDERBY Default order for rt list
1934 <type>/<id>[/<attributes>]
1936 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1937 ID. Some types of objects can also be identified by name (like users
1938 and queues). Furthermore, objects may have named attributes (such as
1939 "ticket/1/history").
1941 An object specification is like a path in a virtual filesystem, with
1942 object types as top-level directories, object IDs as subdirectories,
1943 and named attributes as further subdirectories.
1945 A comma-separated list of names, numeric IDs, or numeric ranges can
1946 be used to specify more than one object of the same type. Note that
1947 the list must be a single argument (i.e., no spaces). For example,
1948 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1949 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1951 If just a number is given as object specification it will be
1952 interpreted as ticket/<number>
1956 1 # the same as ticket/1
1958 ticket/1/attachments
1959 ticket/1/attachments/3
1960 ticket/1/attachments/3/content
1962 ticket/1-3,5-7/history
1966 For more information:
1968 - rt help <action> (action-specific details)
1969 - rt help <type> (type-specific details)
1977 You can currently perform the following actions on all objects:
1979 - list (list objects matching some condition)
1980 - show (display object details)
1981 - edit (edit object details)
1982 - create (create a new object)
1984 Each type may define actions specific to itself; these are listed in
1985 the help item about that type.
1987 For more information:
1989 - rt help <action> (action-specific details)
1990 - rt help types (a list of possible types)
1992 The following actions on tickets are also possible:
1994 - comment Add comments to a ticket
1995 - correspond Add comments to a ticket
1996 - merge Merge one ticket into another
1997 - link Link one ticket to another
1998 - take Take a ticket (steal and untake are possible as well)
2000 For several edit set subcommands that are frequently used abbreviations
2001 have been introduced. These abbreviations are:
2003 - delete or del delete a ticket (edit set status=deleted)
2004 - resolve or res resolve a ticket (edit set status=resolved)
2005 - subject change subject of ticket (edit set subject=string)
2006 - give give a ticket to somebody (edit set owner=user)
2013 You can currently operate on the following types of objects:
2020 For more information:
2022 - rt help <type> (type-specific details)
2023 - rt help objects (how to specify objects)
2024 - rt help actions (a list of possible actions)
2031 Tickets are identified by a numeric ID.
2033 The following generic operations may be performed upon tickets:
2040 In addition, the following ticket-specific actions exist:
2056 The following attributes can be used with "rt show" or "rt edit"
2057 to retrieve or edit other information associated with tickets:
2059 links A ticket's relationships with others.
2060 history All of a ticket's transactions.
2061 history/type/<type> Only a particular type of transaction.
2062 history/id/<id> Only the transaction of the specified id.
2063 attachments A list of attachments.
2064 attachments/<id> The metadata for an individual attachment.
2065 attachments/<id>/content The content of an individual attachment.
2073 Users and groups are identified by name or numeric ID.
2075 The following generic operations may be performed upon them:
2087 Queues are identified by name or numeric ID.
2089 Currently, they can be subjected to the following actions:
2102 rt subject <id> <new subject text>
2104 Change the subject of a ticket whose ticket id is given.
2113 rt give <id> <accountname>
2115 Give a ticket whose ticket id is given to another user.
2124 Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2135 Take a ticket whose ticket id is given, i.e. set the owner to myself.
2146 Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2158 Resolves a ticket whose ticket id is given.
2170 Deletes a ticket whose ticket id is given.
2181 Terminates the currently established login session. You will need to
2182 provide authentication credentials before you can continue using the
2183 server. (See "rt help config" for details about authentication.)
2194 rt <ls|list|search> [options] "query string"
2196 Displays a list of objects matching the specified conditions.
2197 ("ls", "list", and "search" are synonyms.)
2199 Conditions are expressed in the SQL-like syntax used internally by
2200 RT. (For more information, see "rt help query".) The query string
2201 must be supplied as one argument.
2203 (Right now, the server doesn't support listing anything but tickets.
2204 Other types will be supported in future; this client will be able to
2205 take advantage of that support without any changes.)
2209 The following options control how much information is displayed
2210 about each matching object:
2212 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
2213 -s Short description.
2214 -l Longer description.
2215 -f <field[s] Display only the fields listed and the ticket id
2219 -o +/-<field> Orders the returned list by the specified field.
2220 -r reversed order (useful if a default was given)
2221 -q queue[s] restricts the query to the queue[s] given
2222 multiple queues are separated by comma
2223 -S var=val Submits the specified variable with the request.
2224 -t type Specifies the type of object to look for. (The
2225 default is "ticket".)
2229 rt ls "Priority > 5 and Status=new"
2230 rt ls -o +Subject "Priority > 5 and Status=new"
2231 rt ls -o -Created "Priority > 5 and Status=new"
2232 rt ls -i "Priority > 5"|rt edit - set status=resolved
2233 rt ls -t ticket "Subject like '[PATCH]%'"
2235 rt ls -f owner,subject
2244 rt show [options] <object-ids>
2246 Displays details of the specified objects.
2248 For some types, object information is further classified into named
2249 attributes (for example, "1-3/links" is a valid ticket specification
2250 that refers to the links for tickets 1-3). Consult "rt help <type>"
2251 and "rt help objects" for further details.
2253 If only a number is given it will be interpreted as the objects
2254 ticket/number and ticket/number/history
2256 This command writes a set of forms representing the requested object
2261 The following options control how much information is displayed
2262 about each matching object:
2264 Without any formatting options prettyprinted output is generated.
2265 Giving any of the two options below reverts to raw output.
2266 -s Short description (history and attachments only).
2267 -l Longer description (history and attachments only).
2270 - Read IDs from STDIN instead of the command-line.
2271 -t type Specifies object type.
2272 -f a,b,c Restrict the display to the specified fields.
2273 -S var=val Submits the specified variable with the request.
2277 rt show -t ticket -f id,subject,status 1-3
2278 rt show ticket/3/attachments/29
2279 rt show ticket/3/attachments/29/content
2280 rt show ticket/1-3/links
2281 rt show ticket/3/history
2282 rt show -l ticket/3/history
2295 rt edit [options] <object-ids> set field=value [field=value] ...
2296 add field=value [field=value] ...
2297 del field=value [field=value] ...
2299 Edits information corresponding to the specified objects.
2301 A purely numeric object id nnn is translated into ticket/nnn
2303 If, instead of "edit", an action of "new" or "create" is specified,
2304 then a new object is created. In this case, no numeric object IDs
2305 may be specified, but the syntax and behaviour remain otherwise
2308 This command typically starts an editor to allow you to edit object
2309 data in a form for submission. If you specified enough information
2310 on the command-line, however, it will make the submission directly.
2312 The command line may specify field-values in three different ways.
2313 "set" sets the named field to the given value, "add" adds a value
2314 to a multi-valued field, and "del" deletes the corresponding value.
2315 Each "field=value" specification must be given as a single argument.
2317 For some types, object information is further classified into named
2318 attributes (for example, "1-3/links" is a valid ticket specification
2319 that refers to the links for tickets 1-3). These attributes may also
2320 be edited. Consult "rt help <type>" and "rt help object" for further
2325 - Read numeric IDs from STDIN instead of the command-line.
2326 (Useful with rt ls ... | rt edit -; see examples below.)
2327 -i Read a completed form from STDIN before submitting.
2328 -o Dump the completed form to STDOUT instead of submitting.
2329 -e Allows you to edit the form even if the command-line has
2330 enough information to make a submission directly.
2332 Submits the specified variable with the request.
2333 -t type Specifies object type.
2334 -ct content-type Specifies content type of message(tickets only).
2338 # Interactive (starts $EDITOR with a form).
2341 rt create -t ticket -ct text/html
2344 rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2345 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2346 rt edit ticket/4 set priority=3 owner=bar@example.com \
2347 add cc=foo@example.com bcc=quux@example.net
2348 rt create -t ticket set subject='new ticket' priority=10 \
2349 add cc=foo@example.com
2359 rt <comment|correspond> [options] <ticket-id>
2361 Adds a comment (or correspondence) to the specified ticket (the only
2362 difference being that comments aren't sent to the requestors.)
2364 This command will typically start an editor and allow you to type a
2365 comment into a form. If, however, you specified all the necessary
2366 information on the command line, it submits the comment directly.
2368 (See "rt help forms" for more information about forms.)
2372 -m <text> Specify comment text.
2373 -ct <content-type> Specify content-type of comment text.
2374 -a <file> Attach a file to the comment. (May be used more
2375 than once to attach multiple files.)
2376 -c <addrs> A comma-separated list of Cc addresses.
2377 -b <addrs> A comma-separated list of Bcc addresses.
2378 -w <time> Specify the time spent working on this ticket.
2379 -e Starts an editor before the submission, even if
2380 arguments from the command line were sufficient.
2384 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2393 rt merge <from-id> <to-id>
2395 Merges the first ticket specified into the second ticket specified.
2404 rt link [-d] <id-A> <link> <id-B>
2406 Creates (or, with -d, deletes) a link between the specified tickets.
2407 The link can (irrespective of case) be any of:
2409 DependsOn/DependedOnBy: A depends upon B (or vice versa).
2410 RefersTo/ReferredToBy: A refers to B (or vice versa).
2411 MemberOf/HasMember: A is a member of B (or vice versa).
2413 To view a ticket's links, use "rt show ticket/3/links". (See
2414 "rt help ticket" and "rt help show".)
2418 -d Deletes the specified link.
2422 rt link 2 dependson 3
2423 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
2430 RT uses an SQL-like syntax to specify object selection constraints.
2431 See the <RT:...> documentation for details.
2433 (XXX: I'm going to have to write it, aren't I?)
2435 Until it exists here a short description of important constructs:
2437 The two simple forms of query expressions are the constructs
2438 Attribute like Value and
2439 Attribute = Value or Attribute != Value
2441 Whether attributes can be matched using like or using = is built into RT.
2442 The attributes id, Queue, Owner Priority and Status require the = or !=
2445 If Value is a string it must be quoted and may contain the wildcard
2446 character %. If the string does not contain white space, the quoting
2447 may however be omitted, it will be added automatically when parsing
2450 Simple query expressions can be combined using and, or and parentheses
2451 can be used to group expressions.
2453 As a special case a standalone string (which would not form a correct
2454 query) is transformed into (Owner='string' or Requestor like 'string%')
2455 and added to the default query, i.e. the query is narrowed down.
2457 If no Queue=name clause is contained in the query, a default clause
2458 Queue=$config{queue} is added.
2461 Status!='resolved' and Status!='rejected'
2462 (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2470 This program uses RFC822 header-style forms to represent object data
2471 in a form that's suitable for processing both by humans and scripts.
2473 A form is a set of (field, value) specifications, with some initial
2474 commented text and interspersed blank lines allowed for convenience.
2475 Field names may appear more than once in a form; a comma-separated
2476 list of multiple field values may also be specified directly.
2478 Field values can be wrapped as in RFC822, with leading whitespace.
2479 The longest sequence of leading whitespace common to all the lines
2480 is removed (preserving further indentation). There is no limit on
2481 the length of a value.
2483 Multiple forms are separated by a line containing only "--\n".
2485 (XXX: A more detailed specification will be provided soon. For now,
2486 the server-side syntax checking will suffice.)
2497 Get help on any of the following subjects:
2499 - tickets, users, groups, queues.
2500 - show, edit, ls/list/search, new/create.
2502 - query (search query syntax)
2503 - forms (form specification)
2505 - objects (how to specify objects)
2506 - types (a list of object types)
2507 - actions/commands (a list of actions)
2508 - usage/syntax (syntax details)
2509 - conf/config/configuration (configuration details)
2510 - examples (a few useful examples)
2518 some useful examples
2520 All the following list requests will be restricted to the default queue.
2521 That can be changed by adding the option -q queuename
2523 List all tickets that are not rejected/resolved
2525 List all tickets that are new and do not have an owner
2526 rt ls "status=new and owner=nobody"
2527 List all tickets which I have sent or of which I am the owner
2529 List all attributes for the ticket 6977 (ls -l instead of ls)
2531 Show the content of ticket 6977
2533 Show all attributes in the ticket and in the history of the ticket
2535 Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2537 This will open an editor and lets you add text (attribute Text:)
2538 Other attributes may be changed as well, but usually don't do that.
2539 Correspond a ticket (like comment, but mail is also sent to requestors)
2541 Edit a ticket (generic change, interactive using the editor)
2543 Change the owner of a ticket non interactively
2544 rt edit 6977 set owner=myaccount
2546 rt give 6977 account
2549 Change the status of a ticket
2550 rt edit 6977 set status=resolved
2553 Change the status of all tickets I own to resolved !!!
2554 rt ls -i owner=myaccount | rt edit - set status=resolved
2565 Opens an interactive shell, at which you can issue commands of
2566 the form "<action> [options] [arguments]".
2568 To exit the shell, type "quit" or "exit".
2570 Commands can be given at the shell in the same form as they would
2571 be given at the command line without the leading 'rt' invocation.
2575 rt> create -t ticket set subject='new' add cc=foo@example.com
2589 rt <take|untake|steal> <ticket-id>
2591 Sets the owner of the specified ticket to the current user,
2592 assuming said user has the bits to do so, or releases the
2595 'Take' is used on tickets which are not currently owned
2596 (Owner: Nobody), 'steal' is used on tickets which *are*
2597 currently owned, and 'untake' is used to "release" a ticket
2598 (reset its Owner to Nobody). 'Take' cannot be used on
2599 tickets which are currently owned.
2602 alice$ rt create -t ticket set subject="New ticket"
2605 # Owner changed from Nobody to alice
2608 # Owner changed from alice to bob
2610 # Owner changed from bob to Nobody
2618 Use "quit" or "exit" to leave the shell. Only valid within shell
2630 rt - command-line interface to RT 3.0 or newer
2638 This script allows you to interact with an RT server over HTTP, and offers an
2639 interface to RT's functionality that is better-suited to automation and
2640 integration with other tools.
2642 In general, each invocation of this program should specify an action to
2643 perform on one or more objects, and any other arguments required to complete