2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
7 # <jesse@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
65 # strong (GSSAPI based) authentication is supported if the server does provide
66 # it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
67 # it can be suppressed by setting externalauth=0 (default is undef)
68 eval { require GSSAPI };
69 my $no_strong_auth = 'missing perl module GSSAPI';
71 eval {require LWP::Authen::Negotiate};
72 $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
75 # We derive configuration information from hardwired defaults, dotfiles,
76 # and the RT* environment variables (in increasing order of precedence).
77 # Session information is stored in ~/.rt_sessions.
80 my $HOME = eval{(getpwuid($<))[7]}
81 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
86 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
88 server => 'http://localhost/',
89 query => "Status!='resolved' and Status!='rejected'",
92 # to protect against unlimited searches a better choice would be
93 # queue => 'Unknown_Queue',
94 # setting externalauth => undef will try GSSAPI auth if the corresponding perl
95 # modules are installed, externalauth => 0 is the backward compatible choice
98 config_from_file($ENV{RTCONFIG} || ".rtrc"),
101 my $session = new Session("$HOME/.rt_sessions");
102 my $REST = "$config{server}/REST/1.0";
103 $no_strong_auth = 'switched off by externalauth=0'
104 if defined $config{externalauth};
110 sub DEBUG { warn @_ if $config{debug} >= shift }
112 # These regexes are used by command handlers to parse arguments.
113 # (XXX: Ask Autrijus how i18n changes these definitions.)
115 my $name = '[\w.-]+';
116 my $CF_name = '[\sa-z0-9_ :()/-]+';
117 my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
118 my $label = '[a-zA-Z0-9@_.+-]+';
119 my $labels = "(?:$label,)*$label";
120 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
122 # Our command line looks like this:
124 # rt <action> [options] [arguments]
126 # We'll parse just enough of it to decide upon an action to perform, and
127 # leave the rest to per-action handlers to interpret appropriately.
130 # handler => [ ...aliases... ],
131 version => ["version", "ver"],
133 logout => ["logout"],
134 help => ["help", "man"],
135 show => ["show", "cat"],
136 edit => ["create", "edit", "new", "ed"],
137 list => ["search", "list", "ls"],
138 comment => ["comment", "correspond"],
139 link => ["link", "ln"],
141 grant => ["grant", "revoke"],
142 take => ["take", "steal", "untake"],
143 quit => ["quit", "exit"],
144 setcommand => ["del", "delete", "give", "res", "resolve",
149 foreach my $fn (keys %handlers) {
150 foreach my $alias (@{ $handlers{$fn} }) {
151 $actions{$alias} = \&{"$fn"};
155 # Once we find and call an appropriate handler, we're done.
160 push @ARGV, 'shell' if (!@ARGV); # default to shell mode
161 shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
162 if (@ARGV && exists $actions{$ARGV[0]}) {
163 $action = shift @ARGV;
164 return $actions{$action}->($action);
167 print STDERR "rt: Unknown command '@ARGV'.\n";
168 print STDERR "rt: For help, run 'rt help'.\n";
178 # The following subs are handlers for each entry in %actions.
182 my $term = new Term::ReadLine 'RT CLI';
183 while ( defined ($_ = $term->readline($prompt)) ) {
184 next if /^#/ || /^\s*$/;
186 @ARGV = shellwords($_);
192 print "rt $VERSION\n";
197 submit("$REST/logout") if defined $session->cookie;
208 my ($action, $type, $rv) = @_;
209 $rv = defined $rv ? $rv : 0;
212 # What help topics do we know about?
215 foreach my $item (@{ Form::parse(<DATA>) }) {
216 my $title = $item->[2]{Title};
217 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
219 foreach $title (grep $_, @titles) {
220 $help{$title} = $item->[2]{Text};
225 # What does the user want help with?
226 undef $action if ($action && $actions{$action} eq \&help);
227 unless ($action || $type) {
228 # If we don't know, we'll look for clues in @ARGV.
230 if (exists $help{$_}) { $key = $_; last; }
233 # Tolerate possibly plural words.
235 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
240 if ($type && $action) {
241 $key = "$type.$action";
243 $key ||= $type || $action || "introduction";
245 # Find a suitable topic to display.
246 while (!exists $help{$key}) {
247 if ($type && $action) {
248 if ($key eq "$type.$action") { $key = $action; }
249 elsif ($key eq $action) { $key = $type; }
250 else { $key = "introduction"; }
253 $key = "introduction";
257 print STDERR $help{$key}, "\n\n";
261 # Displays a list of objects that match some specified condition.
264 my ($q, $type, %data);
265 my $orderby = $config{orderby};
267 if ($config{orderby}) {
268 $data{orderby} = $config{orderby};
272 my $reverse_sort = 0;
273 my $queue = $config{queue};
279 $bad = 1, last unless defined($type = get_type_argument());
282 $bad = 1, last unless get_var_argument(\%data);
285 $data{'orderby'} = shift @ARGV;
287 elsif (/^-([isl])$/) {
292 $queue = shift @ARGV;
298 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
299 whine "No valid field list in '-f $ARGV[0]'.";
302 $data{fields} = shift @ARGV;
303 $data{format} = 's' if ! $data{format};
306 elsif (!defined $q && !/^-/) {
310 my $datum = /^-/ ? "option" : "argument";
311 whine "Unrecognised $datum '$_'.";
315 if ( ! $rawprint and ! exists $data{format} ) {
318 if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
319 $data{orderby} =~ s/^-/+/;
320 } elsif ($reverse_sort) {
321 $data{orderby} =~ s/^\+?(.*)/-$1/;
328 $q =~ s/^#//; # get rid of leading hash
330 # only digits, must be an id, formulate a correct query
331 $q = "id=$q" if $q =~ /^\d+$/;
333 # a string only, take it as an owner or requestor (quoting done later)
334 $q = "(Owner=$q or Requestor like $q) and $config{query}"
335 if $q =~ /^[\w\-]+$/;
336 # always add a query for a specific queue or (comma separated) queues
337 $queue =~ s/,/ or Queue=/g if $queue;
338 $q .= " and (Queue=$queue)" if $queue and $q and $q !~ /Queue\s*=/i
341 # correctly quote strings in a query
342 $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
345 unless ($type && defined $q) {
346 my $item = $type ? "query string" : "object type";
347 whine "No $item specified.";
350 #return help("list", $type) if $bad;
351 return suggest_help("list", $type, $bad) if $bad;
353 print "Query:$q\n" if ! $rawprint;
354 my $r = submit("$REST/search/$type", { query => $q, %data });
358 my $forms = Form::parse($r->content);
364 # Displays selected information about a single object.
367 my ($type, @objects, %data);
375 s/^#// if /^#\d+/; # get rid of leading hash
377 $bad = 1, last unless defined($type = get_type_argument());
380 $bad = 1, last unless get_var_argument(\%data);
382 elsif (/^-([isl])$/) {
386 elsif (/^-$/ && !$slurped) {
387 chomp(my @lines = <STDIN>);
389 unless (is_object_spec($_, $type)) {
390 whine "Invalid object on STDIN: '$_'.";
398 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
399 whine "No valid field list in '-f $ARGV[0]'.";
402 $data{fields} = shift @ARGV;
403 # option f requires short raw listing format
407 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
408 push @objects, $spc2;
409 $histspec = is_object_spec("ticket/$_/history", $type);
411 elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
412 push @objects, $spc3;
413 $rawprint = 1 if $_ =~ /\/content$/;
415 elsif (my $spec = is_object_spec($_, $type)) {
416 push @objects, $spec;
417 $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
420 my $datum = /^-/ ? "option" : "argument";
421 whine "Unrecognised $datum '$_'.";
426 push @objects, $histspec if $histspec;
427 $data{format} = 'l' if ! exists $data{format};
431 whine "No objects specified.";
434 #return help("show", $type) if $bad;
435 return suggest_help("show", $type, $bad) if $bad;
437 my $r = submit("$REST/show", { id => \@objects, %data });
439 # if this isn't a text reply, remove the trailing newline so we
440 # don't corrupt things like tarballs when people do
441 # show ticket/id/attachments/id/content > foo.tar.gz
442 if ($r->content_type !~ /^text\//) {
449 # I do not know how to get more than one form correctly returned
450 $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
451 my $forms = Form::parse($c);
457 # To create a new object, we ask the server for a form with the defaults
458 # filled in, allow the user to edit it, and send the form back.
460 # To edit an object, we must ask the server for a form representing that
461 # object, make changes requested by the user (either on the command line
462 # or interactively via $EDITOR), and send the form back.
466 my (%data, $type, @objects);
467 my ($cl, $text, $edit, $input, $output);
469 use vars qw(%set %add %del);
470 %set = %add = %del = ();
476 s/^#// if /^#\d+/; # get rid of leading hash
478 if (/^-e$/) { $edit = 1 }
479 elsif (/^-i$/) { $input = 1 }
480 elsif (/^-o$/) { $output = 1 }
482 $bad = 1, last unless defined($type = get_type_argument());
485 $bad = 1, last unless get_var_argument(\%data);
487 elsif (/^-$/ && !($slurped || $input)) {
488 chomp(my @lines = <STDIN>);
490 unless (is_object_spec($_, $type)) {
491 whine "Invalid object on STDIN: '$_'.";
501 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
502 my ($key, $op, $val) = ($1, $2, $3);
503 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
505 vpush($hash, lc $key, $val);
510 whine "No variables to set.";
515 elsif (/^(?:add|del)$/i) {
517 my $hash = ($_ eq "add") ? \%add : \%del;
519 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
520 my ($key, $val) = ($1, $2);
522 vpush($hash, lc $key, $val);
527 whine "No variables to set.";
532 elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
533 push @objects, $spc2;
535 elsif (my $spec = is_object_spec($_, $type)) {
536 push @objects, $spec;
539 my $datum = /^-/ ? "option" : "argument";
540 whine "Unrecognised $datum '$_'.";
545 if ($action =~ /^ed(?:it)?$/) {
547 whine "No objects specified.";
553 whine "You shouldn't specify objects as arguments to $action.";
557 whine "What type of object do you want to create?";
560 @objects = ("$type/new") if defined($type);
562 #return help($action, $type) if $bad;
563 return suggest_help($action, $type, $bad) if $bad;
565 # We need a form to make changes to. We usually ask the server for
566 # one, but we can avoid that if we are fed one on STDIN, or if the
567 # user doesn't want to edit the form by hand, and the command line
568 # specifies only simple variable assignments. We *should* get a
569 # form if we're creating a new ticket, so that the default values
570 # get filled in properly.
572 my @new_objects = grep /\/new$/, @objects;
578 elsif ($edit || %add || %del || !$cl || @new_objects) {
579 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
583 # If any changes were specified on the command line, apply them.
586 # We're updating forms from the server.
587 my $forms = Form::parse($text);
589 foreach my $form (@$forms) {
590 my ($c, $o, $k, $e) = @$form;
593 next if ($e || !@$o);
599 # Make changes to existing fields.
601 if (exists $add{lc $key}) {
602 $val = delete $add{lc $key};
603 vpush($k, $key, $val);
604 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
606 if (exists $del{lc $key}) {
607 $val = delete $del{lc $key};
608 my %val = map {$_=>1} @{ vsplit($val) };
609 $k->{$key} = vsplit($k->{$key});
610 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
612 if (exists $set{lc $key}) {
613 $k->{$key} = delete $set{lc $key};
617 # Then update the others.
618 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
619 foreach $key (keys %add) {
620 vpush($k, $key, $add{$key});
621 $k->{$key} = vsplit($k->{$key});
623 push @$o, (keys %add, keys %set);
626 $text = Form::compose($forms);
629 # We're rolling our own set of forms.
632 my ($type, $ids, $args) =
633 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
636 foreach my $obj (expand_list($ids)) {
637 my %set = (%set, id => "$type/$obj$args");
638 push @forms, ["", [keys %set], \%set];
641 $text = Form::compose(\@forms);
653 # We'll let the user edit the form before sending it to the server,
654 # unless we have enough information to submit it non-interactively.
655 if ($edit || (!$input && !$cl)) {
656 my $newtext = vi($text);
657 # We won't resubmit a bad form unless it was changed.
658 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
662 my $r = submit("$REST/edit", {content => $text, %data});
663 if ($r->code == 409) {
664 # If we submitted a bad form, we'll give the user a chance
665 # to correct it and resubmit.
666 if ($edit || (!$input && !$cl)) {
681 # handler for special edit commands. A valid edit command is constructed and
682 # further work is delegated to the edit handler
686 my ($id, $bad, $what);
689 $id = $1 if (m|^(?:ticket/)?($idlist)$|);
693 whine "No ticket number specified.";
696 if ($action eq 'subject') {
697 my $subject = '"'.join (" ", @ARGV).'"';
699 $what = "subject=$subject";
700 } elsif ($action eq 'give') {
701 my $owner = shift @ARGV;
702 $what = "owner=$owner";
705 if ( $action eq 'delete' or $action eq 'del' ) {
706 $what = "status=deleted";
707 } elsif ($action eq 'resolve' or $action eq 'res' ) {
708 $what = "status=resolved";
709 } elsif ($action eq 'take' ) {
710 $what = "owner=$config{user}";
711 } elsif ($action eq 'untake') {
712 $what = "owner=Nobody";
717 whine "Extraneous arguments for action $action: @ARGV.";
721 whine "unrecognized action $action.";
723 return help("edit", undef, $bad) if $bad;
724 @ARGV = ( $id, "set", $what );
725 print "Executing: rt edit @ARGV\n";
729 # We roll "comment" and "correspond" into the same handler.
733 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
742 elsif (/^-[abcmw]$/) {
744 whine "No argument specified with $_.";
749 unless (-f $ARGV[0] && -r $ARGV[0]) {
750 whine "Cannot read attachment: '$ARGV[0]'.";
753 push @files, shift @ARGV;
756 my $a = $_ eq "-b" ? \@bcc : \@cc;
757 @$a = split /\s*,\s*/, shift @ARGV;
761 if ( $msg =~ /^-$/ ) {
763 while (<STDIN>) { $msg .= $_ }
767 elsif (/-w/) { $wtime = shift @ARGV }
769 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
773 my $datum = /^-/ ? "option" : "argument";
774 whine "Unrecognised $datum '$_'.";
780 whine "No object specified.";
783 #return help($action, "ticket") if $bad;
784 return suggest_help($action, "ticket") if $bad;
788 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
794 Attachment => [ @files ],
795 TimeWorked => $wtime || '',
801 my $text = Form::compose([ $form ]);
803 if ($edit || !$msg) {
808 my $ntext = vi($text);
809 return if ($error && $ntext eq $text);
811 $form = Form::parse($text);
814 ($c, $o, $k, $e) = @{ $form->[0] };
817 $c = "# Syntax error.";
823 @files = @{ vsplit($k->{Attachment}) };
826 $text = Form::compose([[$c, $o, $k, $e]]);
831 foreach my $file (@files) {
832 $data{"attachment_$i"} = bless([ $file ], "Attachment");
835 $data{content} = $text;
837 my $r = submit("$REST/ticket/$id/comment", \%data);
842 # Merge one ticket into another.
850 s/^#// if /^#\d+/; # get rid of leading hash
856 whine "Unrecognised argument: '$_'.";
862 my $evil = @id > 2 ? "many" : "few";
863 whine "Too $evil arguments specified.";
866 #return help("merge", "ticket") if $bad;
867 return suggest_help("merge", "ticket", $bad) if $bad;
869 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
874 # Link one ticket to another.
877 my ($bad, $del, %data) = (0, 0, ());
880 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
881 ReferredToBy HasMember MemberOf);
883 while (@ARGV && $ARGV[0] =~ /^-/) {
890 $bad = 1, last unless defined($type = get_type_argument());
893 whine "Unrecognised option: '$_'.";
898 $type = "ticket" unless $type; # default type to tickets
901 my ($from, $rel, $to) = @ARGV;
902 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
903 my $bad = $from =~ /^\d+$/ ? $to : $from;
904 whine "Invalid $type ID '$bad' specified.";
907 if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
908 whine "Invalid link '$rel' for type $type specified.";
911 %data = (id => $from, rel => $rel, to => $to, del => $del);
914 my $bad = @ARGV < 3 ? "few" : "many";
915 whine "Too $bad arguments specified.";
918 return suggest_help("link", $type, $bad) if $bad;
920 my $r = submit("$REST/$type/link", \%data);
925 # Take/steal a ticket
928 my ($bad, %data) = (0, ());
935 unless ($id =~ /^\d+$/) {
936 whine "Invalid ticket ID $id specified.";
941 [ "Ticket", "Action" ],
949 my $text = Form::compose([ $form ]);
950 $data{content} = $text;
953 $bad = @ARGV < 1 ? "few" : "many";
954 whine "Too $bad arguments specified.";
957 return suggest_help("take", "ticket", $bad) if $bad;
959 my $r = submit("$REST/ticket/$id/take", \%data);
964 # Grant/revoke a user's rights.
973 $revoke = 1 if $cmd->{action} eq 'revoke';
977 # Client <-> Server communication.
978 # --------------------------------
980 # This function composes and sends an HTTP request to the RT server, and
981 # interprets the response. It takes a request URI, and optional request
982 # data (a string, or a reference to a set of key-value pairs).
985 my ($uri, $content) = @_;
987 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
988 my $h = HTTP::Headers->new;
990 # Did the caller specify any data to send with the request?
992 if (defined $content) {
993 unless (ref $content) {
994 # If it's just a string, make sure LWP handles it properly.
995 # (By pretending that it's a file!)
996 $content = [ content => [undef, "", Content => $content] ];
998 elsif (ref $content eq 'HASH') {
1000 foreach my $k (keys %$content) {
1001 if (ref $content->{$k} eq 'ARRAY') {
1002 foreach my $v (@{ $content->{$k} }) {
1006 else { push @data, $k, $content->{$k} }
1013 # Should we send authentication information to start a new session?
1014 my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
1015 (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
1016 if ($config{externalauth}) {
1017 $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
1018 print " Password will be sent to $server $how\n",
1019 " Press CTRL-C now if you do not want to continue\n"
1020 if ! $config{passwd};
1021 } elsif ( $no_strong_auth ) {
1022 if (!defined $session->cookie) {
1023 print " Strong encryption not available, $no_strong_auth\n",
1024 " Password will be sent to $server $how\n",
1025 " Press CTRL-C now if you do not want to continue\n"
1026 if ! $config{passwd};
1027 push @$data, ( user => $config{user} );
1028 push @$data, ( pass => $config{passwd} || read_passwd() );
1032 # Now, we construct the request.
1034 $req = POST($uri, $data, Content_Type => 'form-data');
1039 $session->add_cookie_header($req);
1040 if ($config{externalauth}) {
1044 # Then we send the request and parse the response.
1045 DEBUG(3, $req->as_string);
1046 my $res = $ua->request($req);
1047 DEBUG(3, $res->as_string);
1049 if ($res->is_success) {
1050 # The content of the response we get from the RT server consists
1051 # of an HTTP-like status line followed by optional header lines,
1052 # a blank line, and arbitrary text.
1054 my ($head, $text) = split /\n\n/, $res->content, 2;
1055 my ($status, @headers) = split /\n/, $head;
1056 $text =~ s/\n*$/\n/ if ($text);
1058 # "RT/3.0.1 401 Credentials required"
1059 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
1060 warn "rt: Malformed RT response from $config{server}.\n";
1061 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
1065 # Our caller can pretend that the server returned a custom HTTP
1066 # response code and message. (Doing that directly is apparently
1067 # not sufficiently portable and uncomplicated.)
1070 $res->content($text);
1071 $session->update($res) if ($res->is_success || $res->code != 401);
1073 if (!$res->is_success) {
1074 # We can deal with authentication failures ourselves. Either
1075 # we sent invalid credentials, or our session has expired.
1076 if ($res->code == 401) {
1078 if (exists $d{user}) {
1079 warn "rt: Incorrect username or password.\n";
1082 elsif ($req->header("Cookie")) {
1083 # We'll retry the request with credentials, unless
1084 # we only wanted to logout in the first place.
1086 return submit(@_) unless $uri eq "$REST/logout";
1089 # Conflicts should be dealt with by the handler and user.
1090 # For anything else, we just die.
1091 elsif ($res->code != 409) {
1092 warn "rt: ", $res->content;
1098 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
1105 # Session management.
1106 # -------------------
1108 # Maintains a list of active sessions in the ~/.rt_sessions file.
1113 # Initialises the session cache.
1115 my ($class, $file) = @_;
1117 file => $file || "$HOME/.rt_sessions",
1121 # The current session is identified by the currently configured
1123 ($s, $u) = @config{"server", "user"};
1125 bless $self, $class;
1131 # Returns the current session cookie.
1134 my $cookie = $self->{sids}{$s}{$u};
1135 return defined $cookie ? "RT_SID_$cookie" : undef;
1138 # Deletes the current session cookie.
1141 delete $self->{sids}{$s}{$u};
1144 # Adds a Cookie header to an outgoing HTTP request.
1145 sub add_cookie_header {
1146 my ($self, $request) = @_;
1147 my $cookie = $self->cookie();
1149 $request->header(Cookie => $cookie) if defined $cookie;
1152 # Extracts the Set-Cookie header from an HTTP response, and updates
1153 # session information accordingly.
1155 my ($self, $response) = @_;
1156 my $cookie = $response->header("Set-Cookie");
1158 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
1159 $self->{sids}{$s}{$u} = $1;
1163 # Loads the session cache from the specified file.
1165 my ($self, $file) = @_;
1166 $file ||= $self->{file};
1169 open(F, $file) && do {
1170 $self->{file} = $file;
1171 my $sids = $self->{sids} = {};
1174 next if /^$/ || /^#/;
1175 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
1176 my ($server, $user, $cookie) = split / /, $_;
1177 $sids->{$server}{$user} = $cookie;
1184 # Writes the current session cache to the specified file.
1186 my ($self, $file) = shift;
1187 $file ||= $self->{file};
1190 open(F, ">$file") && do {
1191 my $sids = $self->{sids};
1192 foreach my $server (keys %$sids) {
1193 foreach my $user (keys %{ $sids->{$server} }) {
1194 my $sid = $sids->{$server}{$user};
1196 print F "$server $user $sid\n";
1216 # Forms are RFC822-style sets of (field, value) specifications with some
1217 # initial comments and interspersed blank lines allowed for convenience.
1218 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1220 # Each form is parsed into an array with four elements: commented text
1221 # at the start of the form, an array with the order of keys, a hash with
1222 # key/value pairs, and optional error text if the form syntax was wrong.
1224 # Returns a reference to an array of parsed forms.
1228 my @lines = split /\n/, $_[0] if $_[0];
1229 my ($c, $o, $k, $e) = ("", [], {}, "");
1233 my $line = shift @lines;
1235 next LINE if $line eq '';
1237 if ($line eq '--') {
1238 # We reached the end of one form. We'll ignore it if it was
1239 # empty, and store it otherwise, errors and all.
1240 if ($e || $c || @$o) {
1241 push @forms, [ $c, $o, $k, $e ];
1242 $c = ""; $o = []; $k = {}; $e = "";
1246 elsif ($state != -1) {
1247 if ($state == 0 && $line =~ /^#/) {
1248 # Read an optional block of comments (only) at the start
1252 while (@lines && $lines[0] =~ /^#/) {
1253 $c .= "\n".shift @lines;
1257 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1258 # Read a field: value specification.
1262 # Read continuation lines, if any.
1263 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1264 push @v, shift @lines;
1266 pop @v while (@v && $v[-1] eq '');
1268 # Strip longest common leading indent from text.
1270 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1271 $ws = $ls if (!$ws || length($ls) < length($ws));
1273 s/^$ws// foreach @v;
1275 push(@$o, $f) unless exists $k->{$f};
1276 vpush($k, $f, join("\n", @v));
1280 elsif ($line !~ /^#/) {
1281 # We've found a syntax error, so we'll reconstruct the
1282 # form parsed thus far, and add an error marker. (>>)
1284 $e = Form::compose([[ "", $o, $k, "" ]]);
1285 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1289 # We saw a syntax error earlier, so we'll accumulate the
1290 # contents of this form until the end.
1294 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1296 foreach my $l (keys %$k) {
1297 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1303 # Returns text representing a set of forms.
1308 foreach my $form (@$forms) {
1309 my ($c, $o, $k, $e) = @$form;
1322 foreach my $key (@$o) {
1325 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1327 $sp = " "x(length("$key: "));
1328 $sp = " "x4 if length($sp) > 16;
1330 foreach $v (@values) {
1336 push @lines, "$line\n\n";
1339 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1342 push @lines, "$key: $v\n\n";
1345 length($line)+length($v)-rindex($line, "\n") >= 70)
1347 $line .= ",\n$sp$v";
1350 $line = $line ? "$line, $v" : "$key: $v";
1354 $line = "$key:" unless @values;
1356 if ($line =~ /\n/) {
1357 if (@lines && $lines[-1] !~ /\n\n$/) {
1362 push @lines, "$line\n";
1366 $text .= join "", @lines;
1374 return join "\n--\n\n", @text;
1380 # Returns configuration information from the environment.
1381 sub config_from_env {
1384 foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
1386 if (exists $ENV{"RT$k"}) {
1387 $env{lc $k} = $ENV{"RT$k"};
1394 # Finds a suitable configuration file and returns information from it.
1395 sub config_from_file {
1399 # We'll use an absolute path if we were given one.
1400 return parse_config_file($rc);
1403 # Otherwise we'll use the first file we can find in the current
1404 # directory, or in one of its (increasingly distant) ancestors.
1406 my @dirs = split /\//, cwd;
1408 my $file = join('/', @dirs, $rc);
1410 return parse_config_file($file);
1413 # Remove the last directory component each time.
1417 # Still nothing? We'll fall back to some likely defaults.
1418 for ("$HOME/$rc", "/etc/rt.conf") {
1419 return parse_config_file($_) if (-r $_);
1426 # Makes a hash of the specified configuration file.
1427 sub parse_config_file {
1430 local $_; # $_ may be aliased to a constant, from line 1163
1432 open(CFG, $file) && do {
1435 next if (/^#/ || /^\s*$/);
1437 if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
1441 die "rt: $file:$.: unknown configuration directive.\n";
1453 my $sub = (caller(1))[3];
1454 $sub =~ s/^main:://;
1455 warn "rt: $sub: @_\n";
1460 eval 'require Term::ReadKey';
1462 die "No password specified (and Term::ReadKey not installed).\n";
1466 Term::ReadKey::ReadMode('noecho');
1467 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1468 Term::ReadKey::ReadMode('restore');
1476 my $file = "/tmp/rt.form.$$";
1477 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1482 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1483 system($editor, $file) && die "Couldn't run $editor.\n";
1484 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1490 # Add a value to a (possibly multi-valued) hash key.
1492 my ($hash, $key, $val) = @_;
1493 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1495 if (exists $hash->{$key}) {
1496 unless (ref $hash->{$key} eq 'ARRAY') {
1497 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1498 $hash->{$key} = \@v;
1500 push @{ $hash->{$key} }, @val;
1503 $hash->{$key} = $val;
1507 # "Normalise" a hash key that's known to be multi-valued.
1511 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1513 foreach my $line (map {split /\n/} @values) {
1514 # XXX: This should become a real parser, Ã la Text::ParseWords.
1517 my ( $a, $b ) = split /,/, $line, 2;
1520 no warnings 'uninitialized';
1523 while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/
1524 && $a =~ /(\\)+'$/ )) {
1525 ( $a, $b ) = split /,/, $b, 2;
1530 elsif ( $a =~ /^q{/ ) {
1532 while ( $a !~ /}$/ ) {
1544 ( $a, $b ) = split /,/, $b, 2;
1553 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1554 # change both functions at once
1559 foreach (split /,/, $list) {
1560 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1563 return map $_->[0], # schwartzian transform
1565 defined $a->[1] && defined $b->[1]?
1568 :!defined $a->[1] && !defined $b->[1]?
1571 # mix, number must be first
1572 :defined $a->[1]? -1: 1
1574 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1578 sub get_type_argument {
1582 $type = shift @ARGV;
1583 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1584 # We want whine to mention our caller, not us.
1585 @_ = ("Invalid type '$type' specified.");
1590 @_ = ("No type argument specified with -t.");
1594 $type =~ s/s$//; # "Plural". Ugh.
1598 sub get_var_argument {
1602 my $kv = shift @ARGV;
1603 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1604 push @{ $data->{$k} }, $v;
1607 @_ = ("Invalid variable specification: '$kv'.");
1612 @_ = ("No variable argument specified with -S.");
1617 sub is_object_spec {
1618 my ($spec, $type) = @_;
1620 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1621 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1626 my ($action, $type, $rv) = @_;
1628 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1629 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1634 # simplified procedure for parsing date, avoid loading Date::Parse
1635 my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
1636 Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
1638 my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
1639 if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
1640 ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
1641 $mon = $month{$monstr} if exists $month{$monstr};
1642 } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
1643 ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
1645 if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
1646 return timelocal($sec,$min,$hr,$day,$mon,$yr);
1648 print "Unknown date format in parsedate: $_\n";
1654 my ($old, $new) = @_;
1655 $new = time() if ! $new;
1656 $old = str2time($old) if $old !~ /^\d+$/;
1657 $new = str2time($new) if $new !~ /^\d+$/;
1658 return "???" if ! $old or ! $new;
1660 my %seconds = (min => 60,
1665 yr => 60*60*24*365);
1667 my $diff = $new - $old;
1669 my $howmuch = $diff;
1670 for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
1671 last if $diff < $seconds{$_};
1673 $howmuch = int($diff/$seconds{$_});
1675 return "$howmuch $what";
1680 my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
1682 # dates are in local time zone
1684 print "Date: $k->{Created}\n";
1685 print "From: $k->{Requestors}\n";
1686 print "Cc: $k->{Cc}\n" if $k->{Cc};
1687 print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
1688 print "X-Queue: $k->{Queue}\n";
1689 print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
1691 # dates in these attributes are in GMT and will be converted
1692 foreach my $form (@$forms) {
1693 my ($c, $o, $k, $e) = @$form;
1694 next if ! $k->{id} or exists $k->{Queue};
1695 if ( exists $k->{Created} ) {
1696 my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
1698 my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
1699 if ( exists $k->{Description} ) {
1700 print "===> $k->{Description} on $created\n";
1703 print "$k->{Content}\n" if exists $k->{Content} and
1704 $k->{Content} !~ /to have no content$/ and
1705 $k->{Type} ne 'EmailRecord';
1706 print "$k->{Attachments}\n" if exists $k->{Attachments} and
1713 my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
1714 $heading .= '-' x 80 . "\n";
1716 foreach my $form (@$forms) {
1717 my ($c, $o, $k, $e) = @$form;
1719 print $heading if $heading;
1722 $id =~ s!^ticket/!!;
1723 my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
1724 $owner = substr($owner, 0, 5);
1725 my $queue = substr($k->{Queue}, 0, 5);
1726 my $subject = substr($k->{Subject}, 0, 30);
1727 my $age = date_diff($k->{Created});
1728 my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
1729 my $status = substr($k->{Status}, 0, 6);
1730 my $requestor = substr($k->{Requestors}, 0, 9);
1731 my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
1732 $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
1733 if ( $k->{Owner} eq 'Nobody' ) {
1735 } elsif ($k->{Owner} eq $config{user} ) {
1741 print "No matches found\n" if $heading;
1742 printf "========== my %2d open tickets ==========\n", scalar @me if @me;
1744 printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
1745 print @open if @open;
1754 This is a command-line interface to RT 3.0 or newer.
1756 It allows you to interact with an RT server over HTTP, and offers an
1757 interface to RT's functionality that is better-suited to automation
1758 and integration with other tools.
1760 In general, each invocation of this program should specify an action
1761 to perform on one or more objects, and any other arguments required
1762 to complete the desired action.
1764 For more information:
1766 - rt help usage (syntax information)
1767 - rt help objects (how to specify objects)
1768 - rt help actions (a list of possible actions)
1769 - rt help types (a list of object types)
1771 - rt help config (configuration details)
1772 - rt help examples (a few useful examples)
1773 - rt help topics (a list of help topics)
1783 rt <action> [options] [arguments]
1787 Each invocation of this program must specify an action (e.g. "edit",
1788 "create"), options to modify behaviour, and other arguments required
1789 by the specified action. (For example, most actions expect a list of
1790 numeric object IDs to act upon.)
1792 The details of the syntax and arguments for each action are given by
1793 "rt help <action>". Some actions may be referred to by more than one
1794 name ("create" is the same as "new", for example).
1796 You may also call "rt shell", which will give you an 'rt>' prompt at
1797 which you can issue commands of the form "<action> [options]
1798 [arguments]". See "rt help shell" for details.
1800 Objects are identified by a type and an ID (which can be a name or a
1801 number, depending on the type). For some actions, the object type is
1802 implied (you can only comment on tickets); for others, the user must
1803 specify it explicitly. See "rt help objects" for details.
1805 In syntax descriptions, mandatory arguments that must be replaced by
1806 appropriate value are enclosed in <>, and optional arguments are
1807 indicated by [] (for example, <action> and [options] above).
1809 For more information:
1811 - rt help objects (how to specify objects)
1812 - rt help actions (a list of actions)
1813 - rt help types (a list of object types)
1814 - rt help shell (how to use the shell)
1820 Title: configuration
1823 This program has two major sources of configuration information: its
1824 configuration files, and the environment.
1826 The program looks for configuration directives in a file named .rtrc
1827 (or $RTCONFIG; see below) in the current directory, and then in more
1828 distant ancestors, until it reaches /. If no suitable configuration
1829 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1831 Configuration directives:
1833 The following directives may occur, one per line:
1835 - server <URL> URL to RT server.
1836 - user <username> RT username.
1837 - passwd <passwd> RT user's password.
1838 - query <RT Query> Default RT Query for list action
1839 - orderby <order> Default RT order for list action
1840 - queue <queuename> Default RT Queue for list action
1841 - externalauth <0|1> Use HTTP Basic authentication
1842 explicitely setting externalauth to 0 inhibits also GSSAPI based
1843 authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
1845 Blank and #-commented lines are ignored.
1847 Sample configuration file contents:
1849 server https://rt.somewhere.com/
1850 # more than one queue can be given (by adding a query expression)
1851 queue helpdesk or queue=support
1852 query Status != resolved and Owner=myaccount
1855 Environment variables:
1857 The following environment variables override any corresponding
1858 values defined in configuration files:
1864 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1865 - RTCONFIG Specifies a name other than ".rtrc" for the
1867 - RTQUERY Default RT Query for rt list
1868 - RTORDERBY Default order for rt list
1877 <type>/<id>[/<attributes>]
1879 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1880 ID. Some types of objects can also be identified by name (like users
1881 and queues). Furthermore, objects may have named attributes (such as
1882 "ticket/1/history").
1884 An object specification is like a path in a virtual filesystem, with
1885 object types as top-level directories, object IDs as subdirectories,
1886 and named attributes as further subdirectories.
1888 A comma-separated list of names, numeric IDs, or numeric ranges can
1889 be used to specify more than one object of the same type. Note that
1890 the list must be a single argument (i.e., no spaces). For example,
1891 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1892 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1894 If just a number is given as object specification it will be
1895 interpreted as ticket/<number>
1899 1 # the same as ticket/1
1901 ticket/1/attachments
1902 ticket/1/attachments/3
1903 ticket/1/attachments/3/content
1905 ticket/1-3,5-7/history
1909 user/ams,rai,1/rights
1911 For more information:
1913 - rt help <action> (action-specific details)
1914 - rt help <type> (type-specific details)
1922 You can currently perform the following actions on all objects:
1924 - list (list objects matching some condition)
1925 - show (display object details)
1926 - edit (edit object details)
1927 - create (create a new object)
1929 Each type may define actions specific to itself; these are listed in
1930 the help item about that type.
1932 For more information:
1934 - rt help <action> (action-specific details)
1935 - rt help types (a list of possible types)
1937 The following actions on tickets are also possible:
1939 - comment Add comments to a ticket
1940 - correspond Add comments to a ticket
1941 - merge Merge one ticket into another
1942 - link Link one ticket to another
1943 - take Take a ticket (steal and untake are possible as well)
1945 For several edit set subcommands that are frequently used abbreviations
1946 have been introduced. These abbreviations are:
1948 - delete or del delete a ticket (edit set status=deleted)
1949 - resolve or res resolve a ticket (edit set status=resolved)
1950 - subject change subject of ticket (edit set subject=string)
1951 - give give a ticket to somebody (edit set owner=user)
1958 You can currently operate on the following types of objects:
1965 For more information:
1967 - rt help <type> (type-specific details)
1968 - rt help objects (how to specify objects)
1969 - rt help actions (a list of possible actions)
1976 Tickets are identified by a numeric ID.
1978 The following generic operations may be performed upon tickets:
1985 In addition, the following ticket-specific actions exist:
2001 The following attributes can be used with "rt show" or "rt edit"
2002 to retrieve or edit other information associated with tickets:
2004 links A ticket's relationships with others.
2005 history All of a ticket's transactions.
2006 history/type/<type> Only a particular type of transaction.
2007 history/id/<id> Only the transaction of the specified id.
2008 attachments A list of attachments.
2009 attachments/<id> The metadata for an individual attachment.
2010 attachments/<id>/content The content of an individual attachment.
2018 Users and groups are identified by name or numeric ID.
2020 The following generic operations may be performed upon them:
2027 In addition, the following type-specific actions exist:
2034 The following attributes can be used with "rt show" or "rt edit"
2035 to retrieve or edit other information associated with users and
2038 rights Global rights granted to this user.
2039 rights/<queue> Queue rights for this user.
2046 Queues are identified by name or numeric ID.
2048 Currently, they can be subjected to the following actions:
2061 rt subject <id> <new subject text>
2063 Change the subject of a ticket whose ticket id is given.
2072 rt give <id> <accountname>
2074 Give a ticket whose ticket id is given to another user.
2083 Steal a ticket whose ticket id is given, i.e. set the owner to myself.
2094 Take a ticket whose ticket id is given, i.e. set the owner to myself.
2105 Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
2117 Resolves a ticket whose ticket id is given.
2129 Deletes a ticket whose ticket id is given.
2140 Terminates the currently established login session. You will need to
2141 provide authentication credentials before you can continue using the
2142 server. (See "rt help config" for details about authentication.)
2153 rt <ls|list|search> [options] "query string"
2155 Displays a list of objects matching the specified conditions.
2156 ("ls", "list", and "search" are synonyms.)
2158 Conditions are expressed in the SQL-like syntax used internally by
2159 RT3. (For more information, see "rt help query".) The query string
2160 must be supplied as one argument.
2162 (Right now, the server doesn't support listing anything but tickets.
2163 Other types will be supported in future; this client will be able to
2164 take advantage of that support without any changes.)
2168 The following options control how much information is displayed
2169 about each matching object:
2171 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
2172 -s Short description.
2173 -l Longer description.
2174 -f <field[s] Display only the fields listed and the ticket id
2178 -o +/-<field> Orders the returned list by the specified field.
2179 -r reversed order (useful if a default was given)
2180 -q queue[s] restricts the query to the queue[s] given
2181 multiple queues are separated by comma
2182 -S var=val Submits the specified variable with the request.
2183 -t type Specifies the type of object to look for. (The
2184 default is "ticket".)
2188 rt ls "Priority > 5 and Status=new"
2189 rt ls -o +Subject "Priority > 5 and Status=new"
2190 rt ls -o -Created "Priority > 5 and Status=new"
2191 rt ls -i "Priority > 5"|rt edit - set status=resolved
2192 rt ls -t ticket "Subject like '[PATCH]%'"
2194 rt ls -f owner,subject
2203 rt show [options] <object-ids>
2205 Displays details of the specified objects.
2207 For some types, object information is further classified into named
2208 attributes (for example, "1-3/links" is a valid ticket specification
2209 that refers to the links for tickets 1-3). Consult "rt help <type>"
2210 and "rt help objects" for further details.
2212 If only a number is given it will be interpreted as the objects
2213 ticket/number and ticket/number/history
2215 This command writes a set of forms representing the requested object
2220 The following options control how much information is displayed
2221 about each matching object:
2223 Without any formatting options prettyprinted output is generated.
2224 Giving any of the two options below reverts to raw output.
2225 -s Short description (history and attachments only).
2226 -l Longer description (history and attachments only).
2229 - Read IDs from STDIN instead of the command-line.
2230 -t type Specifies object type.
2231 -f a,b,c Restrict the display to the specified fields.
2232 -S var=val Submits the specified variable with the request.
2236 rt show -t ticket -f id,subject,status 1-3
2237 rt show ticket/3/attachments/29
2238 rt show ticket/3/attachments/29/content
2239 rt show ticket/1-3/links
2240 rt show ticket/3/history
2241 rt show -l ticket/3/history
2254 rt edit [options] <object-ids> set field=value [field=value] ...
2255 add field=value [field=value] ...
2256 del field=value [field=value] ...
2258 Edits information corresponding to the specified objects.
2260 A purely numeric object id nnn is translated into ticket/nnn
2262 If, instead of "edit", an action of "new" or "create" is specified,
2263 then a new object is created. In this case, no numeric object IDs
2264 may be specified, but the syntax and behaviour remain otherwise
2267 This command typically starts an editor to allow you to edit object
2268 data in a form for submission. If you specified enough information
2269 on the command-line, however, it will make the submission directly.
2271 The command line may specify field-values in three different ways.
2272 "set" sets the named field to the given value, "add" adds a value
2273 to a multi-valued field, and "del" deletes the corresponding value.
2274 Each "field=value" specification must be given as a single argument.
2276 For some types, object information is further classified into named
2277 attributes (for example, "1-3/links" is a valid ticket specification
2278 that refers to the links for tickets 1-3). These attributes may also
2279 be edited. Consult "rt help <type>" and "rt help object" for further
2284 - Read numeric IDs from STDIN instead of the command-line.
2285 (Useful with rt ls ... | rt edit -; see examples below.)
2286 -i Read a completed form from STDIN before submitting.
2287 -o Dump the completed form to STDOUT instead of submitting.
2288 -e Allows you to edit the form even if the command-line has
2289 enough information to make a submission directly.
2291 Submits the specified variable with the request.
2292 -t type Specifies object type.
2296 # Interactive (starts $EDITOR with a form).
2301 rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
2302 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
2303 rt edit ticket/4 set priority=3 owner=bar@example.com \
2304 add cc=foo@example.com bcc=quux@example.net
2305 rt create -t ticket set subject='new ticket' priority=10 \
2306 add cc=foo@example.com
2316 rt <comment|correspond> [options] <ticket-id>
2318 Adds a comment (or correspondence) to the specified ticket (the only
2319 difference being that comments aren't sent to the requestors.)
2321 This command will typically start an editor and allow you to type a
2322 comment into a form. If, however, you specified all the necessary
2323 information on the command line, it submits the comment directly.
2325 (See "rt help forms" for more information about forms.)
2329 -m <text> Specify comment text.
2330 -a <file> Attach a file to the comment. (May be used more
2331 than once to attach multiple files.)
2332 -c <addrs> A comma-separated list of Cc addresses.
2333 -b <addrs> A comma-separated list of Bcc addresses.
2334 -w <time> Specify the time spent working on this ticket.
2335 -e Starts an editor before the submission, even if
2336 arguments from the command line were sufficient.
2340 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
2349 rt merge <from-id> <to-id>
2351 Merges the first ticket specified into the second ticket specified.
2360 rt link [-d] <id-A> <link> <id-B>
2362 Creates (or, with -d, deletes) a link between the specified tickets.
2363 The link can (irrespective of case) be any of:
2365 DependsOn/DependedOnBy: A depends upon B (or vice versa).
2366 RefersTo/ReferredToBy: A refers to B (or vice versa).
2367 MemberOf/HasMember: A is a member of B (or vice versa).
2369 To view a ticket's links, use "rt show ticket/3/links". (See
2370 "rt help ticket" and "rt help show".)
2374 -d Deletes the specified link.
2378 rt link 2 dependson 3
2379 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
2392 RT3 uses an SQL-like syntax to specify object selection constraints.
2393 See the <RT:...> documentation for details.
2395 (XXX: I'm going to have to write it, aren't I?)
2397 Until it exists here a short description of important constructs:
2399 The two simple forms of query expressions are the constructs
2400 Attribute like Value and
2401 Attribute = Value or Attribute != Value
2403 Whether attributes can be matched using like or using = is built into RT.
2404 The attributes id, Queue, Owner Priority and Status require the = or !=
2407 If Value is a string it must be quoted and may contain the wildcard
2408 character %. If the string does not contain white space, the quoting
2409 may however be omitted, it will be added automatically when parsing
2412 Simple query expressions can be combined using and, or and parentheses
2413 can be used to group expressions.
2415 As a special case a standalone string (which would not form a correct
2416 query) is transformed into (Owner='string' or Requestor like 'string%')
2417 and added to the default query, i.e. the query is narrowed down.
2419 If no Queue=name clause is contained in the query, a default clause
2420 Queue=$config{queue} is added.
2423 Status!='resolved' and Status!='rejected'
2424 (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
2432 This program uses RFC822 header-style forms to represent object data
2433 in a form that's suitable for processing both by humans and scripts.
2435 A form is a set of (field, value) specifications, with some initial
2436 commented text and interspersed blank lines allowed for convenience.
2437 Field names may appear more than once in a form; a comma-separated
2438 list of multiple field values may also be specified directly.
2440 Field values can be wrapped as in RFC822, with leading whitespace.
2441 The longest sequence of leading whitespace common to all the lines
2442 is removed (preserving further indentation). There is no limit on
2443 the length of a value.
2445 Multiple forms are separated by a line containing only "--\n".
2447 (XXX: A more detailed specification will be provided soon. For now,
2448 the server-side syntax checking will suffice.)
2459 Get help on any of the following subjects:
2461 - tickets, users, groups, queues.
2462 - show, edit, ls/list/search, new/create.
2464 - query (search query syntax)
2465 - forms (form specification)
2467 - objects (how to specify objects)
2468 - types (a list of object types)
2469 - actions/commands (a list of actions)
2470 - usage/syntax (syntax details)
2471 - conf/config/configuration (configuration details)
2472 - examples (a few useful examples)
2480 some useful examples
2482 All the following list requests will be restricted to the default queue.
2483 That can be changed by adding the option -q queuename
2485 List all tickets that are not rejected/resolved
2487 List all tickets that are new and do not have an owner
2488 rt ls "status=new and owner=nobody"
2489 List all tickets which I have sent or of which I am the owner
2491 List all attributes for the ticket 6977 (ls -l instead of ls)
2493 Show the content of ticket 6977
2495 Show all attributes in the ticket and in the history of the ticket
2497 Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
2499 This will open an editor and lets you add text (attribute Text:)
2500 Other attributes may be changed as well, but usually don't do that.
2501 Correspond a ticket (like comment, but mail is also sent to requestors)
2503 Edit a ticket (generic change, interactive using the editor)
2505 Change the owner of a ticket non interactively
2506 rt edit 6977 set owner=myaccount
2508 rt give 6977 account
2511 Change the status of a ticket
2512 rt edit 6977 set status=resolved
2515 Change the status of all tickets I own to resolved !!!
2516 rt ls -i owner=myaccount | rt edit - set status=resolved
2527 Opens an interactive shell, at which you can issue commands of
2528 the form "<action> [options] [arguments]".
2530 To exit the shell, type "quit" or "exit".
2532 Commands can be given at the shell in the same form as they would
2533 be given at the command line without the leading 'rt' invocation.
2537 rt> create -t ticket set subject='new' add cc=foo@example.com
2551 rt <take|untake|steal> <ticket-id>
2553 Sets the owner of the specified ticket to the current user,
2554 assuming said user has the bits to do so, or releases the
2557 'Take' is used on tickets which are not currently owned
2558 (Owner: Nobody), 'steal' is used on tickets which *are*
2559 currently owned, and 'untake' is used to "release" a ticket
2560 (reset its Owner to Nobody). 'Take' cannot be used on
2561 tickets which are currently owned.
2564 alice$ rt create -t ticket set subject="New ticket"
2567 # Owner changed from Nobody to alice
2570 # Owner changed from alice to bob
2572 # Owner changed from bob to Nobody
2580 Use "quit" or "exit" to leave the shell. Only valid within shell