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>
54 # This program is intentionally written to have as few non-core module
55 # dependencies as possible. It should stay that way.
60 use HTTP::Request::Common;
63 use Time::Local; # used in prettyshow
66 # strong (GSSAPI based) authentication is supported if the server does provide
67 # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
68 # it can be suppressed by setting externalauth=0 (default is undef)
69 eval { require GSSAPI };
70 my $no_strong_auth = 'missing perl module GSSAPI';
72 eval {require LWP::Authen::Negotiate};
73 $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
76 # We derive configuration information from hardwired defaults, dotfiles,
77 # and the RT* environment variables (in increasing order of precedence).
78 # Session information is stored in ~/.rt_sessions.
81 my $HOME = eval{(getpwuid($<))[7]}
82 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
87 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
89 server => 'http://localhost/',
90 query => "Status!='resolved' and Status!='rejected'",
93 # to protect against unlimited searches a better choice would be
94 # queue => 'Unknown_Queue',
95 # setting externalauth => undef will try GSSAPI auth if the corresponding perl
96 # modules are installed, externalauth => 0 is the backward compatible choice
99 config_from_file($ENV{RTCONFIG} || ".rtrc"),
102 my $session = new Session("$HOME/.rt_sessions");
103 my $REST = "$config{server}/REST/1.0";
104 $no_strong_auth = 'switched off by externalauth=0'
105 if defined $config{externalauth};
111 sub DEBUG { warn @_ if $config{debug} >= shift }
113 # These regexes are used by command handlers to parse arguments.
114 # (XXX: Ask Autrijus how i18n changes these definitions.)
116 my $name = '[\w.-]+';
117 my $CF_name = '[\sa-z0-9_ :()/-]+';
118 my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
119 my $label = '[a-zA-Z0-9@_.+-]+';
120 my $labels = "(?:$label,)*$label";
121 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
123 # Our command line looks like this:
125 # rt <action> [options] [arguments]
127 # We'll parse just enough of it to decide upon an action to perform, and
128 # leave the rest to per-action handlers to interpret appropriately.
131 # handler => [ ...aliases... ],
132 version => ["version", "ver"],
134 logout => ["logout"],
135 help => ["help", "man"],
136 show => ["show", "cat"],
137 edit => ["create", "edit", "new", "ed"],
138 list => ["search", "list", "ls"],
139 comment => ["comment", "correspond"],
140 link => ["link", "ln"],
142 grant => ["grant", "revoke"],
143 take => ["take", "steal", "untake"],
144 quit => ["quit", "exit"],
145 setcommand => ["del", "delete", "give", "res", "resolve",
150 foreach my $fn (keys %handlers) {
151 foreach my $alias (@{ $handlers{$fn} }) {
152 $actions{$alias} = \&{"$fn"};
156 # Once we find and call an appropriate handler, we're done.
161 push @ARGV, 'shell' if (!@ARGV); # default to shell mode
162 shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
163 if (@ARGV && exists $actions{$ARGV[0]}) {
164 $action = shift @ARGV;
165 return $actions{$action}->($action);
168 print STDERR "rt: Unknown command '@ARGV'.\n";
169 print STDERR "rt: For help, run 'rt help'.\n";
179 # The following subs are handlers for each entry in %actions.
183 my $term = new Term::ReadLine 'RT CLI';
184 while ( defined ($_ = $term->readline($prompt)) ) {
185 next if /^#/ || /^\s*$/;
187 @ARGV = shellwords($_);
193 print "rt $VERSION\n";
198 submit("$REST/logout") if defined $session->cookie;
209 my ($action, $type, $rv) = @_;
210 $rv = defined $rv ? $rv : 0;
213 # What help topics do we know about?
216 foreach my $item (@{ Form::parse(<DATA>) }) {
217 my $title = $item->[2]{Title};
218 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
220 foreach $title (grep $_, @titles) {
221 $help{$title} = $item->[2]{Text};
226 # What does the user want help with?
227 undef $action if ($action && $actions{$action} eq \&help);
228 unless ($action || $type) {
229 # If we don't know, we'll look for clues in @ARGV.
231 if (exists $help{$_}) { $key = $_; last; }
234 # Tolerate possibly plural words.
236 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
241 if ($type && $action) {
242 $key = "$type.$action";
244 $key ||= $type || $action || "introduction";
246 # Find a suitable topic to display.
247 while (!exists $help{$key}) {
248 if ($type && $action) {
249 if ($key eq "$type.$action") { $key = $action; }
250 elsif ($key eq $action) { $key = $type; }
251 else { $key = "introduction"; }
254 $key = "introduction";
258 print STDERR $help{$key}, "\n\n";
262 # Displays a list of objects that match some specified condition.
265 my ($q, $type, %data);
266 my $orderby = $config{orderby};
268 if ($config{orderby}) {
269 $data{orderby} = $config{orderby};
273 my $reverse_sort = 0;
274 my $queue = $config{queue};
280 $bad = 1, last unless defined($type = get_type_argument());
283 $bad = 1, last unless get_var_argument(\%data);
286 $data{'orderby'} = shift @ARGV;
288 elsif (/^-([isl])$/) {
293 $queue = shift @ARGV;
299 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
300 whine "No valid field list in '-f $ARGV[0]'.";
303 $data{fields} = shift @ARGV;
304 $data{format} = 's' if ! $data{format};
307 elsif (!defined $q && !/^-/) {
311 my $datum = /^-/ ? "option" : "argument";
312 whine "Unrecognised $datum '$_'.";
316 if ( ! $rawprint and ! exists $data{format} ) {
319 if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
320 $data{orderby} =~ s/^-/+/;
321 } elsif ($reverse_sort) {
322 $data{orderby} =~ s/^\+?(.*)/-$1/;
329 $q =~ s/^#//; # get rid of leading hash
331 # only digits, must be an id, formulate a correct query
332 $q = "id=$q" if $q =~ /^\d+$/;
334 # a string only, take it as an owner or requestor (quoting done later)
335 $q = "(Owner=$q or Requestor like $q) and $config{query}"
336 if $q =~ /^[\w\-]+$/;
337 # always add a query for a specific queue or (comma separated) queues
338 $queue =~ s/,/ or Queue=/g if $queue;
339 $q .= " and (Queue=$queue)" if $queue and $q and $q !~ /Queue\s*=/i
342 # correctly quote strings in a query
343 $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
346 unless ($type && defined $q) {
347 my $item = $type ? "query string" : "object type";
348 whine "No $item specified.";
351 #return help("list", $type) if $bad;
352 return suggest_help("list", $type, $bad) if $bad;
354 print "Query:$q\n" if ! $rawprint;
355 my $r = submit("$REST/search/$type", { query => $q, %data });
359 my $forms = Form::parse($r->content);
365 # Displays selected information about a single object.
368 my ($type, @objects, %data);
376 s/^#// if /^#\d+/; # get rid of leading hash
378 $bad = 1, last unless defined($type = get_type_argument());
381 $bad = 1, last unless get_var_argument(\%data);
383 elsif (/^-([isl])$/) {
387 elsif (/^-$/ && !$slurped) {
388 chomp(my @lines = <STDIN>);
390 unless (is_object_spec($_, $type)) {
391 whine "Invalid object on STDIN: '$_'.";
399 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
400 whine "No valid field list in '-f $ARGV[0]'.";
403 $data{fields} = shift @ARGV;
404 # option f requires short raw listing format
408 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
409 push @objects, $spc2;
410 $histspec = is_object_spec("ticket/$_/history", $type);
412 elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
413 push @objects, $spc3;
414 $rawprint = 1 if $_ =~ /\/content$/;
416 elsif (my $spec = is_object_spec($_, $type)) {
417 push @objects, $spec;
418 $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
421 my $datum = /^-/ ? "option" : "argument";
422 whine "Unrecognised $datum '$_'.";
427 push @objects, $histspec if $histspec;
428 $data{format} = 'l' if ! exists $data{format};
432 whine "No objects specified.";
435 #return help("show", $type) if $bad;
436 return suggest_help("show", $type, $bad) if $bad;
438 my $r = submit("$REST/show", { id => \@objects, %data });
440 # if this isn't a text reply, remove the trailing newline so we
441 # don't corrupt things like tarballs when people do
442 # show ticket/id/attachments/id/content > foo.tar.gz
443 if ($r->content_type !~ /^text\//) {
450 # I do not know how to get more than one form correctly returned
451 $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
452 my $forms = Form::parse($c);
458 # To create a new object, we ask the server for a form with the defaults
459 # filled in, allow the user to edit it, and send the form back.
461 # To edit an object, we must ask the server for a form representing that
462 # object, make changes requested by the user (either on the command line
463 # or interactively via $EDITOR), and send the form back.
467 my (%data, $type, @objects);
468 my ($cl, $text, $edit, $input, $output);
470 use vars qw(%set %add %del);
471 %set = %add = %del = ();
477 s/^#// if /^#\d+/; # get rid of leading hash
479 if (/^-e$/) { $edit = 1 }
480 elsif (/^-i$/) { $input = 1 }
481 elsif (/^-o$/) { $output = 1 }
483 $bad = 1, last unless defined($type = get_type_argument());
486 $bad = 1, last unless get_var_argument(\%data);
488 elsif (/^-$/ && !($slurped || $input)) {
489 chomp(my @lines = <STDIN>);
491 unless (is_object_spec($_, $type)) {
492 whine "Invalid object on STDIN: '$_'.";
502 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
503 my ($key, $op, $val) = ($1, $2, $3);
504 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
506 vpush($hash, lc $key, $val);
511 whine "No variables to set.";
516 elsif (/^(?:add|del)$/i) {
518 my $hash = ($_ eq "add") ? \%add : \%del;
520 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
521 my ($key, $val) = ($1, $2);
523 vpush($hash, lc $key, $val);
528 whine "No variables to set.";
533 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
534 push @objects, $spc2;
536 elsif (my $spec = is_object_spec($_, $type)) {
537 push @objects, $spec;
540 my $datum = /^-/ ? "option" : "argument";
541 whine "Unrecognised $datum '$_'.";
546 if ($action =~ /^ed(?:it)?$/) {
548 whine "No objects specified.";
554 whine "You shouldn't specify objects as arguments to $action.";
558 whine "What type of object do you want to create?";
561 @objects = ("$type/new") if defined($type);
563 #return help($action, $type) if $bad;
564 return suggest_help($action, $type, $bad) if $bad;
566 # We need a form to make changes to. We usually ask the server for
567 # one, but we can avoid that if we are fed one on STDIN, or if the
568 # user doesn't want to edit the form by hand, and the command line
569 # specifies only simple variable assignments. We *should* get a
570 # form if we're creating a new ticket, so that the default values
571 # get filled in properly.
573 my @new_objects = grep /\/new$/, @objects;
579 elsif ($edit || %add || %del || !$cl || @new_objects) {
580 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
584 # If any changes were specified on the command line, apply them.
587 # We're updating forms from the server.
588 my $forms = Form::parse($text);
590 foreach my $form (@$forms) {
591 my ($c, $o, $k, $e) = @$form;
594 next if ($e || !@$o);
600 # Make changes to existing fields.
602 if (exists $add{lc $key}) {
603 $val = delete $add{lc $key};
604 vpush($k, $key, $val);
605 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
607 if (exists $del{lc $key}) {
608 $val = delete $del{lc $key};
609 my %val = map {$_=>1} @{ vsplit($val) };
610 $k->{$key} = vsplit($k->{$key});
611 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
613 if (exists $set{lc $key}) {
614 $k->{$key} = delete $set{lc $key};
618 # Then update the others.
619 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
620 foreach $key (keys %add) {
621 vpush($k, $key, $add{$key});
622 $k->{$key} = vsplit($k->{$key});
624 push @$o, (keys %add, keys %set);
627 $text = Form::compose($forms);
630 # We're rolling our own set of forms.
633 my ($type, $ids, $args) =
634 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
637 foreach my $obj (expand_list($ids)) {
638 my %set = (%set, id => "$type/$obj$args");
639 push @forms, ["", [keys %set], \%set];
642 $text = Form::compose(\@forms);
654 # We'll let the user edit the form before sending it to the server,
655 # unless we have enough information to submit it non-interactively.
656 if ($edit || (!$input && !$cl)) {
657 my $newtext = vi($text);
658 # We won't resubmit a bad form unless it was changed.
659 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
663 my $r = submit("$REST/edit", {content => $text, %data});
664 if ($r->code == 409) {
665 # If we submitted a bad form, we'll give the user a chance
666 # to correct it and resubmit.
667 if ($edit || (!$input && !$cl)) {
682 # handler for special edit commands. A valid edit command is constructed and
683 # further work is delegated to the edit handler
687 my ($id, $bad, $what);
690 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
694 whine "No ticket number specified.";
697 if ($action eq 'subject') {
698 my $subject = '"'.join (" ", @ARGV).'"';
700 $what = "subject=$subject";
701 } elsif ($action eq 'give') {
702 my $owner = shift @ARGV;
703 $what = "owner=$owner";
706 if ( $action eq 'delete' or $action eq 'del' ) {
707 $what = "status=deleted";
708 } elsif ($action eq 'resolve' or $action eq 'res' ) {
709 $what = "status=resolved";
710 } elsif ($action eq 'take' ) {
711 $what = "owner=$config{user}";
712 } elsif ($action eq 'untake') {
713 $what = "owner=Nobody";
718 whine "Extraneous arguments for action $action: @ARGV.";
722 whine "unrecognized action $action.";
724 return help("edit", undef, $bad) if $bad;
725 @ARGV = ( $id, "set", $what );
726 print "Executing: rt edit @ARGV\n";
730 # We roll "comment" and "correspond" into the same handler.
734 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
743 elsif (/^-[abcmw]$/) {
745 whine "No argument specified with $_.";
750 unless (-f $ARGV[0] && -r $ARGV[0]) {
751 whine "Cannot read attachment: '$ARGV[0]'.";
754 push @files, shift @ARGV;
757 my $a = $_ eq "-b" ? \@bcc : \@cc;
758 @$a = split /\s*,\s*/, shift @ARGV;
762 if ( $msg =~ /^-$/ ) {
764 while (<STDIN>) { $msg .= $_ }
768 elsif (/-w/) { $wtime = shift @ARGV }
770 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
774 my $datum = /^-/ ? "option" : "argument";
775 whine "Unrecognised $datum '$_'.";
781 whine "No object specified.";
784 #return help($action, "ticket") if $bad;
785 return suggest_help($action, "ticket") if $bad;
789 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
795 Attachment => [ @files ],
796 TimeWorked => $wtime || '',
802 my $text = Form::compose([ $form ]);
804 if ($edit || !$msg) {
809 my $ntext = vi($text);
810 return if ($error && $ntext eq $text);
812 $form = Form::parse($text);
815 ($c, $o, $k, $e) = @{ $form->[0] };
818 $c = "# Syntax error.";
824 @files = @{ vsplit($k->{Attachment}) };
827 $text = Form::compose([[$c, $o, $k, $e]]);
832 foreach my $file (@files) {
833 $data{"attachment_$i"} = bless([ $file ], "Attachment");
836 $data{content} = $text;
838 my $r = submit("$REST/ticket/$id/comment", \%data);
843 # Merge one ticket into another.
851 s/^#// if /^#\d+/; # get rid of leading hash
857 whine "Unrecognised argument: '$_'.";
863 my $evil = @id > 2 ? "many" : "few";
864 whine "Too $evil arguments specified.";
867 #return help("merge", "ticket") if $bad;
868 return suggest_help("merge", "ticket", $bad) if $bad;
870 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
875 # Link one ticket to another.
878 my ($bad, $del, %data) = (0, 0, ());
881 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
882 ReferredToBy HasMember MemberOf);
884 while (@ARGV && $ARGV[0] =~ /^-/) {
891 $bad = 1, last unless defined($type = get_type_argument());
894 whine "Unrecognised option: '$_'.";
899 $type = "ticket" unless $type; # default type to tickets
902 my ($from, $rel, $to) = @ARGV;
903 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
904 my $bad = $from =~ /^\d+$/ ? $to : $from;
905 whine "Invalid $type ID '$bad' specified.";
908 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
909 whine "Invalid link '$rel' for type $type specified.";
912 %data = (id => $from, rel => $rel, to => $to, del => $del);
915 my $bad = @ARGV < 3 ? "few" : "many";
916 whine "Too $bad arguments specified.";
919 return suggest_help("link", $type, $bad) if $bad;
921 my $r = submit("$REST/$type/link", \%data);
926 # Take/steal a ticket
929 my ($bad, %data) = (0, ());
936 unless ($id =~ /^\d+$/) {
937 whine "Invalid ticket ID $id specified.";
942 [ "Ticket", "Action" ],
950 my $text = Form::compose([ $form ]);
951 $data{content} = $text;
954 $bad = @ARGV < 1 ? "few" : "many";
955 whine "Too $bad arguments specified.";
958 return suggest_help("take", "ticket", $bad) if $bad;
960 my $r = submit("$REST/ticket/$id/take", \%data);
965 # Grant/revoke a user's rights.
974 $revoke = 1 if $cmd->{action} eq 'revoke';
978 # Client <-> Server communication.
979 # --------------------------------
981 # This function composes and sends an HTTP request to the RT server, and
982 # interprets the response. It takes a request URI, and optional request
983 # data (a string, or a reference to a set of key-value pairs).
986 my ($uri, $content) = @_;
988 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
989 my $h = HTTP::Headers->new;
991 # Did the caller specify any data to send with the request?
993 if (defined $content) {
994 unless (ref $content) {
995 # If it's just a string, make sure LWP handles it properly.
996 # (By pretending that it's a file!)
997 $content = [ content => [undef, "", Content => $content] ];
999 elsif (ref $content eq 'HASH') {
1001 foreach my $k (keys %$content) {
1002 if (ref $content->{$k} eq 'ARRAY') {
1003 foreach my $v (@{ $content->{$k} }) {
1007 else { push @data, $k, $content->{$k} }
1014 # Should we send authentication information to start a new session?
1015 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1016 (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1017 if ($config{externalauth}) {
1018 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1019 print " Password will be sent to $server $how\n",
1020 " Press CTRL-C now if you do not want to continue\n"
1021 if ! $config{passwd};
1022 } elsif ( $no_strong_auth ) {
1023 if (!defined $session->cookie) {
1024 print " Strong encryption not available, $no_strong_auth\n",
1025 " Password will be sent to $server $how\n",
1026 " Press CTRL-C now if you do not want to continue\n"
1027 if ! $config{passwd};
1028 push @$data, ( user => $config{user} );
1029 push @$data, ( pass => $config{passwd} || read_passwd() );
1033 # Now, we construct the request.
1035 $req = POST($uri, $data, Content_Type => 'form-data');
1040 $session->add_cookie_header($req);
1041 if ($config{externalauth}) {
1045 # Then we send the request and parse the response.
1046 DEBUG(3, $req->as_string);
1047 my $res = $ua->request($req);
1048 DEBUG(3, $res->as_string);
1050 if ($res->is_success) {
1051 # The content of the response we get from the RT server consists
1052 # of an HTTP-like status line followed by optional header lines,
1053 # a blank line, and arbitrary text.
1055 my ($head, $text) = split /\n\n/, $res->content, 2;
1056 my ($status, @headers) = split /\n/, $head;
1057 $text =~ s/\n*$/\n/ if ($text);
1059 # "RT/3.0.1 401 Credentials required"
1060 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1061 warn "rt: Malformed RT response from $config{server}.\n";
1062 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1066 # Our caller can pretend that the server returned a custom HTTP
1067 # response code and message. (Doing that directly is apparently
1068 # not sufficiently portable and uncomplicated.)
1071 $res->content($text);
1072 $session->update($res) if ($res->is_success || $res->code != 401);
1074 if (!$res->is_success) {
1075 # We can deal with authentication failures ourselves. Either
1076 # we sent invalid credentials, or our session has expired.
1077 if ($res->code == 401) {
1079 if (exists $d{user}) {
1080 warn "rt: Incorrect username or password.\n";
1083 elsif ($req->header("Cookie")) {
1084 # We'll retry the request with credentials, unless
1085 # we only wanted to logout in the first place.
1087 return submit(@_) unless $uri eq "$REST/logout";
1090 # Conflicts should be dealt with by the handler and user.
1091 # For anything else, we just die.
1092 elsif ($res->code != 409) {
1093 warn "rt: ", $res->content;
1099 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1106 # Session management.
1107 # -------------------
1109 # Maintains a list of active sessions in the ~/.rt_sessions file.
1114 # Initialises the session cache.
1116 my ($class, $file) = @_;
1118 file => $file || "$HOME/.rt_sessions",
1122 # The current session is identified by the currently configured
1124 ($s, $u) = @config{"server", "user"};
1126 bless $self, $class;
1132 # Returns the current session cookie.
1135 my $cookie = $self->{sids}{$s}{$u};
1136 return defined $cookie ? "RT_SID_$cookie" : undef;
1139 # Deletes the current session cookie.
1142 delete $self->{sids}{$s}{$u};
1145 # Adds a Cookie header to an outgoing HTTP request.
1146 sub add_cookie_header {
1147 my ($self, $request) = @_;
1148 my $cookie = $self->cookie();
1150 $request->header(Cookie => $cookie) if defined $cookie;
1153 # Extracts the Set-Cookie header from an HTTP response, and updates
1154 # session information accordingly.
1156 my ($self, $response) = @_;
1157 my $cookie = $response->header("Set-Cookie");
1159 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1160 $self->{sids}{$s}{$u} = $1;
1164 # Loads the session cache from the specified file.
1166 my ($self, $file) = @_;
1167 $file ||= $self->{file};
1170 open(F, '<', $file) && do {
1171 $self->{file} = $file;
1172 my $sids = $self->{sids} = {};
1175 next if /^$/ || /^#/;
1176 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1177 my ($server, $user, $cookie) = split / /, $_;
1178 $sids->{$server}{$user} = $cookie;
1185 # Writes the current session cache to the specified file.
1187 my ($self, $file) = shift;
1188 $file ||= $self->{file};
1191 open(F, '>', $file) && do {
1192 my $sids = $self->{sids};
1193 foreach my $server (keys %$sids) {
1194 foreach my $user (keys %{ $sids->{$server} }) {
1195 my $sid = $sids->{$server}{$user};
1197 print F "$server $user $sid\n";
1217 # Forms are RFC822-style sets of (field, value) specifications with some
1218 # initial comments and interspersed blank lines allowed for convenience.
1219 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1221 # Each form is parsed into an array with four elements: commented text
1222 # at the start of the form, an array with the order of keys, a hash with
1223 # key/value pairs, and optional error text if the form syntax was wrong.
1225 # Returns a reference to an array of parsed forms.
1229 my @lines = split /\n/, $_[0] if $_[0];
1230 my ($c, $o, $k, $e) = ("", [], {}, "");
1234 my $line = shift @lines;
1236 next LINE if $line eq '';
1238 if ($line eq '--') {
1239 # We reached the end of one form. We'll ignore it if it was
1240 # empty, and store it otherwise, errors and all.
1241 if ($e || $c || @$o) {
1242 push @forms, [ $c, $o, $k, $e ];
1243 $c = ""; $o = []; $k = {}; $e = "";
1247 elsif ($state != -1) {
1248 if ($state == 0 && $line =~ /^#/) {
1249 # Read an optional block of comments (only) at the start
1253 while (@lines && $lines[0] =~ /^#/) {
1254 $c .= "\n".shift @lines;
1258 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1259 # Read a field: value specification.
1263 # Read continuation lines, if any.
1264 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1265 push @v, shift @lines;
1267 pop @v while (@v && $v[-1] eq '');
1269 # Strip longest common leading indent from text.
1271 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1272 $ws = $ls if (!$ws || length($ls) < length($ws));
1274 s/^$ws// foreach @v;
1276 push(@$o, $f) unless exists $k->{$f};
1277 vpush($k, $f, join("\n", @v));
1281 elsif ($line !~ /^#/) {
1282 # We've found a syntax error, so we'll reconstruct the
1283 # form parsed thus far, and add an error marker. (>>)
1285 $e = Form::compose([[ "", $o, $k, "" ]]);
1286 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1290 # We saw a syntax error earlier, so we'll accumulate the
1291 # contents of this form until the end.
1295 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1297 foreach my $l (keys %$k) {
1298 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1304 # Returns text representing a set of forms.
1309 foreach my $form (@$forms) {
1310 my ($c, $o, $k, $e) = @$form;
1323 foreach my $key (@$o) {
1326 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1328 $sp = " "x(length("$key: "));
1329 $sp = " "x4 if length($sp) > 16;
1331 foreach $v (@values) {
1337 push @lines, "$line\n\n";
1340 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1343 push @lines, "$key: $v\n\n";
1346 length($line)+length($v)-rindex($line, "\n") >= 70)
1348 $line .= ",\n$sp$v";
1351 $line = $line ? "$line,$v" : "$key: $v";
1355 $line = "$key:" unless @values;
1357 if ($line =~ /\n/) {
1358 if (@lines && $lines[-1] !~ /\n\n$/) {
1363 push @lines, "$line\n";
1367 $text .= join "", @lines;
1375 return join "\n--\n\n", @text;
1381 # Returns configuration information from the environment.
1382 sub config_from_env {
1385 foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1387 if (exists $ENV{"RT$k"}) {
1388 $env{lc $k} = $ENV{"RT$k"};
1395 # Finds a suitable configuration file and returns information from it.
1396 sub config_from_file {
1400 # We'll use an absolute path if we were given one.
1401 return parse_config_file($rc);
1404 # Otherwise we'll use the first file we can find in the current
1405 # directory, or in one of its (increasingly distant) ancestors.
1407 my @dirs = split /\//, cwd;
1409 my $file = join('/', @dirs, $rc);
1411 return parse_config_file($file);
1414 # Remove the last directory component each time.
1418 # Still nothing? We'll fall back to some likely defaults.
1419 for ("$HOME/$rc", "@LOCAL_ETC_PATH@/rt.conf", "/etc/rt.conf") {
1420 return parse_config_file($_) if (-r $_);
1427 # Makes a hash of the specified configuration file.
1428 sub parse_config_file {
1431 local $_; # $_ may be aliased to a constant, from line 1163
1433 open(CFG, '<', $file) && do {
1436 next if (/^#/ || /^\s*$/);
1438 if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1442 die "rt: $file:$.: unknown configuration directive.\n";
1454 my $sub = (caller(1))[3];
1455 $sub =~ s/^main:://;
1456 warn "rt: $sub: @_\n";
1461 eval 'require Term::ReadKey';
1463 die "No password specified (and Term::ReadKey not installed).\n";
1467 Term::ReadKey::ReadMode('noecho');
1468 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1469 Term::ReadKey::ReadMode('restore');
1477 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1481 my $handle = File::Temp->new;
1482 print $handle $text;
1485 system($editor, $handle->filename) && die "Couldn't run $editor.\n";
1487 open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
1494 # Add a value to a (possibly multi-valued) hash key.
1496 my ($hash, $key, $val) = @_;
1497 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1499 if (exists $hash->{$key}) {
1500 unless (ref $hash->{$key} eq 'ARRAY') {
1501 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1502 $hash->{$key} = \@v;
1504 push @{ $hash->{$key} }, @val;
1507 $hash->{$key} = $val;
1511 # "Normalise" a hash key that's known to be multi-valued.
1515 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1517 foreach my $line (map {split /\n/} @values) {
1518 # XXX: This should become a real parser, Ã la Text::ParseWords.
1521 my ( $a, $b ) = split /\s*,\s*/, $line, 2;
1524 no warnings 'uninitialized';
1527 while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/
1528 && $a =~ /(\\)+'$/ )) {
1529 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1534 elsif ( $a =~ /^q{/ ) {
1536 while ( $a !~ /}$/ ) {
1538 split /\s*,\s*/, $b, 2;
1548 ( $a, $b ) = split /\s*,\s*/, $b, 2;
1557 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1558 # change both functions at once
1563 foreach (split /\s*,\s*/, $list) {
1564 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1567 return map $_->[0], # schwartzian transform
1569 defined $a->[1] && defined $b->[1]?
1572 :!defined $a->[1] && !defined $b->[1]?
1575 # mix, number must be first
1576 :defined $a->[1]? -1: 1
1578 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1582 sub get_type_argument {
1586 $type = shift @ARGV;
1587 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1588 # We want whine to mention our caller, not us.
1589 @_ = ("Invalid type '$type' specified.");
1594 @_ = ("No type argument specified with -t.");
1598 $type =~ s/s$//; # "Plural". Ugh.
1602 sub get_var_argument {
1606 my $kv = shift @ARGV;
1607 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1608 push @{ $data->{$k} }, $v;
1611 @_ = ("Invalid variable specification: '$kv'.");
1616 @_ = ("No variable argument specified with -S.");
1621 sub is_object_spec {
1622 my ($spec, $type) = @_;
1624 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1625 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1630 my ($action, $type, $rv) = @_;
1632 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1633 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1638 # simplified procedure for parsing date, avoid loading Date::Parse
1639 my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1640 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1642 my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1643 if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1644 ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1645 $mon = $month{$monstr} if exists $month{$monstr};
1646 } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1647 ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1649 if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1650 return timelocal($sec,$min,$hr,$day,$mon,$yr);
1652 print "Unknown date format in parsedate: $_\n";
1658 my ($old, $new) = @_;
1659 $new = time() if ! $new;
1660 $old = str2time($old) if $old !~ /^\d+$/;
1661 $new = str2time($new) if $new !~ /^\d+$/;
1662 return "???" if ! $old or ! $new;
1664 my %seconds = (min => 60,
1669 yr => 60*60*24*365);
1671 my $diff = $new - $old;
1673 my $howmuch = $diff;
1674 for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1675 last if $diff < $seconds{$_};
1677 $howmuch = int($diff/$seconds{$_});
1679 return "$howmuch $what";
1684 my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1686 # dates are in local time zone
1688 print "Date: $k->{Created}\n";
1689 print "From: $k->{Requestors}\n";
1690 print "Cc: $k->{Cc}\n" if $k->{Cc};
1691 print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1692 print "X-Queue: $k->{Queue}\n";
1693 print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1695 # dates in these attributes are in GMT and will be converted
1696 foreach my $form (@$forms) {
1697 my ($c, $o, $k, $e) = @$form;
1698 next if ! $k->{id} or exists $k->{Queue};
1699 if ( exists $k->{Created} ) {
1700 my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1702 my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1703 if ( exists $k->{Description} ) {
1704 print "===> $k->{Description} on $created\n";
1707 print "$k->{Content}\n" if exists $k->{Content} and
1708 $k->{Content} !~ /to have no content$/ and
1709 $k->{Type} ne 'EmailRecord';
1710 print "$k->{Attachments}\n" if exists $k->{Attachments} and
1717 my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1718 $heading .= '-' x 80 . "\n";
1720 foreach my $form (@$forms) {
1721 my ($c, $o, $k, $e) = @$form;
1723 print $heading if $heading;
1726 $id =~ s!^ticket/!!;
1727 my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1728 $owner = substr($owner, 0, 5);
1729 my $queue = substr($k->{Queue}, 0, 5);
1730 my $subject = substr($k->{Subject}, 0, 30);
1731 my $age = date_diff($k->{Created});
1732 my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1733 my $status = substr($k->{Status}, 0, 6);
1734 my $requestor = substr($k->{Requestors}, 0, 9);
1735 my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1736 $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1737 if ( $k->{Owner} eq 'Nobody' ) {
1739 } elsif ($k->{Owner} eq $config{user} ) {
1745 print "No matches found\n" if $heading;
1746 printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1748 printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1749 print @open if @open;
1758 This is a command-line interface to RT 3.0 or newer.
1760 It allows you to interact with an RT server over HTTP, and offers an
1761 interface to RT's functionality that is better-suited to automation
1762 and integration with other tools.
1764 In general, each invocation of this program should specify an action
1765 to perform on one or more objects, and any other arguments required
1766 to complete the desired action.
1768 For more information:
1770 - rt help usage (syntax information)
1771 - rt help objects (how to specify objects)
1772 - rt help actions (a list of possible actions)
1773 - rt help types (a list of object types)
1775 - rt help config (configuration details)
1776 - rt help examples (a few useful examples)
1777 - rt help topics (a list of help topics)
1787 rt <action> [options] [arguments]
1791 Each invocation of this program must specify an action (e.g. "edit",
1792 "create"), options to modify behaviour, and other arguments required
1793 by the specified action. (For example, most actions expect a list of
1794 numeric object IDs to act upon.)
1796 The details of the syntax and arguments for each action are given by
1797 "rt help <action>". Some actions may be referred to by more than one
1798 name ("create" is the same as "new", for example).
1800 You may also call "rt shell", which will give you an 'rt>' prompt at
1801 which you can issue commands of the form "<action> [options]
1802 [arguments]". See "rt help shell" for details.
1804 Objects are identified by a type and an ID (which can be a name or a
1805 number, depending on the type). For some actions, the object type is
1806 implied (you can only comment on tickets); for others, the user must
1807 specify it explicitly. See "rt help objects" for details.
1809 In syntax descriptions, mandatory arguments that must be replaced by
1810 appropriate value are enclosed in <>, and optional arguments are
1811 indicated by [] (for example, <action> and [options] above).
1813 For more information:
1815 - rt help objects (how to specify objects)
1816 - rt help actions (a list of actions)
1817 - rt help types (a list of object types)
1818 - rt help shell (how to use the shell)
1824 Title: configuration
1827 This program has two major sources of configuration information: its
1828 configuration files, and the environment.
1830 The program looks for configuration directives in a file named .rtrc
1831 (or $RTCONFIG; see below) in the current directory, and then in more
1832 distant ancestors, until it reaches /. If no suitable configuration
1833 files are found, it will also check for ~/.rtrc, @LOCAL_ETC_PATH@/rt.conf
1836 Configuration directives:
1838 The following directives may occur, one per line:
1840 - server <URL> URL to RT server.
1841 - user <username> RT username.
1842 - passwd <passwd> RT user's password.
1843 - query <RT Query> Default RT Query for list action
1844 - orderby <order> Default RT order for list action
1845 - queue <queuename> Default RT Queue for list action
1846 - externalauth <0|1> Use HTTP Basic authentication
1847 explicitely setting externalauth to 0 inhibits also GSSAPI based
1848 authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1850 Blank and #-commented lines are ignored.
1852 Sample configuration file contents:
1854 server https://rt.somewhere.com/
1855 # more than one queue can be given (by adding a query expression)
1856 queue helpdesk or queue=support
1857 query Status != resolved and Owner=myaccount
1860 Environment variables:
1862 The following environment variables override any corresponding
1863 values defined in configuration files:
1869 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1870 - RTCONFIG Specifies a name other than ".rtrc" for the
1872 - RTQUERY Default RT Query for rt list
1873 - RTORDERBY Default order for rt list
1882 <type>/<id>[/<attributes>]
1884 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1885 ID. Some types of objects can also be identified by name (like users
1886 and queues). Furthermore, objects may have named attributes (such as
1887 "ticket/1/history").
1889 An object specification is like a path in a virtual filesystem, with
1890 object types as top-level directories, object IDs as subdirectories,
1891 and named attributes as further subdirectories.
1893 A comma-separated list of names, numeric IDs, or numeric ranges can
1894 be used to specify more than one object of the same type. Note that
1895 the list must be a single argument (i.e., no spaces). For example,
1896 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1897 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1899 If just a number is given as object specification it will be
1900 interpreted as ticket/<number>
1904 1 # the same as ticket/1
1906 ticket/1/attachments
1907 ticket/1/attachments/3
1908 ticket/1/attachments/3/content
1910 ticket/1-3,5-7/history
1914 user/ams,rai,1/rights
1916 For more information:
1918 - rt help <action> (action-specific details)
1919 - rt help <type> (type-specific details)
1927 You can currently perform the following actions on all objects:
1929 - list (list objects matching some condition)
1930 - show (display object details)
1931 - edit (edit object details)
1932 - create (create a new object)
1934 Each type may define actions specific to itself; these are listed in
1935 the help item about that type.
1937 For more information:
1939 - rt help <action> (action-specific details)
1940 - rt help types (a list of possible types)
1942 The following actions on tickets are also possible:
1944 - comment Add comments to a ticket
1945 - correspond Add comments to a ticket
1946 - merge Merge one ticket into another
1947 - link Link one ticket to another
1948 - take Take a ticket (steal and untake are possible as well)
1950 For several edit set subcommands that are frequently used abbreviations
1951 have been introduced. These abbreviations are:
1953 - delete or del delete a ticket (edit set status=deleted)
1954 - resolve or res resolve a ticket (edit set status=resolved)
1955 - subject change subject of ticket (edit set subject=string)
1956 - give give a ticket to somebody (edit set owner=user)
1963 You can currently operate on the following types of objects:
1970 For more information:
1972 - rt help <type> (type-specific details)
1973 - rt help objects (how to specify objects)
1974 - rt help actions (a list of possible actions)
1981 Tickets are identified by a numeric ID.
1983 The following generic operations may be performed upon tickets:
1990 In addition, the following ticket-specific actions exist:
2006 The following attributes can be used with "rt show" or "rt edit"
2007 to retrieve or edit other information associated with tickets:
2009 links A ticket's relationships with others.
2010 history All of a ticket's transactions.
2011 history/type/<type> Only a particular type of transaction.
2012 history/id/<id> Only the transaction of the specified id.
2013 attachments A list of attachments.
2014 attachments/<id> The metadata for an individual attachment.
2015 attachments/<id>/content The content of an individual attachment.
2023 Users and groups are identified by name or numeric ID.
2025 The following generic operations may be performed upon them:
2032 In addition, the following type-specific actions exist:
2039 The following attributes can be used with "rt show" or "rt edit"
2040 to retrieve or edit other information associated with users and
2043 rights Global rights granted to this user.
2044 rights/<queue> Queue rights for this user.
2051 Queues are identified by name or numeric ID.
2053 Currently, they can be subjected to the following actions:
2066 rt subject <id> <new subject text>
2068 Change the subject of a ticket whose ticket id is given.
2077 rt give <id> <accountname>
2079 Give a ticket whose ticket id is given to another user.
2088 Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2099 Take a ticket whose ticket id is given, i.e. set the owner to myself.
2110 Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2122 Resolves a ticket whose ticket id is given.
2134 Deletes a ticket whose ticket id is given.
2145 Terminates the currently established login session. You will need to
2146 provide authentication credentials before you can continue using the
2147 server. (See "rt help config" for details about authentication.)
2158 rt <ls|list|search> [options] "query string"
2160 Displays a list of objects matching the specified conditions.
2161 ("ls", "list", and "search" are synonyms.)
2163 Conditions are expressed in the SQL-like syntax used internally by
2164 RT3. (For more information, see "rt help query".) The query string
2165 must be supplied as one argument.
2167 (Right now, the server doesn't support listing anything but tickets.
2168 Other types will be supported in future; this client will be able to
2169 take advantage of that support without any changes.)
2173 The following options control how much information is displayed
2174 about each matching object:
2176 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
2177 -s Short description.
2178 -l Longer description.
2179 -f <field[s] Display only the fields listed and the ticket id
2183 -o +/-<field> Orders the returned list by the specified field.
2184 -r reversed order (useful if a default was given)
2185 -q queue[s] restricts the query to the queue[s] given
2186 multiple queues are separated by comma
2187 -S var=val Submits the specified variable with the request.
2188 -t type Specifies the type of object to look for. (The
2189 default is "ticket".)
2193 rt ls "Priority > 5 and Status=new"
2194 rt ls -o +Subject "Priority > 5 and Status=new"
2195 rt ls -o -Created "Priority > 5 and Status=new"
2196 rt ls -i "Priority > 5"|rt edit - set status=resolved
2197 rt ls -t ticket "Subject like '[PATCH]%'"
2199 rt ls -f owner,subject
2208 rt show [options] <object-ids>
2210 Displays details of the specified objects.
2212 For some types, object information is further classified into named
2213 attributes (for example, "1-3/links" is a valid ticket specification
2214 that refers to the links for tickets 1-3). Consult "rt help <type>"
2215 and "rt help objects" for further details.
2217 If only a number is given it will be interpreted as the objects
2218 ticket/number and ticket/number/history
2220 This command writes a set of forms representing the requested object
2225 The following options control how much information is displayed
2226 about each matching object:
2228 Without any formatting options prettyprinted output is generated.
2229 Giving any of the two options below reverts to raw output.
2230 -s Short description (history and attachments only).
2231 -l Longer description (history and attachments only).
2234 - Read IDs from STDIN instead of the command-line.
2235 -t type Specifies object type.
2236 -f a,b,c Restrict the display to the specified fields.
2237 -S var=val Submits the specified variable with the request.
2241 rt show -t ticket -f id,subject,status 1-3
2242 rt show ticket/3/attachments/29
2243 rt show ticket/3/attachments/29/content
2244 rt show ticket/1-3/links
2245 rt show ticket/3/history
2246 rt show -l ticket/3/history
2259 rt edit [options] <object-ids> set field=value [field=value] ...
2260 add field=value [field=value] ...
2261 del field=value [field=value] ...
2263 Edits information corresponding to the specified objects.
2265 A purely numeric object id nnn is translated into ticket/nnn
2267 If, instead of "edit", an action of "new" or "create" is specified,
2268 then a new object is created. In this case, no numeric object IDs
2269 may be specified, but the syntax and behaviour remain otherwise
2272 This command typically starts an editor to allow you to edit object
2273 data in a form for submission. If you specified enough information
2274 on the command-line, however, it will make the submission directly.
2276 The command line may specify field-values in three different ways.
2277 "set" sets the named field to the given value, "add" adds a value
2278 to a multi-valued field, and "del" deletes the corresponding value.
2279 Each "field=value" specification must be given as a single argument.
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). These attributes may also
2284 be edited. Consult "rt help <type>" and "rt help object" for further
2289 - Read numeric IDs from STDIN instead of the command-line.
2290 (Useful with rt ls ... | rt edit -; see examples below.)
2291 -i Read a completed form from STDIN before submitting.
2292 -o Dump the completed form to STDOUT instead of submitting.
2293 -e Allows you to edit the form even if the command-line has
2294 enough information to make a submission directly.
2296 Submits the specified variable with the request.
2297 -t type Specifies object type.
2301 # Interactive (starts $EDITOR with a form).
2306 rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2307 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2308 rt edit ticket/4 set priority=3 owner=bar@example.com \
2309 add cc=foo@example.com bcc=quux@example.net
2310 rt create -t ticket set subject='new ticket' priority=10 \
2311 add cc=foo@example.com
2321 rt <comment|correspond> [options] <ticket-id>
2323 Adds a comment (or correspondence) to the specified ticket (the only
2324 difference being that comments aren't sent to the requestors.)
2326 This command will typically start an editor and allow you to type a
2327 comment into a form. If, however, you specified all the necessary
2328 information on the command line, it submits the comment directly.
2330 (See "rt help forms" for more information about forms.)
2334 -m <text> Specify comment text.
2335 -a <file> Attach a file to the comment. (May be used more
2336 than once to attach multiple files.)
2337 -c <addrs> A comma-separated list of Cc addresses.
2338 -b <addrs> A comma-separated list of Bcc addresses.
2339 -w <time> Specify the time spent working on this ticket.
2340 -e Starts an editor before the submission, even if
2341 arguments from the command line were sufficient.
2345 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2354 rt merge <from-id> <to-id>
2356 Merges the first ticket specified into the second ticket specified.
2365 rt link [-d] <id-A> <link> <id-B>
2367 Creates (or, with -d, deletes) a link between the specified tickets.
2368 The link can (irrespective of case) be any of:
2370 DependsOn/DependedOnBy: A depends upon B (or vice versa).
2371 RefersTo/ReferredToBy: A refers to B (or vice versa).
2372 MemberOf/HasMember: A is a member of B (or vice versa).
2374 To view a ticket's links, use "rt show ticket/3/links". (See
2375 "rt help ticket" and "rt help show".)
2379 -d Deletes the specified link.
2383 rt link 2 dependson 3
2384 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
2397 RT3 uses an SQL-like syntax to specify object selection constraints.
2398 See the <RT:...> documentation for details.
2400 (XXX: I'm going to have to write it, aren't I?)
2402 Until it exists here a short description of important constructs:
2404 The two simple forms of query expressions are the constructs
2405 Attribute like Value and
2406 Attribute = Value or Attribute != Value
2408 Whether attributes can be matched using like or using = is built into RT.
2409 The attributes id, Queue, Owner Priority and Status require the = or !=
2412 If Value is a string it must be quoted and may contain the wildcard
2413 character %. If the string does not contain white space, the quoting
2414 may however be omitted, it will be added automatically when parsing
2417 Simple query expressions can be combined using and, or and parentheses
2418 can be used to group expressions.
2420 As a special case a standalone string (which would not form a correct
2421 query) is transformed into (Owner='string' or Requestor like 'string%')
2422 and added to the default query, i.e. the query is narrowed down.
2424 If no Queue=name clause is contained in the query, a default clause
2425 Queue=$config{queue} is added.
2428 Status!='resolved' and Status!='rejected'
2429 (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2437 This program uses RFC822 header-style forms to represent object data
2438 in a form that's suitable for processing both by humans and scripts.
2440 A form is a set of (field, value) specifications, with some initial
2441 commented text and interspersed blank lines allowed for convenience.
2442 Field names may appear more than once in a form; a comma-separated
2443 list of multiple field values may also be specified directly.
2445 Field values can be wrapped as in RFC822, with leading whitespace.
2446 The longest sequence of leading whitespace common to all the lines
2447 is removed (preserving further indentation). There is no limit on
2448 the length of a value.
2450 Multiple forms are separated by a line containing only "--\n".
2452 (XXX: A more detailed specification will be provided soon. For now,
2453 the server-side syntax checking will suffice.)
2464 Get help on any of the following subjects:
2466 - tickets, users, groups, queues.
2467 - show, edit, ls/list/search, new/create.
2469 - query (search query syntax)
2470 - forms (form specification)
2472 - objects (how to specify objects)
2473 - types (a list of object types)
2474 - actions/commands (a list of actions)
2475 - usage/syntax (syntax details)
2476 - conf/config/configuration (configuration details)
2477 - examples (a few useful examples)
2485 some useful examples
2487 All the following list requests will be restricted to the default queue.
2488 That can be changed by adding the option -q queuename
2490 List all tickets that are not rejected/resolved
2492 List all tickets that are new and do not have an owner
2493 rt ls "status=new and owner=nobody"
2494 List all tickets which I have sent or of which I am the owner
2496 List all attributes for the ticket 6977 (ls -l instead of ls)
2498 Show the content of ticket 6977
2500 Show all attributes in the ticket and in the history of the ticket
2502 Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2504 This will open an editor and lets you add text (attribute Text:)
2505 Other attributes may be changed as well, but usually don't do that.
2506 Correspond a ticket (like comment, but mail is also sent to requestors)
2508 Edit a ticket (generic change, interactive using the editor)
2510 Change the owner of a ticket non interactively
2511 rt edit 6977 set owner=myaccount
2513 rt give 6977 account
2516 Change the status of a ticket
2517 rt edit 6977 set status=resolved
2520 Change the status of all tickets I own to resolved !!!
2521 rt ls -i owner=myaccount | rt edit - set status=resolved
2532 Opens an interactive shell, at which you can issue commands of
2533 the form "<action> [options] [arguments]".
2535 To exit the shell, type "quit" or "exit".
2537 Commands can be given at the shell in the same form as they would
2538 be given at the command line without the leading 'rt' invocation.
2542 rt> create -t ticket set subject='new' add cc=foo@example.com
2556 rt <take|untake|steal> <ticket-id>
2558 Sets the owner of the specified ticket to the current user,
2559 assuming said user has the bits to do so, or releases the
2562 'Take' is used on tickets which are not currently owned
2563 (Owner: Nobody), 'steal' is used on tickets which *are*
2564 currently owned, and 'untake' is used to "release" a ticket
2565 (reset its Owner to Nobody). 'Take' cannot be used on
2566 tickets which are currently owned.
2569 alice$ rt create -t ticket set subject="New ticket"
2572 # Owner changed from Nobody to alice
2575 # Owner changed from alice to bob
2577 # Owner changed from bob to Nobody
2585 Use "quit" or "exit" to leave the shell. Only valid within shell