2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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 # We derive configuration information from hardwired defaults, dotfiles,
64 # and the RT* environment variables (in increasing order of precedence).
65 # Session information is stored in ~/.rt_sessions.
68 my $HOME = eval{(getpwuid($<))[7]}
69 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
74 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
76 server => 'http://localhost/',
80 config_from_file($ENV{RTCONFIG} || ".rtrc"),
83 my $session = new Session("$HOME/.rt_sessions");
84 my $REST = "$config{server}/REST/1.0";
89 sub DEBUG { warn @_ if $config{debug} >= shift }
91 # These regexes are used by command handlers to parse arguments.
92 # (XXX: Ask Autrijus how i18n changes these definitions.)
95 my $field = '(?:[a-zA-Z](?:[a-zA-Z0-9_-]|\s+)*)';
96 my $label = '[a-zA-Z0-9@_.+-]+';
97 my $labels = "(?:$label,)*$label";
98 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
100 # Our command line looks like this:
102 # rt <action> [options] [arguments]
104 # We'll parse just enough of it to decide upon an action to perform, and
105 # leave the rest to per-action handlers to interpret appropriately.
108 # handler => [ ...aliases... ],
109 version => ["version", "ver"],
111 logout => ["logout"],
112 help => ["help", "man"],
113 show => ["show", "cat"],
114 edit => ["create", "edit", "new", "ed"],
115 list => ["search", "list", "ls"],
116 comment => ["comment", "correspond"],
117 link => ["link", "ln"],
119 grant => ["grant", "revoke"],
120 take => ["take", "steal", "untake"],
121 quit => ["quit", "exit"],
125 foreach my $fn (keys %handlers) {
126 foreach my $alias (@{ $handlers{$fn} }) {
127 $actions{$alias} = \&{"$fn"};
131 # Once we find and call an appropriate handler, we're done.
136 push @ARGV, 'shell' if (!@ARGV); # default to shell mode
137 shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
138 if (@ARGV && exists $actions{$ARGV[0]}) {
139 $action = shift @ARGV;
140 $actions{$action}->($action);
143 print STDERR "rt: Unknown command '@ARGV'.\n";
144 print STDERR "rt: For help, run 'rt help'.\n";
154 # The following subs are handlers for each entry in %actions.
158 my $term = new Term::ReadLine 'RT CLI';
159 while ( defined ($_ = $term->readline($prompt)) ) {
160 next if /^#/ || /^\s*$/;
162 @ARGV = shellwords($_);
168 print "rt $VERSION\n";
172 submit("$REST/logout") if defined $session->cookie;
182 my ($action, $type) = @_;
185 # What help topics do we know about?
188 foreach my $item (@{ Form::parse(<DATA>) }) {
189 my $title = $item->[2]{Title};
190 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
192 foreach $title (grep $_, @titles) {
193 $help{$title} = $item->[2]{Text};
198 # What does the user want help with?
199 undef $action if ($action && $actions{$action} eq \&help);
200 unless ($action || $type) {
201 # If we don't know, we'll look for clues in @ARGV.
203 if (exists $help{$_}) { $key = $_; last; }
206 # Tolerate possibly plural words.
208 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
213 if ($type && $action) {
214 $key = "$type.$action";
216 $key ||= $type || $action || "introduction";
218 # Find a suitable topic to display.
219 while (!exists $help{$key}) {
220 if ($type && $action) {
221 if ($key eq "$type.$action") { $key = $action; }
222 elsif ($key eq $action) { $key = $type; }
223 else { $key = "introduction"; }
226 $key = "introduction";
230 print STDERR $help{$key}, "\n\n";
233 # Displays a list of objects that match some specified condition.
236 my ($q, $type, %data);
237 my $orderby = $config{orderby};
239 if ($config{orderby}) {
240 $data{orderby} = $config{orderby};
248 $bad = 1, last unless defined($type = get_type_argument());
251 $bad = 1, last unless get_var_argument(\%data);
254 $data{'orderby'} = shift @ARGV;
256 elsif (/^-([isl])$/) {
260 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
261 whine "No valid field list in '-f $ARGV[0]'.";
264 $data{fields} = shift @ARGV;
266 elsif (!defined $q && !/^-/) {
270 my $datum = /^-/ ? "option" : "argument";
271 whine "Unrecognised $datum '$_'.";
280 unless ($type && defined $q) {
281 my $item = $type ? "query string" : "object type";
282 whine "No $item specified.";
285 #return help("list", $type) if $bad;
286 return suggest_help("list", $type) if $bad;
288 my $r = submit("$REST/search/$type", { query => $q, %data });
292 # Displays selected information about a single object.
295 my ($type, @objects, %data);
303 $bad = 1, last unless defined($type = get_type_argument());
306 $bad = 1, last unless get_var_argument(\%data);
308 elsif (/^-([isl])$/) {
311 elsif (/^-$/ && !$slurped) {
312 chomp(my @lines = <STDIN>);
314 unless (is_object_spec($_, $type)) {
315 whine "Invalid object on STDIN: '$_'.";
323 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
324 whine "No valid field list in '-f $ARGV[0]'.";
327 $data{fields} = shift @ARGV;
329 elsif (my $spec = is_object_spec($_, $type)) {
330 push @objects, $spec;
333 my $datum = /^-/ ? "option" : "argument";
334 whine "Unrecognised $datum '$_'.";
340 whine "No objects specified.";
343 #return help("show", $type) if $bad;
344 return suggest_help("show", $type) if $bad;
346 my $r = submit("$REST/show", { id => \@objects, %data });
348 # if this isn't a text reply, remove the trailing newline so we
349 # don't corrupt things like tarballs when people do
350 # show ticket/id/attachments/id/content > foo.tar.gz
351 if ($r->content_type !~ /^text\//) {
357 # To create a new object, we ask the server for a form with the defaults
358 # filled in, allow the user to edit it, and send the form back.
360 # To edit an object, we must ask the server for a form representing that
361 # object, make changes requested by the user (either on the command line
362 # or interactively via $EDITOR), and send the form back.
366 my (%data, $type, @objects);
367 my ($cl, $text, $edit, $input, $output);
369 use vars qw(%set %add %del);
370 %set = %add = %del = ();
377 if (/^-e$/) { $edit = 1 }
378 elsif (/^-i$/) { $input = 1 }
379 elsif (/^-o$/) { $output = 1 }
381 $bad = 1, last unless defined($type = get_type_argument());
384 $bad = 1, last unless get_var_argument(\%data);
386 elsif (/^-$/ && !($slurped || $input)) {
387 chomp(my @lines = <STDIN>);
389 unless (is_object_spec($_, $type)) {
390 whine "Invalid object on STDIN: '$_'.";
400 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
401 my ($key, $op, $val) = ($1, $2, $3);
402 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
404 vpush($hash, lc $key, $val);
409 whine "No variables to set.";
414 elsif (/^(?:add|del)$/i) {
416 my $hash = ($_ eq "add") ? \%add : \%del;
418 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
419 my ($key, $val) = ($1, $2);
421 vpush($hash, lc $key, $val);
426 whine "No variables to set.";
431 elsif (my $spec = is_object_spec($_, $type)) {
432 push @objects, $spec;
435 my $datum = /^-/ ? "option" : "argument";
436 whine "Unrecognised $datum '$_'.";
441 if ($action =~ /^ed(?:it)?$/) {
443 whine "No objects specified.";
449 whine "You shouldn't specify objects as arguments to $action.";
453 whine "What type of object do you want to create?";
456 @objects = ("$type/new");
458 #return help($action, $type) if $bad;
459 return suggest_help($action, $type) if $bad;
461 # We need a form to make changes to. We usually ask the server for
462 # one, but we can avoid that if we are fed one on STDIN, or if the
463 # user doesn't want to edit the form by hand, and the command line
464 # specifies only simple variable assignments. We *should* get a
465 # form if we're creating a new ticket, so that the default values
466 # get filled in properly.
468 my @new_objects = grep /\/new$/, @objects;
474 elsif ($edit || %add || %del || !$cl || @new_objects) {
475 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
479 # If any changes were specified on the command line, apply them.
482 # We're updating forms from the server.
483 my $forms = Form::parse($text);
485 foreach my $form (@$forms) {
486 my ($c, $o, $k, $e) = @$form;
489 next if ($e || !@$o);
495 # Make changes to existing fields.
497 if (exists $add{lc $key}) {
498 $val = delete $add{lc $key};
499 vpush($k, $key, $val);
500 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
502 if (exists $del{lc $key}) {
503 $val = delete $del{lc $key};
504 my %val = map {$_=>1} @{ vsplit($val) };
505 $k->{$key} = vsplit($k->{$key});
506 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
508 if (exists $set{lc $key}) {
509 $k->{$key} = delete $set{lc $key};
513 # Then update the others.
514 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
515 foreach $key (keys %add) {
516 vpush($k, $key, $add{$key});
517 $k->{$key} = vsplit($k->{$key});
519 push @$o, (keys %add, keys %set);
522 $text = Form::compose($forms);
525 # We're rolling our own set of forms.
528 my ($type, $ids, $args) =
529 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
532 foreach my $obj (expand_list($ids)) {
533 my %set = (%set, id => "$type/$obj$args");
534 push @forms, ["", [keys %set], \%set];
537 $text = Form::compose(\@forms);
549 # We'll let the user edit the form before sending it to the server,
550 # unless we have enough information to submit it non-interactively.
551 if ($edit || (!$input && !$cl)) {
552 my $newtext = vi($text);
553 # We won't resubmit a bad form unless it was changed.
554 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
558 my $r = submit("$REST/edit", {content => $text, %data});
559 if ($r->code == 409) {
560 # If we submitted a bad form, we'll give the user a chance
561 # to correct it and resubmit.
562 if ($edit || (!$input && !$cl)) {
576 # We roll "comment" and "correspond" into the same handler.
580 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
589 elsif (/^-[abcmw]$/) {
591 whine "No argument specified with $_.";
596 unless (-f $ARGV[0] && -r $ARGV[0]) {
597 whine "Cannot read attachment: '$ARGV[0]'.";
600 push @files, shift @ARGV;
603 my $a = $_ eq "-b" ? \@bcc : \@cc;
604 @$a = split /\s*,\s*/, shift @ARGV;
608 if ( $msg =~ /^-$/ ) {
610 while (<STDIN>) { $msg .= $_ }
614 elsif (/-w/) { $wtime = shift @ARGV }
616 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
620 my $datum = /^-/ ? "option" : "argument";
621 whine "Unrecognised $datum '$_'.";
627 whine "No object specified.";
630 #return help($action, "ticket") if $bad;
631 return suggest_help($action, "ticket") if $bad;
635 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
641 Attachment => [ @files ],
642 TimeWorked => $wtime || '',
648 my $text = Form::compose([ $form ]);
650 if ($edit || !$msg) {
655 my $ntext = vi($text);
656 return if ($error && $ntext eq $text);
658 $form = Form::parse($text);
661 ($c, $o, $k, $e) = @{ $form->[0] };
664 $c = "# Syntax error.";
670 @files = @{ vsplit($k->{Attachment}) };
673 $text = Form::compose([[$c, $o, $k, $e]]);
678 foreach my $file (@files) {
679 $data{"attachment_$i"} = bless([ $file ], "Attachment");
682 $data{content} = $text;
684 my $r = submit("$REST/ticket/$id/comment", \%data);
688 # Merge one ticket into another.
701 whine "Unrecognised argument: '$_'.";
707 my $evil = @id > 2 ? "many" : "few";
708 whine "Too $evil arguments specified.";
711 #return help("merge", "ticket") if $bad;
712 return suggest_help("merge", "ticket") if $bad;
714 my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
718 # Link one ticket to another.
721 my ($bad, $del, %data) = (0, 0, ());
722 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
723 ReferredToBy HasMember MemberOf);
725 while (@ARGV && $ARGV[0] =~ /^-/) {
732 whine "Unrecognised option: '$_'.";
738 my ($from, $rel, $to) = @ARGV;
739 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
740 my $bad = $from =~ /^\d+$/ ? $to : $from;
741 whine "Invalid ticket ID '$bad' specified.";
744 unless (exists $ltypes{lc $rel}) {
745 whine "Invalid link '$rel' specified.";
748 %data = (id => $from, rel => $rel, to => $to, del => $del);
751 my $bad = @ARGV < 3 ? "few" : "many";
752 whine "Too $bad arguments specified.";
755 #return help("link", "ticket") if $bad;
756 return suggest_help("link", "ticket") if $bad;
758 my $r = submit("$REST/ticket/link", \%data);
762 # Take/steal a ticket
765 my ($bad, %data) = (0, ());
772 unless ($id =~ /^\d+$/) {
773 whine "Invalid ticket ID $id specified.";
778 [ "Ticket", "Action" ],
786 my $text = Form::compose([ $form ]);
787 $data{content} = $text;
790 $bad = @ARGV < 1 ? "few" : "many";
791 whine "Too $bad arguments specified.";
794 return suggest_help("take", "ticket") if $bad;
796 my $r = submit("$REST/ticket/$id/take", \%data);
800 # Grant/revoke a user's rights.
809 $revoke = 1 if $cmd->{action} eq 'revoke';
812 # Client <-> Server communication.
813 # --------------------------------
815 # This function composes and sends an HTTP request to the RT server, and
816 # interprets the response. It takes a request URI, and optional request
817 # data (a string, or a reference to a set of key-value pairs).
820 my ($uri, $content) = @_;
822 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
824 # Did the caller specify any data to send with the request?
826 if (defined $content) {
827 unless (ref $content) {
828 # If it's just a string, make sure LWP handles it properly.
829 # (By pretending that it's a file!)
830 $content = [ content => [undef, "", Content => $content] ];
832 elsif (ref $content eq 'HASH') {
834 foreach my $k (keys %$content) {
835 if (ref $content->{$k} eq 'ARRAY') {
836 foreach my $v (@{ $content->{$k} }) {
840 else { push @data, $k, $content->{$k} }
847 # Should we send authentication information to start a new session?
848 if (!defined $session->cookie) {
849 push @$data, ( user => $config{user} );
850 push @$data, ( pass => $config{passwd} || read_passwd() );
853 # Now, we construct the request.
855 $req = POST($uri, $data, Content_Type => 'form-data');
860 $session->add_cookie_header($req);
862 # Then we send the request and parse the response.
863 DEBUG(3, $req->as_string);
864 my $res = $ua->request($req);
865 DEBUG(3, $res->as_string);
867 if ($res->is_success) {
868 # The content of the response we get from the RT server consists
869 # of an HTTP-like status line followed by optional header lines,
870 # a blank line, and arbitrary text.
872 my ($head, $text) = split /\n\n/, $res->content, 2;
873 my ($status, @headers) = split /\n/, $head;
874 $text =~ s/\n*$/\n/ if ($text);
876 # "RT/3.0.1 401 Credentials required"
877 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
878 warn "rt: Malformed RT response from $config{server}.\n";
879 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
883 # Our caller can pretend that the server returned a custom HTTP
884 # response code and message. (Doing that directly is apparently
885 # not sufficiently portable and uncomplicated.)
888 $res->content($text);
889 $session->update($res) if ($res->is_success || $res->code != 401);
891 if (!$res->is_success) {
892 # We can deal with authentication failures ourselves. Either
893 # we sent invalid credentials, or our session has expired.
894 if ($res->code == 401) {
896 if (exists $d{user}) {
897 warn "rt: Incorrect username or password.\n";
900 elsif ($req->header("Cookie")) {
901 # We'll retry the request with credentials, unless
902 # we only wanted to logout in the first place.
904 return submit(@_) unless $uri eq "$REST/logout";
907 # Conflicts should be dealt with by the handler and user.
908 # For anything else, we just die.
909 elsif ($res->code != 409) {
910 warn "rt: ", $res->content;
916 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
923 # Session management.
924 # -------------------
926 # Maintains a list of active sessions in the ~/.rt_sessions file.
931 # Initialises the session cache.
933 my ($class, $file) = @_;
935 file => $file || "$HOME/.rt_sessions",
939 # The current session is identified by the currently configured
941 ($s, $u) = @config{"server", "user"};
949 # Returns the current session cookie.
952 my $cookie = $self->{sids}{$s}{$u};
953 return defined $cookie ? "RT_SID_$cookie" : undef;
956 # Deletes the current session cookie.
959 delete $self->{sids}{$s}{$u};
962 # Adds a Cookie header to an outgoing HTTP request.
963 sub add_cookie_header {
964 my ($self, $request) = @_;
965 my $cookie = $self->cookie();
967 $request->header(Cookie => $cookie) if defined $cookie;
970 # Extracts the Set-Cookie header from an HTTP response, and updates
971 # session information accordingly.
973 my ($self, $response) = @_;
974 my $cookie = $response->header("Set-Cookie");
976 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
977 $self->{sids}{$s}{$u} = $1;
981 # Loads the session cache from the specified file.
983 my ($self, $file) = @_;
984 $file ||= $self->{file};
987 open(F, $file) && do {
988 $self->{file} = $file;
989 my $sids = $self->{sids} = {};
992 next if /^$/ || /^#/;
993 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
994 my ($server, $user, $cookie) = split / /, $_;
995 $sids->{$server}{$user} = $cookie;
1002 # Writes the current session cache to the specified file.
1004 my ($self, $file) = shift;
1005 $file ||= $self->{file};
1008 open(F, ">$file") && do {
1009 my $sids = $self->{sids};
1010 foreach my $server (keys %$sids) {
1011 foreach my $user (keys %{ $sids->{$server} }) {
1012 my $sid = $sids->{$server}{$user};
1014 print F "$server $user $sid\n";
1034 # Forms are RFC822-style sets of (field, value) specifications with some
1035 # initial comments and interspersed blank lines allowed for convenience.
1036 # Sets of forms are separated by --\n (in a cheap parody of MIME).
1038 # Each form is parsed into an array with four elements: commented text
1039 # at the start of the form, an array with the order of keys, a hash with
1040 # key/value pairs, and optional error text if the form syntax was wrong.
1042 # Returns a reference to an array of parsed forms.
1046 my @lines = split /\n/, $_[0];
1047 my ($c, $o, $k, $e) = ("", [], {}, "");
1051 my $line = shift @lines;
1053 next LINE if $line eq '';
1055 if ($line eq '--') {
1056 # We reached the end of one form. We'll ignore it if it was
1057 # empty, and store it otherwise, errors and all.
1058 if ($e || $c || @$o) {
1059 push @forms, [ $c, $o, $k, $e ];
1060 $c = ""; $o = []; $k = {}; $e = "";
1064 elsif ($state != -1) {
1065 if ($state == 0 && $line =~ /^#/) {
1066 # Read an optional block of comments (only) at the start
1070 while (@lines && $lines[0] =~ /^#/) {
1071 $c .= "\n".shift @lines;
1075 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1076 # Read a field: value specification.
1080 # Read continuation lines, if any.
1081 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1082 push @v, shift @lines;
1084 pop @v while (@v && $v[-1] eq '');
1086 # Strip longest common leading indent from text.
1088 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1089 $ws = $ls if (!$ws || length($ls) < length($ws));
1091 s/^$ws// foreach @v;
1093 push(@$o, $f) unless exists $k->{$f};
1094 vpush($k, $f, join("\n", @v));
1098 elsif ($line !~ /^#/) {
1099 # We've found a syntax error, so we'll reconstruct the
1100 # form parsed thus far, and add an error marker. (>>)
1102 $e = Form::compose([[ "", $o, $k, "" ]]);
1103 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1107 # We saw a syntax error earlier, so we'll accumulate the
1108 # contents of this form until the end.
1112 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1114 foreach my $l (keys %$k) {
1115 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1121 # Returns text representing a set of forms.
1126 foreach my $form (@$forms) {
1127 my ($c, $o, $k, $e) = @$form;
1140 foreach my $key (@$o) {
1143 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1145 $sp = " "x(length("$key: "));
1146 $sp = " "x4 if length($sp) > 16;
1148 foreach $v (@values) {
1154 push @lines, "$line\n\n";
1157 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1160 push @lines, "$key: $v\n\n";
1163 length($line)+length($v)-rindex($line, "\n") >= 70)
1165 $line .= ",\n$sp$v";
1168 $line = $line ? "$line, $v" : "$key: $v";
1172 $line = "$key:" unless @values;
1174 if ($line =~ /\n/) {
1175 if (@lines && $lines[-1] !~ /\n\n$/) {
1180 push @lines, "$line\n";
1184 $text .= join "", @lines;
1192 return join "\n--\n\n", @text;
1198 # Returns configuration information from the environment.
1199 sub config_from_env {
1202 foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
1203 if (exists $ENV{"RT$k"}) {
1204 $env{lc $k} = $ENV{"RT$k"};
1211 # Finds a suitable configuration file and returns information from it.
1212 sub config_from_file {
1216 # We'll use an absolute path if we were given one.
1217 return parse_config_file($rc);
1220 # Otherwise we'll use the first file we can find in the current
1221 # directory, or in one of its (increasingly distant) ancestors.
1223 my @dirs = split /\//, cwd;
1225 my $file = join('/', @dirs, $rc);
1227 return parse_config_file($file);
1230 # Remove the last directory component each time.
1234 # Still nothing? We'll fall back to some likely defaults.
1235 for ("$HOME/$rc", "/etc/rt.conf") {
1236 return parse_config_file($_) if (-r $_);
1243 # Makes a hash of the specified configuration file.
1244 sub parse_config_file {
1247 local $_; # $_ may be aliased to a constant, from line 1163
1249 open(CFG, $file) && do {
1252 next if (/^#/ || /^\s*$/);
1254 if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
1258 die "rt: $file:$.: unknown configuration directive.\n";
1270 my $sub = (caller(1))[3];
1271 $sub =~ s/^main:://;
1272 warn "rt: $sub: @_\n";
1277 eval 'require Term::ReadKey';
1279 die "No password specified (and Term::ReadKey not installed).\n";
1283 Term::ReadKey::ReadMode('noecho');
1284 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1285 Term::ReadKey::ReadMode('restore');
1293 my $file = "/tmp/rt.form.$$";
1294 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1299 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1300 system($editor, $file) && die "Couldn't run $editor.\n";
1301 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1307 # Add a value to a (possibly multi-valued) hash key.
1309 my ($hash, $key, $val) = @_;
1310 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1312 if (exists $hash->{$key}) {
1313 unless (ref $hash->{$key} eq 'ARRAY') {
1314 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1315 $hash->{$key} = \@v;
1317 push @{ $hash->{$key} }, @val;
1320 $hash->{$key} = $val;
1324 # "Normalise" a hash key that's known to be multi-valued.
1328 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1330 foreach my $line (map {split /\n/} @values) {
1331 # XXX: This should become a real parser, Ã la Text::ParseWords.
1334 push @words, split /\s*,\s*/, $line;
1340 # WARN: this code is duplicated in lib/RT/Interface/REST.pm
1341 # change both functions at once
1346 foreach (split /,/, $list) {
1347 push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
1350 return map $_->[0], # schwartzian transform
1352 defined $a->[1] && defined $b->[1]?
1355 :!defined $a->[1] && !defined $b->[1]?
1358 # mix, number must be first
1359 :defined $a->[1]? -1: 1
1361 map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
1365 sub get_type_argument {
1369 $type = shift @ARGV;
1370 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1371 # We want whine to mention our caller, not us.
1372 @_ = ("Invalid type '$type' specified.");
1377 @_ = ("No type argument specified with -t.");
1381 $type =~ s/s$//; # "Plural". Ugh.
1385 sub get_var_argument {
1389 my $kv = shift @ARGV;
1390 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1391 push @{ $data->{$k} }, $v;
1394 @_ = ("Invalid variable specification: '$kv'.");
1399 @_ = ("No variable argument specified with -S.");
1404 sub is_object_spec {
1405 my ($spec, $type) = @_;
1407 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1408 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1413 my ($action, $type) = @_;
1415 print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
1416 print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
1425 ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1426 ** PLEASE REPORT BUGS TO rt-bugs@bestpractical.com **
1428 This is a command-line interface to RT 3.0 or newer
1430 It allows you to interact with an RT server over HTTP, and offers an
1431 interface to RT's functionality that is better-suited to automation
1432 and integration with other tools.
1434 In general, each invocation of this program should specify an action
1435 to perform on one or more objects, and any other arguments required
1436 to complete the desired action.
1438 For more information:
1440 - rt help usage (syntax information)
1441 - rt help objects (how to specify objects)
1442 - rt help actions (a list of possible actions)
1443 - rt help types (a list of object types)
1445 - rt help config (configuration details)
1446 - rt help examples (a few useful examples)
1447 - rt help topics (a list of help topics)
1457 rt <action> [options] [arguments]
1461 Each invocation of this program must specify an action (e.g. "edit",
1462 "create"), options to modify behaviour, and other arguments required
1463 by the specified action. (For example, most actions expect a list of
1464 numeric object IDs to act upon.)
1466 The details of the syntax and arguments for each action are given by
1467 "rt help <action>". Some actions may be referred to by more than one
1468 name ("create" is the same as "new", for example).
1470 You may also call "rt shell", which will give you an 'rt>' prompt at
1471 which you can issue commands of the form "<action> [options]
1472 [arguments]". See "rt help shell" for details.
1474 Objects are identified by a type and an ID (which can be a name or a
1475 number, depending on the type). For some actions, the object type is
1476 implied (you can only comment on tickets); for others, the user must
1477 specify it explicitly. See "rt help objects" for details.
1479 In syntax descriptions, mandatory arguments that must be replaced by
1480 appropriate value are enclosed in <>, and optional arguments are
1481 indicated by [] (for example, <action> and [options] above).
1483 For more information:
1485 - rt help objects (how to specify objects)
1486 - rt help actions (a list of actions)
1487 - rt help types (a list of object types)
1488 - rt help shell (how to use the shell)
1494 Title: configuration
1497 This program has two major sources of configuration information: its
1498 configuration files, and the environment.
1500 The program looks for configuration directives in a file named .rtrc
1501 (or $RTCONFIG; see below) in the current directory, and then in more
1502 distant ancestors, until it reaches /. If no suitable configuration
1503 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1505 Configuration directives:
1507 The following directives may occur, one per line:
1509 - server <URL> URL to RT server.
1510 - user <username> RT username.
1511 - passwd <passwd> RT user's password.
1512 - query <RT Query> Default RT Query for list action
1513 - orderby <order> Default RT order for list action
1515 Blank and #-commented lines are ignored.
1517 Environment variables:
1519 The following environment variables override any corresponding
1520 values defined in configuration files:
1525 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1526 - RTCONFIG Specifies a name other than ".rtrc" for the
1528 - RTQUERY Default RT Query for rt list
1529 - RTORDERBY Default order for rt list
1538 <type>/<id>[/<attributes>]
1540 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1541 ID. Some types of objects can also be identified by name (like users
1542 and queues). Furthermore, objects may have named attributes (such as
1543 "ticket/1/history").
1545 An object specification is like a path in a virtual filesystem, with
1546 object types as top-level directories, object IDs as subdirectories,
1547 and named attributes as further subdirectories.
1549 A comma-separated list of names, numeric IDs, or numeric ranges can
1550 be used to specify more than one object of the same type. Note that
1551 the list must be a single argument (i.e., no spaces). For example,
1552 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1553 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1558 ticket/1/attachments
1559 ticket/1/attachments/3
1560 ticket/1/attachments/3/content
1562 ticket/1-3,5-7/history
1566 user/ams,rai,1/rights
1568 For more information:
1570 - rt help <action> (action-specific details)
1571 - rt help <type> (type-specific details)
1579 You can currently perform the following actions on all objects:
1581 - list (list objects matching some condition)
1582 - show (display object details)
1583 - edit (edit object details)
1584 - create (create a new object)
1586 Each type may define actions specific to itself; these are listed in
1587 the help item about that type.
1589 For more information:
1591 - rt help <action> (action-specific details)
1592 - rt help types (a list of possible types)
1599 You can currently operate on the following types of objects:
1606 For more information:
1608 - rt help <type> (type-specific details)
1609 - rt help objects (how to specify objects)
1610 - rt help actions (a list of possible actions)
1617 Tickets are identified by a numeric ID.
1619 The following generic operations may be performed upon tickets:
1626 In addition, the following ticket-specific actions exist:
1635 The following attributes can be used with "rt show" or "rt edit"
1636 to retrieve or edit other information associated with tickets:
1638 links A ticket's relationships with others.
1639 history All of a ticket's transactions.
1640 history/type/<type> Only a particular type of transaction.
1641 history/id/<id> Only the transaction of the specified id.
1642 attachments A list of attachments.
1643 attachments/<id> The metadata for an individual attachment.
1644 attachments/<id>/content The content of an individual attachment.
1652 Users and groups are identified by name or numeric ID.
1654 The following generic operations may be performed upon them:
1661 In addition, the following type-specific actions exist:
1668 The following attributes can be used with "rt show" or "rt edit"
1669 to retrieve or edit other information associated with users and
1672 rights Global rights granted to this user.
1673 rights/<queue> Queue rights for this user.
1680 Queues are identified by name or numeric ID.
1682 Currently, they can be subjected to the following actions:
1697 Terminates the currently established login session. You will need to
1698 provide authentication credentials before you can continue using the
1699 server. (See "rt help config" for details about authentication.)
1710 rt <ls|list|search> [options] "query string"
1712 Displays a list of objects matching the specified conditions.
1713 ("ls", "list", and "search" are synonyms.)
1715 Conditions are expressed in the SQL-like syntax used internally by
1716 RT3. (For more information, see "rt help query".) The query string
1717 must be supplied as one argument.
1719 (Right now, the server doesn't support listing anything but tickets.
1720 Other types will be supported in future; this client will be able to
1721 take advantage of that support without any changes.)
1725 The following options control how much information is displayed
1726 about each matching object:
1728 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
1729 -s Short description.
1730 -l Longer description.
1734 -o +/-<field> Orders the returned list by the specified field.
1735 -S var=val Submits the specified variable with the request.
1736 -t type Specifies the type of object to look for. (The
1737 default is "ticket".)
1741 rt ls "Priority > 5 and Status='new'"
1742 rt ls -o +Subject "Priority > 5 and Status='new'"
1743 rt ls -o -Created "Priority > 5 and Status='new'"
1744 rt ls -i "Priority > 5"|rt edit - set status=resolved
1745 rt ls -t ticket "Subject like '[PATCH]%'"
1754 rt show [options] <object-ids>
1756 Displays details of the specified objects.
1758 For some types, object information is further classified into named
1759 attributes (for example, "1-3/links" is a valid ticket specification
1760 that refers to the links for tickets 1-3). Consult "rt help <type>"
1761 and "rt help objects" for further details.
1763 This command writes a set of forms representing the requested object
1768 - Read IDs from STDIN instead of the command-line.
1769 -t type Specifies object type.
1770 -f a,b,c Restrict the display to the specified fields.
1771 -S var=val Submits the specified variable with the request.
1775 rt show -t ticket -f id,subject,status 1-3
1776 rt show ticket/3/attachments/29
1777 rt show ticket/3/attachments/29/content
1778 rt show ticket/1-3/links
1779 rt show ticket/3/history
1780 rt show -v ticket/3/history
1792 rt edit [options] <object-ids> set field=value [field=value] ...
1793 add field=value [field=value] ...
1794 del field=value [field=value] ...
1796 Edits information corresponding to the specified objects.
1798 If, instead of "edit", an action of "new" or "create" is specified,
1799 then a new object is created. In this case, no numeric object IDs
1800 may be specified, but the syntax and behaviour remain otherwise
1803 This command typically starts an editor to allow you to edit object
1804 data in a form for submission. If you specified enough information
1805 on the command-line, however, it will make the submission directly.
1807 The command line may specify field-values in three different ways.
1808 "set" sets the named field to the given value, "add" adds a value
1809 to a multi-valued field, and "del" deletes the corresponding value.
1810 Each "field=value" specification must be given as a single argument.
1812 For some types, object information is further classified into named
1813 attributes (for example, "1-3/links" is a valid ticket specification
1814 that refers to the links for tickets 1-3). These attributes may also
1815 be edited. Consult "rt help <type>" and "rt help object" for further
1820 - Read numeric IDs from STDIN instead of the command-line.
1821 (Useful with rt ls ... | rt edit -; see examples below.)
1822 -i Read a completed form from STDIN before submitting.
1823 -o Dump the completed form to STDOUT instead of submitting.
1824 -e Allows you to edit the form even if the command-line has
1825 enough information to make a submission directly.
1827 Submits the specified variable with the request.
1828 -t type Specifies object type.
1832 # Interactive (starts $EDITOR with a form).
1837 rt edit ticket/1-3 add cc=foo@example.com set priority=3
1838 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1839 rt edit ticket/4 set priority=3 owner=bar@example.com \
1840 add cc=foo@example.com bcc=quux@example.net
1841 rt create -t ticket set subject='new ticket' priority=10 \
1842 add cc=foo@example.com
1852 rt <comment|correspond> [options] <ticket-id>
1854 Adds a comment (or correspondence) to the specified ticket (the only
1855 difference being that comments aren't sent to the requestors.)
1857 This command will typically start an editor and allow you to type a
1858 comment into a form. If, however, you specified all the necessary
1859 information on the command line, it submits the comment directly.
1861 (See "rt help forms" for more information about forms.)
1865 -m <text> Specify comment text.
1866 -a <file> Attach a file to the comment. (May be used more
1867 than once to attach multiple files.)
1868 -c <addrs> A comma-separated list of Cc addresses.
1869 -b <addrs> A comma-separated list of Bcc addresses.
1870 -w <time> Specify the time spent working on this ticket.
1871 -e Starts an editor before the submission, even if
1872 arguments from the command line were sufficient.
1876 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
1885 rt merge <from-id> <to-id>
1887 Merges the first ticket specified into the second ticket specified.
1896 rt link [-d] <id-A> <link> <id-B>
1898 Creates (or, with -d, deletes) a link between the specified tickets.
1899 The link can (irrespective of case) be any of:
1901 DependsOn/DependedOnBy: A depends upon B (or vice versa).
1902 RefersTo/ReferredToBy: A refers to B (or vice versa).
1903 MemberOf/HasMember: A is a member of B (or vice versa).
1905 To view a ticket's links, use "rt show ticket/3/links". (See
1906 "rt help ticket" and "rt help show".)
1910 -d Deletes the specified link.
1914 rt link 2 dependson 3
1915 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
1928 RT3 uses an SQL-like syntax to specify object selection constraints.
1929 See the <RT:...> documentation for details.
1931 (XXX: I'm going to have to write it, aren't I?)
1939 This program uses RFC822 header-style forms to represent object data
1940 in a form that's suitable for processing both by humans and scripts.
1942 A form is a set of (field, value) specifications, with some initial
1943 commented text and interspersed blank lines allowed for convenience.
1944 Field names may appear more than once in a form; a comma-separated
1945 list of multiple field values may also be specified directly.
1947 Field values can be wrapped as in RFC822, with leading whitespace.
1948 The longest sequence of leading whitespace common to all the lines
1949 is removed (preserving further indentation). There is no limit on
1950 the length of a value.
1952 Multiple forms are separated by a line containing only "--\n".
1954 (XXX: A more detailed specification will be provided soon. For now,
1955 the server-side syntax checking will suffice.)
1966 Get help on any of the following subjects:
1968 - tickets, users, groups, queues.
1969 - show, edit, ls/list/search, new/create.
1971 - query (search query syntax)
1972 - forms (form specification)
1974 - objects (how to specify objects)
1975 - types (a list of object types)
1976 - actions/commands (a list of actions)
1977 - usage/syntax (syntax details)
1978 - conf/config/configuration (configuration details)
1979 - examples (a few useful examples)
1987 This section will be filled in with useful examples, once it becomes
1988 more clear what examples may be useful.
1990 For the moment, please consult examples provided with each action.
2001 Opens an interactive shell, at which you can issue commands of
2002 the form "<action> [options] [arguments]".
2004 To exit the shell, type "quit" or "exit".
2006 Commands can be given at the shell in the same form as they would
2007 be given at the command line without the leading 'rt' invocation.
2011 rt> create -t ticket set subject='new' add cc=foo@example.com
2025 rt <take|untake|steal> <ticket-id>
2027 Sets the owner of the specified ticket to the current user,
2028 assuming said user has the bits to do so, or releases the
2031 'Take' is used on tickets which are not currently owned
2032 (Owner: Nobody), 'steal' is used on tickets which *are*
2033 currently owned, and 'untake' is used to "release" a ticket
2034 (reset its Owner to Nobody). 'Take' cannot be used on
2035 tickets which are currently owned.
2038 alice$ rt create -t ticket set subject="New ticket"
2041 # Owner changed from Nobody to alice
2044 # Owner changed from alice to bob
2046 # Owner changed from bob to Nobody
2054 Use "quit" or "exit" to leave the shell. Only valid within shell