2 # BEGIN BPS TAGGED BLOCK {{{
6 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
29 # CONTRIBUTION SUBMISSION POLICY:
31 # (The following paragraph is not intended to limit the rights granted
32 # to you to modify and distribute this software under the terms of
33 # the GNU General Public License and is only of importance to you if
34 # you choose to contribute your changes and enhancements to the
35 # community by submitting them to Best Practical Solutions, LLC.)
37 # By intentionally submitting any modifications, corrections or
38 # derivatives to this work, or any other work intended for use with
39 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
40 # you are the copyright holder for those contributions and you grant
41 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
42 # royalty-free, perpetual, license to use, copy, create derivative
43 # works based on those contributions, and sublicense and distribute
44 # those contributions and any derivatives thereof.
46 # END BPS TAGGED BLOCK }}}
48 # Designed and implemented for Best Practical Solutions, LLC by
49 # Abhijit Menon-Sen <ams@wiw.org>
53 # This program is intentionally written to have as few non-core module
54 # dependencies as possible. It should stay that way.
59 use HTTP::Request::Common;
61 # We derive configuration information from hardwired defaults, dotfiles,
62 # and the RT* environment variables (in increasing order of precedence).
63 # Session information is stored in ~/.rt_sessions.
66 my $HOME = eval{(getpwuid($<))[7]}
67 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
72 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
74 server => 'http://localhost/rt/',
78 config_from_file($ENV{RTCONFIG} || ".rtrc"),
81 my $session = new Session("$HOME/.rt_sessions");
82 my $REST = "$config{server}/REST/1.0";
85 sub DEBUG { warn @_ if $config{debug} >= shift }
87 # These regexes are used by command handlers to parse arguments.
88 # (XXX: Ask Autrijus how i18n changes these definitions.)
91 my $field = '[a-zA-Z][a-zA-Z0-9_-]*';
92 my $label = '[a-zA-Z0-9@_.+-]+';
93 my $labels = "(?:$label,)*$label";
94 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
96 # Our command line looks like this:
98 # rt <action> [options] [arguments]
100 # We'll parse just enough of it to decide upon an action to perform, and
101 # leave the rest to per-action handlers to interpret appropriately.
104 # handler => [ ...aliases... ],
105 version => ["version", "ver"],
107 logout => ["logout"],
108 help => ["help", "man"],
109 show => ["show", "cat"],
110 edit => ["create", "edit", "new", "ed"],
111 list => ["search", "list", "ls"],
112 comment => ["comment", "correspond"],
113 link => ["link", "ln"],
115 grant => ["grant", "revoke"],
119 foreach my $fn (keys %handlers) {
120 foreach my $alias (@{ $handlers{$fn} }) {
121 $actions{$alias} = \&{"$fn"};
125 # Once we find and call an appropriate handler, we're done.
130 if (@ARGV && exists $actions{$ARGV[0]}) {
131 $action = shift @ARGV;
133 $actions{$action || "help"}->($action || ());
142 # The following subs are handlers for each entry in %actions.
149 next if /^#/ || /^\s*$/;
151 @ARGV = shellwords($_);
159 print "rt $VERSION\n";
163 submit("$REST/logout") if defined $session->cookie;
168 my ($action, $type) = @_;
171 # What help topics do we know about?
174 foreach my $item (@{ Form::parse(<DATA>) }) {
175 my $title = $item->[2]{Title};
176 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
178 foreach $title (grep $_, @titles) {
179 $help{$title} = $item->[2]{Text};
184 # What does the user want help with?
185 undef $action if ($action && $actions{$action} eq \&help);
186 unless ($action || $type) {
187 # If we don't know, we'll look for clues in @ARGV.
189 if (exists $help{$_}) { $key = $_; last; }
192 # Tolerate possibly plural words.
194 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
199 if ($type && $action) {
200 $key = "$type.$action";
202 $key ||= $type || $action || "introduction";
204 # Find a suitable topic to display.
205 while (!exists $help{$key}) {
206 if ($type && $action) {
207 if ($key eq "$type.$action") { $key = $action; }
208 elsif ($key eq $action) { $key = $type; }
209 else { $key = "introduction"; }
212 $key = "introduction";
216 print STDERR $help{$key}, "\n\n";
219 # Displays a list of objects that match some specified condition.
222 my ($q, $type, %data);
223 my $orderby = $config{orderby};
225 if ($config{orderby}) {
226 $data{orderby} = $config{orderby};
234 $bad = 1, last unless defined($type = get_type_argument());
237 $bad = 1, last unless get_var_argument(\%data);
240 $data{'orderby'} = shift @ARGV;
242 elsif (/^-([isl])$/) {
246 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
247 whine "No valid field list in '-f $ARGV[0]'.";
250 $data{fields} = shift @ARGV;
252 elsif (!defined $q && !/^-/) {
256 my $datum = /^-/ ? "option" : "argument";
257 whine "Unrecognised $datum '$_'.";
266 unless ($type && defined $q) {
267 my $item = $type ? "query string" : "object type";
268 whine "No $item specified.";
271 return help("list", $type) if $bad;
273 my $r = submit("$REST/search/$type", { query => $q, %data });
277 # Displays selected information about a single object.
280 my ($type, @objects, %data);
288 $bad = 1, last unless defined($type = get_type_argument());
291 $bad = 1, last unless get_var_argument(\%data);
293 elsif (/^-([isl])$/) {
296 elsif (/^-$/ && !$slurped) {
297 chomp(my @lines = <STDIN>);
299 unless (is_object_spec($_, $type)) {
300 whine "Invalid object on STDIN: '$_'.";
308 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
309 whine "No valid field list in '-f $ARGV[0]'.";
312 $data{fields} = shift @ARGV;
314 elsif (my $spec = is_object_spec($_, $type)) {
315 push @objects, $spec;
318 my $datum = /^-/ ? "option" : "argument";
319 whine "Unrecognised $datum '$_'.";
325 whine "No objects specified.";
328 return help("show", $type) if $bad;
330 my $r = submit("$REST/show", { id => \@objects, %data });
334 # To create a new object, we ask the server for a form with the defaults
335 # filled in, allow the user to edit it, and send the form back.
337 # To edit an object, we must ask the server for a form representing that
338 # object, make changes requested by the user (either on the command line
339 # or interactively via $EDITOR), and send the form back.
343 my (%data, $type, @objects);
344 my ($cl, $text, $edit, $input, $output);
346 use vars qw(%set %add %del);
347 %set = %add = %del = ();
354 if (/^-e$/) { $edit = 1 }
355 elsif (/^-i$/) { $input = 1 }
356 elsif (/^-o$/) { $output = 1 }
358 $bad = 1, last unless defined($type = get_type_argument());
361 $bad = 1, last unless get_var_argument(\%data);
363 elsif (/^-$/ && !($slurped || $input)) {
364 chomp(my @lines = <STDIN>);
366 unless (is_object_spec($_, $type)) {
367 whine "Invalid object on STDIN: '$_'.";
377 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
378 my ($key, $op, $val) = ($1, $2, $3);
379 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
381 vpush($hash, lc $key, $val);
386 whine "No variables to set.";
391 elsif (/^(?:add|del)$/i) {
393 my $hash = ($_ eq "add") ? \%add : \%del;
395 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
396 my ($key, $val) = ($1, $2);
398 vpush($hash, lc $key, $val);
403 whine "No variables to set.";
408 elsif (my $spec = is_object_spec($_, $type)) {
409 push @objects, $spec;
412 my $datum = /^-/ ? "option" : "argument";
413 whine "Unrecognised $datum '$_'.";
418 if ($action =~ /^ed(?:it)?$/) {
420 whine "No objects specified.";
426 whine "You shouldn't specify objects as arguments to $action.";
430 whine "What type of object do you want to create?";
433 @objects = ("$type/new");
435 return help($action, $type) if $bad;
437 # We need a form to make changes to. We usually ask the server for
438 # one, but we can avoid that if we are fed one on STDIN, or if the
439 # user doesn't want to edit the form by hand, and the command line
440 # specifies only simple variable assignments.
446 elsif ($edit || %add || %del || !$cl) {
447 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
451 # If any changes were specified on the command line, apply them.
454 # We're updating forms from the server.
455 my $forms = Form::parse($text);
457 foreach my $form (@$forms) {
458 my ($c, $o, $k, $e) = @$form;
461 next if ($e || !@$o);
467 # Make changes to existing fields.
469 if (exists $add{lc $key}) {
470 $val = delete $add{lc $key};
471 vpush($k, $key, $val);
472 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
474 if (exists $del{lc $key}) {
475 $val = delete $del{lc $key};
476 my %val = map {$_=>1} @{ vsplit($val) };
477 $k->{$key} = vsplit($k->{$key});
478 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
480 if (exists $set{lc $key}) {
481 $k->{$key} = delete $set{lc $key};
485 # Then update the others.
486 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
487 foreach $key (keys %add) {
488 vpush($k, $key, $add{$key});
489 $k->{$key} = vsplit($k->{$key});
491 push @$o, (keys %add, keys %set);
494 $text = Form::compose($forms);
497 # We're rolling our own set of forms.
500 my ($type, $ids, $args) =
501 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
504 foreach my $obj (expand_list($ids)) {
505 my %set = (%set, id => "$type/$obj$args");
506 push @forms, ["", [keys %set], \%set];
509 $text = Form::compose(\@forms);
521 # We'll let the user edit the form before sending it to the server,
522 # unless we have enough information to submit it non-interactively.
523 if ($edit || (!$input && !$cl)) {
524 my $newtext = vi($text);
525 # We won't resubmit a bad form unless it was changed.
526 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
530 my $r = submit("$REST/edit", {content => $text, %data});
531 if ($r->code == 409) {
532 # If we submitted a bad form, we'll give the user a chance
533 # to correct it and resubmit.
534 if ($edit || (!$input && !$cl)) {
548 # We roll "comment" and "correspond" into the same handler.
552 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
561 elsif (/^-[abcmw]$/) {
563 whine "No argument specified with $_.";
568 unless (-f $ARGV[0] && -r $ARGV[0]) {
569 whine "Cannot read attachment: '$ARGV[0]'.";
572 push @files, shift @ARGV;
575 my $a = $_ eq "-b" ? \@bcc : \@cc;
576 @$a = split /\s*,\s*/, shift @ARGV;
580 if ( $msg =~ /^-$/ ) {
582 while (<STDIN>) { $msg .= $_ }
586 elsif (/-w/) { $wtime = shift @ARGV }
588 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
592 my $datum = /^-/ ? "option" : "argument";
593 whine "Unrecognised $datum '$_'.";
599 whine "No object specified.";
602 return help($action, "ticket") if $bad;
606 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
612 Attachment => [ @files ],
613 TimeWorked => $wtime || '',
619 my $text = Form::compose([ $form ]);
621 if ($edit || !$msg) {
626 my $ntext = vi($text);
627 return if ($error && $ntext eq $text);
629 $form = Form::parse($text);
632 ($c, $o, $k, $e) = @{ $form->[0] };
635 $c = "# Syntax error.";
641 @files = @{ vsplit($k->{Attachment}) };
644 $text = Form::compose([[$c, $o, $k, $e]]);
649 foreach my $file (@files) {
650 $data{"attachment_$i"} = bless([ $file ], "Attachment");
653 $data{content} = $text;
655 my $r = submit("$REST/ticket/comment/$id", \%data);
659 # Merge one ticket into another.
672 whine "Unrecognised argument: '$_'.";
678 my $evil = @id > 2 ? "many" : "few";
679 whine "Too $evil arguments specified.";
682 return help("merge", "ticket") if $bad;
684 my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
688 # Link one ticket to another.
691 my ($bad, $del, %data) = (0, 0, ());
692 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
693 ReferredToBy HasMember MemberOf);
695 while (@ARGV && $ARGV[0] =~ /^-/) {
702 whine "Unrecognised option: '$_'.";
708 my ($from, $rel, $to) = @ARGV;
709 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
710 my $bad = $from =~ /^\d+$/ ? $to : $from;
711 whine "Invalid ticket ID '$bad' specified.";
714 unless (exists $ltypes{lc $rel}) {
715 whine "Invalid link '$rel' specified.";
718 %data = (id => $from, rel => $rel, to => $to, del => $del);
721 my $bad = @ARGV < 3 ? "few" : "many";
722 whine "Too $bad arguments specified.";
725 return help("link", "ticket") if $bad;
727 my $r = submit("$REST/ticket/link", \%data);
731 # Grant/revoke a user's rights.
740 $revoke = 1 if $cmd->{action} eq 'revoke';
743 # Client <-> Server communication.
744 # --------------------------------
746 # This function composes and sends an HTTP request to the RT server, and
747 # interprets the response. It takes a request URI, and optional request
748 # data (a string, or a reference to a set of key-value pairs).
751 my ($uri, $content) = @_;
753 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
755 # Did the caller specify any data to send with the request?
757 if (defined $content) {
758 unless (ref $content) {
759 # If it's just a string, make sure LWP handles it properly.
760 # (By pretending that it's a file!)
761 $content = [ content => [undef, "", Content => $content] ];
763 elsif (ref $content eq 'HASH') {
765 foreach my $k (keys %$content) {
766 if (ref $content->{$k} eq 'ARRAY') {
767 foreach my $v (@{ $content->{$k} }) {
771 else { push @data, $k, $content->{$k} }
778 # Should we send authentication information to start a new session?
779 if (!defined $session->cookie) {
780 push @$data, ( user => $config{user} );
781 push @$data, ( pass => $config{passwd} || read_passwd() );
784 # Now, we construct the request.
786 $req = POST($uri, $data, Content_Type => 'form-data');
791 $session->add_cookie_header($req);
793 # Then we send the request and parse the response.
794 DEBUG(3, $req->as_string);
795 my $res = $ua->request($req);
796 DEBUG(3, $res->as_string);
798 if ($res->is_success) {
799 # The content of the response we get from the RT server consists
800 # of an HTTP-like status line followed by optional header lines,
801 # a blank line, and arbitrary text.
803 my ($head, $text) = split /\n\n/, $res->content, 2;
804 my ($status, @headers) = split /\n/, $head;
805 $text =~ s/\n*$/\n/ if ($text);
807 # "RT/3.0.1 401 Credentials required"
808 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
809 warn "rt: Malformed RT response from $config{server}.\n";
810 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
814 # Our caller can pretend that the server returned a custom HTTP
815 # response code and message. (Doing that directly is apparently
816 # not sufficiently portable and uncomplicated.)
819 $res->content($text);
820 $session->update($res) if ($res->is_success || $res->code != 401);
822 if (!$res->is_success) {
823 # We can deal with authentication failures ourselves. Either
824 # we sent invalid credentials, or our session has expired.
825 if ($res->code == 401) {
827 if (exists $d{user}) {
828 warn "rt: Incorrect username or password.\n";
831 elsif ($req->header("Cookie")) {
832 # We'll retry the request with credentials, unless
833 # we only wanted to logout in the first place.
835 return submit(@_) unless $uri eq "$REST/logout";
838 # Conflicts should be dealt with by the handler and user.
839 # For anything else, we just die.
840 elsif ($res->code != 409) {
841 warn "rt: ", $res->content;
847 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
854 # Session management.
855 # -------------------
857 # Maintains a list of active sessions in the ~/.rt_sessions file.
862 # Initialises the session cache.
864 my ($class, $file) = @_;
866 file => $file || "$HOME/.rt_sessions",
870 # The current session is identified by the currently configured
872 ($s, $u) = @config{"server", "user"};
880 # Returns the current session cookie.
883 my $cookie = $self->{sids}{$s}{$u};
884 return defined $cookie ? "RT_SID_$cookie" : undef;
887 # Deletes the current session cookie.
890 delete $self->{sids}{$s}{$u};
893 # Adds a Cookie header to an outgoing HTTP request.
894 sub add_cookie_header {
895 my ($self, $request) = @_;
896 my $cookie = $self->cookie();
898 $request->header(Cookie => $cookie) if defined $cookie;
901 # Extracts the Set-Cookie header from an HTTP response, and updates
902 # session information accordingly.
904 my ($self, $response) = @_;
905 my $cookie = $response->header("Set-Cookie");
907 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
908 $self->{sids}{$s}{$u} = $1;
912 # Loads the session cache from the specified file.
914 my ($self, $file) = @_;
915 $file ||= $self->{file};
918 open(F, $file) && do {
919 $self->{file} = $file;
920 my $sids = $self->{sids} = {};
923 next if /^$/ || /^#/;
924 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
925 my ($server, $user, $cookie) = split / /, $_;
926 $sids->{$server}{$user} = $cookie;
933 # Writes the current session cache to the specified file.
935 my ($self, $file) = shift;
936 $file ||= $self->{file};
939 open(F, ">$file") && do {
940 my $sids = $self->{sids};
941 foreach my $server (keys %$sids) {
942 foreach my $user (keys %{ $sids->{$server} }) {
943 my $sid = $sids->{$server}{$user};
945 print F "$server $user $sid\n";
965 # Forms are RFC822-style sets of (field, value) specifications with some
966 # initial comments and interspersed blank lines allowed for convenience.
967 # Sets of forms are separated by --\n (in a cheap parody of MIME).
969 # Each form is parsed into an array with four elements: commented text
970 # at the start of the form, an array with the order of keys, a hash with
971 # key/value pairs, and optional error text if the form syntax was wrong.
973 # Returns a reference to an array of parsed forms.
977 my @lines = split /\n/, $_[0];
978 my ($c, $o, $k, $e) = ("", [], {}, "");
982 my $line = shift @lines;
984 next LINE if $line eq '';
987 # We reached the end of one form. We'll ignore it if it was
988 # empty, and store it otherwise, errors and all.
989 if ($e || $c || @$o) {
990 push @forms, [ $c, $o, $k, $e ];
991 $c = ""; $o = []; $k = {}; $e = "";
995 elsif ($state != -1) {
996 if ($state == 0 && $line =~ /^#/) {
997 # Read an optional block of comments (only) at the start
1001 while (@lines && $lines[0] =~ /^#/) {
1002 $c .= "\n".shift @lines;
1006 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1007 # Read a field: value specification.
1011 # Read continuation lines, if any.
1012 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1013 push @v, shift @lines;
1015 pop @v while (@v && $v[-1] eq '');
1017 # Strip longest common leading indent from text.
1019 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1020 $ws = $ls if (!$ws || length($ls) < length($ws));
1022 s/^$ws// foreach @v;
1024 push(@$o, $f) unless exists $k->{$f};
1025 vpush($k, $f, join("\n", @v));
1029 elsif ($line !~ /^#/) {
1030 # We've found a syntax error, so we'll reconstruct the
1031 # form parsed thus far, and add an error marker. (>>)
1033 $e = Form::compose([[ "", $o, $k, "" ]]);
1034 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1038 # We saw a syntax error earlier, so we'll accumulate the
1039 # contents of this form until the end.
1043 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1045 foreach my $l (keys %$k) {
1046 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1052 # Returns text representing a set of forms.
1057 foreach my $form (@$forms) {
1058 my ($c, $o, $k, $e) = @$form;
1071 foreach my $key (@$o) {
1074 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1076 $sp = " "x(length("$key: "));
1077 $sp = " "x4 if length($sp) > 16;
1079 foreach $v (@values) {
1085 push @lines, "$line\n\n";
1088 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1091 push @lines, "$key: $v\n\n";
1094 length($line)+length($v)-rindex($line, "\n") >= 70)
1096 $line .= ",\n$sp$v";
1099 $line = $line ? "$line, $v" : "$key: $v";
1103 $line = "$key:" unless @values;
1105 if ($line =~ /\n/) {
1106 if (@lines && $lines[-1] !~ /\n\n$/) {
1111 push @lines, "$line\n";
1115 $text .= join "", @lines;
1123 return join "\n--\n\n", @text;
1129 # Returns configuration information from the environment.
1130 sub config_from_env {
1133 foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
1134 if (exists $ENV{"RT$k"}) {
1135 $env{lc $k} = $ENV{"RT$k"};
1142 # Finds a suitable configuration file and returns information from it.
1143 sub config_from_file {
1147 # We'll use an absolute path if we were given one.
1148 return parse_config_file($rc);
1151 # Otherwise we'll use the first file we can find in the current
1152 # directory, or in one of its (increasingly distant) ancestors.
1154 my @dirs = split /\//, cwd;
1156 my $file = join('/', @dirs, $rc);
1158 return parse_config_file($file);
1161 # Remove the last directory component each time.
1165 # Still nothing? We'll fall back to some likely defaults.
1166 for ("$HOME/$rc", "/etc/rt.conf") {
1167 return parse_config_file($_) if (-r $_);
1174 # Makes a hash of the specified configuration file.
1175 sub parse_config_file {
1178 local $_; # $_ may be aliased to a constant, from line 1163
1180 open(CFG, $file) && do {
1183 next if (/^#/ || /^\s*$/);
1185 if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
1189 die "rt: $file:$.: unknown configuration directive.\n";
1201 my $sub = (caller(1))[3];
1202 $sub =~ s/^main:://;
1203 warn "rt: $sub: @_\n";
1208 eval 'require Term::ReadKey';
1210 die "No password specified (and Term::ReadKey not installed).\n";
1214 Term::ReadKey::ReadMode('noecho');
1215 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1216 Term::ReadKey::ReadMode('restore');
1224 my $file = "/tmp/rt.form.$$";
1225 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1230 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1231 system($editor, $file) && die "Couldn't run $editor.\n";
1232 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1238 # Add a value to a (possibly multi-valued) hash key.
1240 my ($hash, $key, $val) = @_;
1241 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1243 if (exists $hash->{$key}) {
1244 unless (ref $hash->{$key} eq 'ARRAY') {
1245 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1246 $hash->{$key} = \@v;
1248 push @{ $hash->{$key} }, @val;
1251 $hash->{$key} = $val;
1255 # "Normalise" a hash key that's known to be multi-valued.
1259 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1261 foreach my $line (map {split /\n/} @values) {
1262 # XXX: This should become a real parser, Ã la Text::ParseWords.
1265 push @words, split /\s*,\s*/, $line;
1273 my ($elt, @elts, %elts);
1275 foreach $elt (split /,/, $list) {
1276 if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
1277 else { push @elts, $elt }
1281 return sort {$a<=>$b} keys %elts;
1284 sub get_type_argument {
1288 $type = shift @ARGV;
1289 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1290 # We want whine to mention our caller, not us.
1291 @_ = ("Invalid type '$type' specified.");
1296 @_ = ("No type argument specified with -t.");
1300 $type =~ s/s$//; # "Plural". Ugh.
1304 sub get_var_argument {
1308 my $kv = shift @ARGV;
1309 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1310 push @{ $data->{$k} }, $v;
1313 @_ = ("Invalid variable specification: '$kv'.");
1318 @_ = ("No variable argument specified with -S.");
1323 sub is_object_spec {
1324 my ($spec, $type) = @_;
1326 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1327 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1337 ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1338 ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
1340 This is a command-line interface to RT 3.
1342 It allows you to interact with an RT server over HTTP, and offers an
1343 interface to RT's functionality that is better-suited to automation
1344 and integration with other tools.
1346 In general, each invocation of this program should specify an action
1347 to perform on one or more objects, and any other arguments required
1348 to complete the desired action.
1350 For more information:
1352 - rt help actions (a list of possible actions)
1353 - rt help objects (how to specify objects)
1354 - rt help usage (syntax information)
1356 - rt help config (configuration details)
1357 - rt help examples (a few useful examples)
1358 - rt help topics (a list of help topics)
1368 rt <action> [options] [arguments]
1370 Each invocation of this program must specify an action (e.g. "edit",
1371 "create"), options to modify behaviour, and other arguments required
1372 by the specified action. (For example, most actions expect a list of
1373 numeric object IDs to act upon.)
1375 The details of the syntax and arguments for each action are given by
1376 "rt help <action>". Some actions may be referred to by more than one
1377 name ("create" is the same as "new", for example).
1379 Objects are identified by a type and an ID (which can be a name or a
1380 number, depending on the type). For some actions, the object type is
1381 implied (you can only comment on tickets); for others, the user must
1382 specify it explicitly. See "rt help objects" for details.
1384 In syntax descriptions, mandatory arguments that must be replaced by
1385 appropriate value are enclosed in <>, and optional arguments are
1386 indicated by [] (for example, <action> and [options] above).
1388 For more information:
1390 - rt help objects (how to specify objects)
1391 - rt help actions (a list of actions)
1392 - rt help types (a list of object types)
1398 Title: configuration
1401 This program has two major sources of configuration information: its
1402 configuration files, and the environment.
1404 The program looks for configuration directives in a file named .rtrc
1405 (or $RTCONFIG; see below) in the current directory, and then in more
1406 distant ancestors, until it reaches /. If no suitable configuration
1407 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1409 Configuration directives:
1411 The following directives may occur, one per line:
1413 - server <URL> URL to RT server.
1414 - user <username> RT username.
1415 - passwd <passwd> RT user's password.
1416 - query <RT Query> Default RT Query for list action
1417 - orderby <order> Default RT order for list action
1419 Blank and #-commented lines are ignored.
1421 Environment variables:
1423 The following environment variables override any corresponding
1424 values defined in configuration files:
1429 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1430 - RTCONFIG Specifies a name other than ".rtrc" for the
1432 - RTQUERY Default RT Query for rt list
1433 - RTORDERBY Default order for rt list
1442 <type>/<id>[/<attributes>]
1444 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1445 ID. Some types of objects can also be identified by name (like users
1446 and queues). Furthermore, objects may have named attributes (such as
1447 "ticket/1/history").
1449 An object specification is like a path in a virtual filesystem, with
1450 object types as top-level directories, object IDs as subdirectories,
1451 and named attributes as further subdirectories.
1453 A comma-separated list of names, numeric IDs, or numeric ranges can
1454 be used to specify more than one object of the same type. Note that
1455 the list must be a single argument (i.e., no spaces). For example,
1456 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1457 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1462 ticket/1/attachments
1463 ticket/1/attachments/3
1464 ticket/1/attachments/3/content
1466 ticket/1-3,5-7/history
1470 user/ams,rai,1/rights
1472 For more information:
1474 - rt help <action> (action-specific details)
1475 - rt help <type> (type-specific details)
1483 You can currently perform the following actions on all objects:
1485 - list (list objects matching some condition)
1486 - show (display object details)
1487 - edit (edit object details)
1488 - create (create a new object)
1490 Each type may define actions specific to itself; these are listed in
1491 the help item about that type.
1493 For more information:
1495 - rt help <action> (action-specific details)
1496 - rt help types (a list of possible types)
1503 You can currently operate on the following types of objects:
1510 For more information:
1512 - rt help <type> (type-specific details)
1513 - rt help objects (how to specify objects)
1514 - rt help actions (a list of possible actions)
1521 Tickets are identified by a numeric ID.
1523 The following generic operations may be performed upon tickets:
1530 In addition, the following ticket-specific actions exist:
1539 The following attributes can be used with "rt show" or "rt edit"
1540 to retrieve or edit other information associated with tickets:
1542 links A ticket's relationships with others.
1543 history All of a ticket's transactions.
1544 history/type/<type> Only a particular type of transaction.
1545 history/id/<id> Only the transaction of the specified id.
1546 attachments A list of attachments.
1547 attachments/<id> The metadata for an individual attachment.
1548 attachments/<id>/content The content of an individual attachment.
1556 Users and groups are identified by name or numeric ID.
1558 The following generic operations may be performed upon them:
1565 In addition, the following type-specific actions exist:
1572 The following attributes can be used with "rt show" or "rt edit"
1573 to retrieve or edit other information associated with users and
1576 rights Global rights granted to this user.
1577 rights/<queue> Queue rights for this user.
1584 Queues are identified by name or numeric ID.
1586 Currently, they can be subjected to the following actions:
1601 Terminates the currently established login session. You will need to
1602 provide authentication credentials before you can continue using the
1603 server. (See "rt help config" for details about authentication.)
1614 rt <ls|list|search> [options] "query string"
1616 Displays a list of objects matching the specified conditions.
1617 ("ls", "list", and "search" are synonyms.)
1619 Conditions are expressed in the SQL-like syntax used internally by
1620 RT3. (For more information, see "rt help query".) The query string
1621 must be supplied as one argument.
1623 (Right now, the server doesn't support listing anything but tickets.
1624 Other types will be supported in future; this client will be able to
1625 take advantage of that support without any changes.)
1629 The following options control how much information is displayed
1630 about each matching object:
1632 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
1633 -s Short description.
1634 -l Longer description.
1638 -o +/-<field> Orders the returned list by the specified field.
1639 -S var=val Submits the specified variable with the request.
1640 -t type Specifies the type of object to look for. (The
1641 default is "ticket".)
1645 rt ls "Priority > 5 and Status='new'"
1646 rt ls -o +Subject "Priority > 5 and Status='new'"
1647 rt ls -o -Created "Priority > 5 and Status='new'"
1648 rt ls -i "Priority > 5"|rt edit - set status=resolved
1649 rt ls -t ticket "Subject like '[PATCH]%'"
1658 rt show [options] <object-ids>
1660 Displays details of the specified objects.
1662 For some types, object information is further classified into named
1663 attributes (for example, "1-3/links" is a valid ticket specification
1664 that refers to the links for tickets 1-3). Consult "rt help <type>"
1665 and "rt help objects" for further details.
1667 This command writes a set of forms representing the requested object
1672 - Read IDs from STDIN instead of the command-line.
1673 -t type Specifies object type.
1674 -f a,b,c Restrict the display to the specified fields.
1675 -S var=val Submits the specified variable with the request.
1679 rt show -t ticket -f id,subject,status 1-3
1680 rt show ticket/3/attachments/29
1681 rt show ticket/3/attachments/29/content
1682 rt show ticket/1-3/links
1694 rt edit [options] <object-ids> set field=value [field=value] ...
1695 add field=value [field=value] ...
1696 del field=value [field=value] ...
1698 Edits information corresponding to the specified objects.
1700 If, instead of "edit", an action of "new" or "create" is specified,
1701 then a new object is created. In this case, no numeric object IDs
1702 may be specified, but the syntax and behaviour remain otherwise
1705 This command typically starts an editor to allow you to edit object
1706 data in a form for submission. If you specified enough information
1707 on the command-line, however, it will make the submission directly.
1709 The command line may specify field-values in three different ways.
1710 "set" sets the named field to the given value, "add" adds a value
1711 to a multi-valued field, and "del" deletes the corresponding value.
1712 Each "field=value" specification must be given as a single argument.
1714 For some types, object information is further classified into named
1715 attributes (for example, "1-3/links" is a valid ticket specification
1716 that refers to the links for tickets 1-3). These attributes may also
1717 be edited. Consult "rt help <type>" and "rt help object" for further
1722 - Read numeric IDs from STDIN instead of the command-line.
1723 (Useful with rt ls ... | rt edit -; see examples below.)
1724 -i Read a completed form from STDIN before submitting.
1725 -o Dump the completed form to STDOUT instead of submitting.
1726 -e Allows you to edit the form even if the command-line has
1727 enough information to make a submission directly.
1729 Submits the specified variable with the request.
1730 -t type Specifies object type.
1734 # Interactive (starts $EDITOR with a form).
1739 rt edit ticket/1-3 add cc=foo@example.com set priority=3
1740 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1741 rt edit ticket/4 set priority=3 owner=bar@example.com \
1742 add cc=foo@example.com bcc=quux@example.net
1743 rt create -t ticket subject='new ticket' priority=10 \
1744 add cc=foo@example.com
1754 rt <comment|correspond> [options] <ticket-id>
1756 Adds a comment (or correspondence) to the specified ticket (the only
1757 difference being that comments aren't sent to the requestors.)
1759 This command will typically start an editor and allow you to type a
1760 comment into a form. If, however, you specified all the necessary
1761 information on the command line, it submits the comment directly.
1763 (See "rt help forms" for more information about forms.)
1767 -m <text> Specify comment text.
1768 -a <file> Attach a file to the comment. (May be used more
1769 than once to attach multiple files.)
1770 -c <addrs> A comma-separated list of Cc addresses.
1771 -b <addrs> A comma-separated list of Bcc addresses.
1772 -w <time> Specify the time spent working on this ticket.
1773 -e Starts an editor before the submission, even if
1774 arguments from the command line were sufficient.
1778 rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
1787 rt merge <from-id> <to-id>
1789 Merges the two specified tickets.
1798 rt link [-d] <id-A> <link> <id-B>
1800 Creates (or, with -d, deletes) a link between the specified tickets.
1801 The link can (irrespective of case) be any of:
1803 DependsOn/DependedOnBy: A depends upon B (or vice versa).
1804 RefersTo/ReferredToBy: A refers to B (or vice versa).
1805 MemberOf/HasMember: A is a member of B (or vice versa).
1807 To view a ticket's links, use "rt show ticket/3/links". (See
1808 "rt help ticket" and "rt help show".)
1812 -d Deletes the specified link.
1816 rt link 2 dependson 3
1817 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
1830 RT3 uses an SQL-like syntax to specify object selection constraints.
1831 See the <RT:...> documentation for details.
1833 (XXX: I'm going to have to write it, aren't I?)
1841 This program uses RFC822 header-style forms to represent object data
1842 in a form that's suitable for processing both by humans and scripts.
1844 A form is a set of (field, value) specifications, with some initial
1845 commented text and interspersed blank lines allowed for convenience.
1846 Field names may appear more than once in a form; a comma-separated
1847 list of multiple field values may also be specified directly.
1849 Field values can be wrapped as in RFC822, with leading whitespace.
1850 The longest sequence of leading whitespace common to all the lines
1851 is removed (preserving further indentation). There is no limit on
1852 the length of a value.
1854 Multiple forms are separated by a line containing only "--\n".
1856 (XXX: A more detailed specification will be provided soon. For now,
1857 the server-side syntax checking will suffice.)
1864 Use "rt help <topic>" for help on any of the following subjects:
1866 - tickets, users, groups, queues.
1867 - show, edit, ls/list/search, new/create.
1869 - query (search query syntax)
1870 - forms (form specification)
1872 - objects (how to specify objects)
1873 - types (a list of object types)
1874 - actions/commands (a list of actions)
1875 - usage/syntax (syntax details)
1876 - conf/config/configuration (configuration details)
1877 - examples (a few useful examples)
1885 This section will be filled in with useful examples, once it becomes
1886 more clear what examples may be useful.
1888 For the moment, please consult examples provided with each action.