2 # {{{ BEGIN BPS TAGGED BLOCK
6 # This software is Copyright (c) 1996-2004 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
49 # This program is intentionally written to have as few non-core module
50 # dependencies as possible. It should stay that way.
55 use HTTP::Request::Common;
57 # We derive configuration information from hardwired defaults, dotfiles,
58 # and the RT* environment variables (in increasing order of precedence).
59 # Session information is stored in ~/.rt_sessions.
62 my $HOME = eval{(getpwuid($<))[7]}
63 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
68 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
70 server => 'http://localhost/rt/',
74 config_from_file($ENV{RTCONFIG} || ".rtrc"),
77 my $session = new Session("$HOME/.rt_sessions");
78 my $REST = "$config{server}/REST/1.0";
81 sub DEBUG { warn @_ if $config{debug} >= shift }
83 # These regexes are used by command handlers to parse arguments.
84 # (XXX: Ask Autrijus how i18n changes these definitions.)
87 my $field = '[a-zA-Z][a-zA-Z0-9_-]*';
88 my $label = '[a-zA-Z0-9@_.+-]+';
89 my $labels = "(?:$label,)*$label";
90 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
92 # Our command line looks like this:
94 # rt <action> [options] [arguments]
96 # We'll parse just enough of it to decide upon an action to perform, and
97 # leave the rest to per-action handlers to interpret appropriately.
100 # handler => [ ...aliases... ],
101 version => ["version", "ver"],
103 logout => ["logout"],
104 help => ["help", "man"],
105 show => ["show", "cat"],
106 edit => ["create", "edit", "new", "ed"],
107 list => ["search", "list", "ls"],
108 comment => ["comment", "correspond"],
109 link => ["link", "ln"],
111 grant => ["grant", "revoke"],
115 foreach my $fn (keys %handlers) {
116 foreach my $alias (@{ $handlers{$fn} }) {
117 $actions{$alias} = \&{"$fn"};
121 # Once we find and call an appropriate handler, we're done.
126 if (@ARGV && exists $actions{$ARGV[0]}) {
127 $action = shift @ARGV;
129 $actions{$action || "help"}->($action || ());
138 # The following subs are handlers for each entry in %actions.
145 next if /^#/ || /^\s*$/;
147 @ARGV = shellwords($_);
155 print "rt $VERSION\n";
159 submit("$REST/logout") if defined $session->cookie;
164 my ($action, $type) = @_;
167 # What help topics do we know about?
170 foreach my $item (@{ Form::parse(<DATA>) }) {
171 my $title = $item->[2]{Title};
172 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
174 foreach $title (grep $_, @titles) {
175 $help{$title} = $item->[2]{Text};
180 # What does the user want help with?
181 undef $action if ($action && $actions{$action} eq \&help);
182 unless ($action || $type) {
183 # If we don't know, we'll look for clues in @ARGV.
185 if (exists $help{$_}) { $key = $_; last; }
188 # Tolerate possibly plural words.
190 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
195 if ($type && $action) {
196 $key = "$type.$action";
198 $key ||= $type || $action || "introduction";
200 # Find a suitable topic to display.
201 while (!exists $help{$key}) {
202 if ($type && $action) {
203 if ($key eq "$type.$action") { $key = $action; }
204 elsif ($key eq $action) { $key = $type; }
205 else { $key = "introduction"; }
208 $key = "introduction";
212 print STDERR $help{$key}, "\n\n";
215 # Displays a list of objects that match some specified condition.
218 my ($q, $type, %data);
219 my $orderby = $config{orderby};
221 if ($config{orderby}) {
222 $data{orderby} = $config{orderby};
230 $bad = 1, last unless defined($type = get_type_argument());
233 $bad = 1, last unless get_var_argument(\%data);
236 $data{'orderby'} = shift @ARGV;
238 elsif (/^-([isl])$/) {
242 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
243 whine "No valid field list in '-f $ARGV[0]'.";
246 $data{fields} = shift @ARGV;
248 elsif (!defined $q && !/^-/) {
252 my $datum = /^-/ ? "option" : "argument";
253 whine "Unrecognised $datum '$_'.";
262 unless ($type && defined $q) {
263 my $item = $type ? "query string" : "object type";
264 whine "No $item specified.";
267 return help("list", $type) if $bad;
269 my $r = submit("$REST/search/$type", { query => $q, %data });
273 # Displays selected information about a single object.
276 my ($type, @objects, %data);
284 $bad = 1, last unless defined($type = get_type_argument());
287 $bad = 1, last unless get_var_argument(\%data);
289 elsif (/^-([isl])$/) {
292 elsif (/^-$/ && !$slurped) {
293 chomp(my @lines = <STDIN>);
295 unless (is_object_spec($_, $type)) {
296 whine "Invalid object on STDIN: '$_'.";
304 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
305 whine "No valid field list in '-f $ARGV[0]'.";
308 $data{fields} = shift @ARGV;
310 elsif (my $spec = is_object_spec($_, $type)) {
311 push @objects, $spec;
314 my $datum = /^-/ ? "option" : "argument";
315 whine "Unrecognised $datum '$_'.";
321 whine "No objects specified.";
324 return help("show", $type) if $bad;
326 my $r = submit("$REST/show", { id => \@objects, %data });
330 # To create a new object, we ask the server for a form with the defaults
331 # filled in, allow the user to edit it, and send the form back.
333 # To edit an object, we must ask the server for a form representing that
334 # object, make changes requested by the user (either on the command line
335 # or interactively via $EDITOR), and send the form back.
339 my (%data, $type, @objects);
340 my ($cl, $text, $edit, $input, $output);
342 use vars qw(%set %add %del);
343 %set = %add = %del = ();
350 if (/^-e$/) { $edit = 1 }
351 elsif (/^-i$/) { $input = 1 }
352 elsif (/^-o$/) { $output = 1 }
354 $bad = 1, last unless defined($type = get_type_argument());
357 $bad = 1, last unless get_var_argument(\%data);
359 elsif (/^-$/ && !($slurped || $input)) {
360 chomp(my @lines = <STDIN>);
362 unless (is_object_spec($_, $type)) {
363 whine "Invalid object on STDIN: '$_'.";
373 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
374 my ($key, $op, $val) = ($1, $2, $3);
375 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
377 vpush($hash, lc $key, $val);
382 whine "No variables to set.";
387 elsif (/^(?:add|del)$/i) {
389 my $hash = ($_ eq "add") ? \%add : \%del;
391 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
392 my ($key, $val) = ($1, $2);
394 vpush($hash, lc $key, $val);
399 whine "No variables to set.";
404 elsif (my $spec = is_object_spec($_, $type)) {
405 push @objects, $spec;
408 my $datum = /^-/ ? "option" : "argument";
409 whine "Unrecognised $datum '$_'.";
414 if ($action =~ /^ed(?:it)?$/) {
416 whine "No objects specified.";
422 whine "You shouldn't specify objects as arguments to $action.";
426 whine "What type of object do you want to create?";
429 @objects = ("$type/new");
431 return help($action, $type) if $bad;
433 # We need a form to make changes to. We usually ask the server for
434 # one, but we can avoid that if we are fed one on STDIN, or if the
435 # user doesn't want to edit the form by hand, and the command line
436 # specifies only simple variable assignments.
442 elsif ($edit || %add || %del || !$cl) {
443 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
447 # If any changes were specified on the command line, apply them.
450 # We're updating forms from the server.
451 my $forms = Form::parse($text);
453 foreach my $form (@$forms) {
454 my ($c, $o, $k, $e) = @$form;
457 next if ($e || !@$o);
463 # Make changes to existing fields.
465 if (exists $add{lc $key}) {
466 $val = delete $add{lc $key};
467 vpush($k, $key, $val);
468 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
470 if (exists $del{lc $key}) {
471 $val = delete $del{lc $key};
472 my %val = map {$_=>1} @{ vsplit($val) };
473 $k->{$key} = vsplit($k->{$key});
474 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
476 if (exists $set{lc $key}) {
477 $k->{$key} = delete $set{lc $key};
481 # Then update the others.
482 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
483 foreach $key (keys %add) {
484 vpush($k, $key, $add{$key});
485 $k->{$key} = vsplit($k->{$key});
487 push @$o, (keys %add, keys %set);
490 $text = Form::compose($forms);
493 # We're rolling our own set of forms.
496 my ($type, $ids, $args) =
497 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
500 foreach my $obj (expand_list($ids)) {
501 my %set = (%set, id => "$type/$obj$args");
502 push @forms, ["", [keys %set], \%set];
505 $text = Form::compose(\@forms);
517 # We'll let the user edit the form before sending it to the server,
518 # unless we have enough information to submit it non-interactively.
519 if ($edit || (!$input && !$cl)) {
520 my $newtext = vi($text);
521 # We won't resubmit a bad form unless it was changed.
522 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
526 my $r = submit("$REST/edit", {content => $text, %data});
527 if ($r->code == 409) {
528 # If we submitted a bad form, we'll give the user a chance
529 # to correct it and resubmit.
530 if ($edit || (!$input && !$cl)) {
544 # We roll "comment" and "correspond" into the same handler.
548 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
557 elsif (/^-[abcmw]$/) {
559 whine "No argument specified with $_.";
564 unless (-f $ARGV[0] && -r $ARGV[0]) {
565 whine "Cannot read attachment: '$ARGV[0]'.";
568 push @files, shift @ARGV;
571 my $a = $_ eq "-b" ? \@bcc : \@cc;
572 @$a = split /\s*,\s*/, shift @ARGV;
576 if ( $msg =~ /^-$/ ) {
578 while (<STDIN>) { $msg .= $_ }
582 elsif (/-w/) { $wtime = shift @ARGV }
584 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
588 my $datum = /^-/ ? "option" : "argument";
589 whine "Unrecognised $datum '$_'.";
595 whine "No object specified.";
598 return help($action, "ticket") if $bad;
602 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
608 Attachment => [ @files ],
609 TimeWorked => $wtime || '',
615 my $text = Form::compose([ $form ]);
617 if ($edit || !$msg) {
622 my $ntext = vi($text);
623 return if ($error && $ntext eq $text);
625 $form = Form::parse($text);
628 ($c, $o, $k, $e) = @{ $form->[0] };
631 $c = "# Syntax error.";
637 @files = @{ vsplit($k->{Attachment}) };
640 $text = Form::compose([[$c, $o, $k, $e]]);
645 foreach my $file (@files) {
646 $data{"attachment_$i"} = bless([ $file ], "Attachment");
649 $data{content} = $text;
651 my $r = submit("$REST/ticket/comment/$id", \%data);
655 # Merge one ticket into another.
668 whine "Unrecognised argument: '$_'.";
674 my $evil = @id > 2 ? "many" : "few";
675 whine "Too $evil arguments specified.";
678 return help("merge", "ticket") if $bad;
680 my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
684 # Link one ticket to another.
687 my ($bad, $del, %data) = (0, 0, ());
688 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
689 ReferredToBy HasMember MemberOf);
691 while (@ARGV && $ARGV[0] =~ /^-/) {
698 whine "Unrecognised option: '$_'.";
704 my ($from, $rel, $to) = @ARGV;
705 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
706 my $bad = $from =~ /^\d+$/ ? $to : $from;
707 whine "Invalid ticket ID '$bad' specified.";
710 unless (exists $ltypes{lc $rel}) {
711 whine "Invalid link '$rel' specified.";
714 %data = (id => $from, rel => $rel, to => $to, del => $del);
717 my $bad = @ARGV < 3 ? "few" : "many";
718 whine "Too $bad arguments specified.";
721 return help("link", "ticket") if $bad;
723 my $r = submit("$REST/ticket/link", \%data);
727 # Grant/revoke a user's rights.
736 $revoke = 1 if $cmd->{action} eq 'revoke';
739 # Client <-> Server communication.
740 # --------------------------------
742 # This function composes and sends an HTTP request to the RT server, and
743 # interprets the response. It takes a request URI, and optional request
744 # data (a string, or a reference to a set of key-value pairs).
747 my ($uri, $content) = @_;
749 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
751 # Did the caller specify any data to send with the request?
753 if (defined $content) {
754 unless (ref $content) {
755 # If it's just a string, make sure LWP handles it properly.
756 # (By pretending that it's a file!)
757 $content = [ content => [undef, "", Content => $content] ];
759 elsif (ref $content eq 'HASH') {
761 foreach my $k (keys %$content) {
762 if (ref $content->{$k} eq 'ARRAY') {
763 foreach my $v (@{ $content->{$k} }) {
767 else { push @data, $k, $content->{$k} }
774 # Should we send authentication information to start a new session?
775 if (!defined $session->cookie) {
776 push @$data, ( user => $config{user} );
777 push @$data, ( pass => $config{passwd} || read_passwd() );
780 # Now, we construct the request.
782 $req = POST($uri, $data, Content_Type => 'form-data');
787 $session->add_cookie_header($req);
789 # Then we send the request and parse the response.
790 DEBUG(3, $req->as_string);
791 my $res = $ua->request($req);
792 DEBUG(3, $res->as_string);
794 if ($res->is_success) {
795 # The content of the response we get from the RT server consists
796 # of an HTTP-like status line followed by optional header lines,
797 # a blank line, and arbitrary text.
799 my ($head, $text) = split /\n\n/, $res->content, 2;
800 my ($status, @headers) = split /\n/, $head;
801 $text =~ s/\n*$/\n/ if ($text);
803 # "RT/3.0.1 401 Credentials required"
804 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
805 warn "rt: Malformed RT response from $config{server}.\n";
806 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
810 # Our caller can pretend that the server returned a custom HTTP
811 # response code and message. (Doing that directly is apparently
812 # not sufficiently portable and uncomplicated.)
815 $res->content($text);
816 $session->update($res) if ($res->is_success || $res->code != 401);
818 if (!$res->is_success) {
819 # We can deal with authentication failures ourselves. Either
820 # we sent invalid credentials, or our session has expired.
821 if ($res->code == 401) {
823 if (exists $d{user}) {
824 warn "rt: Incorrect username or password.\n";
827 elsif ($req->header("Cookie")) {
828 # We'll retry the request with credentials, unless
829 # we only wanted to logout in the first place.
831 return submit(@_) unless $uri eq "$REST/logout";
834 # Conflicts should be dealt with by the handler and user.
835 # For anything else, we just die.
836 elsif ($res->code != 409) {
837 warn "rt: ", $res->content;
843 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
850 # Session management.
851 # -------------------
853 # Maintains a list of active sessions in the ~/.rt_sessions file.
858 # Initialises the session cache.
860 my ($class, $file) = @_;
862 file => $file || "$HOME/.rt_sessions",
866 # The current session is identified by the currently configured
868 ($s, $u) = @config{"server", "user"};
876 # Returns the current session cookie.
879 my $cookie = $self->{sids}{$s}{$u};
880 return defined $cookie ? "RT_SID_$cookie" : undef;
883 # Deletes the current session cookie.
886 delete $self->{sids}{$s}{$u};
889 # Adds a Cookie header to an outgoing HTTP request.
890 sub add_cookie_header {
891 my ($self, $request) = @_;
892 my $cookie = $self->cookie();
894 $request->header(Cookie => $cookie) if defined $cookie;
897 # Extracts the Set-Cookie header from an HTTP response, and updates
898 # session information accordingly.
900 my ($self, $response) = @_;
901 my $cookie = $response->header("Set-Cookie");
903 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
904 $self->{sids}{$s}{$u} = $1;
908 # Loads the session cache from the specified file.
910 my ($self, $file) = @_;
911 $file ||= $self->{file};
914 open(F, $file) && do {
915 $self->{file} = $file;
916 my $sids = $self->{sids} = {};
919 next if /^$/ || /^#/;
920 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
921 my ($server, $user, $cookie) = split / /, $_;
922 $sids->{$server}{$user} = $cookie;
929 # Writes the current session cache to the specified file.
931 my ($self, $file) = shift;
932 $file ||= $self->{file};
935 open(F, ">$file") && do {
936 my $sids = $self->{sids};
937 foreach my $server (keys %$sids) {
938 foreach my $user (keys %{ $sids->{$server} }) {
939 my $sid = $sids->{$server}{$user};
941 print F "$server $user $sid\n";
961 # Forms are RFC822-style sets of (field, value) specifications with some
962 # initial comments and interspersed blank lines allowed for convenience.
963 # Sets of forms are separated by --\n (in a cheap parody of MIME).
965 # Each form is parsed into an array with four elements: commented text
966 # at the start of the form, an array with the order of keys, a hash with
967 # key/value pairs, and optional error text if the form syntax was wrong.
969 # Returns a reference to an array of parsed forms.
973 my @lines = split /\n/, $_[0];
974 my ($c, $o, $k, $e) = ("", [], {}, "");
978 my $line = shift @lines;
980 next LINE if $line eq '';
983 # We reached the end of one form. We'll ignore it if it was
984 # empty, and store it otherwise, errors and all.
985 if ($e || $c || @$o) {
986 push @forms, [ $c, $o, $k, $e ];
987 $c = ""; $o = []; $k = {}; $e = "";
991 elsif ($state != -1) {
992 if ($state == 0 && $line =~ /^#/) {
993 # Read an optional block of comments (only) at the start
997 while (@lines && $lines[0] =~ /^#/) {
998 $c .= "\n".shift @lines;
1002 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
1003 # Read a field: value specification.
1007 # Read continuation lines, if any.
1008 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
1009 push @v, shift @lines;
1011 pop @v while (@v && $v[-1] eq '');
1013 # Strip longest common leading indent from text.
1015 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
1016 $ws = $ls if (!$ws || length($ls) < length($ws));
1018 s/^$ws// foreach @v;
1020 push(@$o, $f) unless exists $k->{$f};
1021 vpush($k, $f, join("\n", @v));
1025 elsif ($line !~ /^#/) {
1026 # We've found a syntax error, so we'll reconstruct the
1027 # form parsed thus far, and add an error marker. (>>)
1029 $e = Form::compose([[ "", $o, $k, "" ]]);
1030 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
1034 # We saw a syntax error earlier, so we'll accumulate the
1035 # contents of this form until the end.
1039 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
1041 foreach my $l (keys %$k) {
1042 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
1048 # Returns text representing a set of forms.
1053 foreach my $form (@$forms) {
1054 my ($c, $o, $k, $e) = @$form;
1067 foreach my $key (@$o) {
1070 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1072 $sp = " "x(length("$key: "));
1073 $sp = " "x4 if length($sp) > 16;
1075 foreach $v (@values) {
1081 push @lines, "$line\n\n";
1084 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1087 push @lines, "$key: $v\n\n";
1090 length($line)+length($v)-rindex($line, "\n") >= 70)
1092 $line .= ",\n$sp$v";
1095 $line = $line ? "$line, $v" : "$key: $v";
1099 $line = "$key:" unless @values;
1101 if ($line =~ /\n/) {
1102 if (@lines && $lines[-1] !~ /\n\n$/) {
1107 push @lines, "$line\n";
1111 $text .= join "", @lines;
1119 return join "\n--\n\n", @text;
1125 # Returns configuration information from the environment.
1126 sub config_from_env {
1129 foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
1130 if (exists $ENV{"RT$k"}) {
1131 $env{lc $k} = $ENV{"RT$k"};
1138 # Finds a suitable configuration file and returns information from it.
1139 sub config_from_file {
1143 # We'll use an absolute path if we were given one.
1144 return parse_config_file($rc);
1147 # Otherwise we'll use the first file we can find in the current
1148 # directory, or in one of its (increasingly distant) ancestors.
1150 my @dirs = split /\//, cwd;
1152 my $file = join('/', @dirs, $rc);
1154 return parse_config_file($file);
1157 # Remove the last directory component each time.
1161 # Still nothing? We'll fall back to some likely defaults.
1162 for ("$HOME/$rc", "/etc/rt.conf") {
1163 return parse_config_file($_) if (-r $_);
1170 # Makes a hash of the specified configuration file.
1171 sub parse_config_file {
1174 local $_; # $_ may be aliased to a constant, from line 1163
1176 open(CFG, $file) && do {
1179 next if (/^#/ || /^\s*$/);
1181 if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
1185 die "rt: $file:$.: unknown configuration directive.\n";
1197 my $sub = (caller(1))[3];
1198 $sub =~ s/^main:://;
1199 warn "rt: $sub: @_\n";
1204 eval 'require Term::ReadKey';
1206 die "No password specified (and Term::ReadKey not installed).\n";
1210 Term::ReadKey::ReadMode('noecho');
1211 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1212 Term::ReadKey::ReadMode('restore');
1220 my $file = "/tmp/rt.form.$$";
1221 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1226 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1227 system($editor, $file) && die "Couldn't run $editor.\n";
1228 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1234 # Add a value to a (possibly multi-valued) hash key.
1236 my ($hash, $key, $val) = @_;
1237 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1239 if (exists $hash->{$key}) {
1240 unless (ref $hash->{$key} eq 'ARRAY') {
1241 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1242 $hash->{$key} = \@v;
1244 push @{ $hash->{$key} }, @val;
1247 $hash->{$key} = $val;
1251 # "Normalise" a hash key that's known to be multi-valued.
1255 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1257 foreach my $line (map {split /\n/} @values) {
1258 # XXX: This should become a real parser, Ã la Text::ParseWords.
1261 push @words, split /\s*,\s*/, $line;
1269 my ($elt, @elts, %elts);
1271 foreach $elt (split /,/, $list) {
1272 if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
1273 else { push @elts, $elt }
1277 return sort {$a<=>$b} keys %elts;
1280 sub get_type_argument {
1284 $type = shift @ARGV;
1285 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1286 # We want whine to mention our caller, not us.
1287 @_ = ("Invalid type '$type' specified.");
1292 @_ = ("No type argument specified with -t.");
1296 $type =~ s/s$//; # "Plural". Ugh.
1300 sub get_var_argument {
1304 my $kv = shift @ARGV;
1305 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1306 push @{ $data->{$k} }, $v;
1309 @_ = ("Invalid variable specification: '$kv'.");
1314 @_ = ("No variable argument specified with -S.");
1319 sub is_object_spec {
1320 my ($spec, $type) = @_;
1322 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1323 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1333 ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1334 ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
1336 This is a command-line interface to RT 3.
1338 It allows you to interact with an RT server over HTTP, and offers an
1339 interface to RT's functionality that is better-suited to automation
1340 and integration with other tools.
1342 In general, each invocation of this program should specify an action
1343 to perform on one or more objects, and any other arguments required
1344 to complete the desired action.
1346 For more information:
1348 - rt help actions (a list of possible actions)
1349 - rt help objects (how to specify objects)
1350 - rt help usage (syntax information)
1352 - rt help config (configuration details)
1353 - rt help examples (a few useful examples)
1354 - rt help topics (a list of help topics)
1364 rt <action> [options] [arguments]
1366 Each invocation of this program must specify an action (e.g. "edit",
1367 "create"), options to modify behaviour, and other arguments required
1368 by the specified action. (For example, most actions expect a list of
1369 numeric object IDs to act upon.)
1371 The details of the syntax and arguments for each action are given by
1372 "rt help <action>". Some actions may be referred to by more than one
1373 name ("create" is the same as "new", for example).
1375 Objects are identified by a type and an ID (which can be a name or a
1376 number, depending on the type). For some actions, the object type is
1377 implied (you can only comment on tickets); for others, the user must
1378 specify it explicitly. See "rt help objects" for details.
1380 In syntax descriptions, mandatory arguments that must be replaced by
1381 appropriate value are enclosed in <>, and optional arguments are
1382 indicated by [] (for example, <action> and [options] above).
1384 For more information:
1386 - rt help objects (how to specify objects)
1387 - rt help actions (a list of actions)
1388 - rt help types (a list of object types)
1394 Title: configuration
1397 This program has two major sources of configuration information: its
1398 configuration files, and the environment.
1400 The program looks for configuration directives in a file named .rtrc
1401 (or $RTCONFIG; see below) in the current directory, and then in more
1402 distant ancestors, until it reaches /. If no suitable configuration
1403 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1405 Configuration directives:
1407 The following directives may occur, one per line:
1409 - server <URL> URL to RT server.
1410 - user <username> RT username.
1411 - passwd <passwd> RT user's password.
1412 - query <RT Query> Default RT Query for list action
1413 - orderby <order> Default RT order for list action
1415 Blank and #-commented lines are ignored.
1417 Environment variables:
1419 The following environment variables override any corresponding
1420 values defined in configuration files:
1425 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1426 - RTCONFIG Specifies a name other than ".rtrc" for the
1428 - RTQUERY Default RT Query for rt list
1429 - RTORDERBY Default order for rt list
1438 <type>/<id>[/<attributes>]
1440 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1441 ID. Some types of objects can also be identified by name (like users
1442 and queues). Furthermore, objects may have named attributes (such as
1443 "ticket/1/history").
1445 An object specification is like a path in a virtual filesystem, with
1446 object types as top-level directories, object IDs as subdirectories,
1447 and named attributes as further subdirectories.
1449 A comma-separated list of names, numeric IDs, or numeric ranges can
1450 be used to specify more than one object of the same type. Note that
1451 the list must be a single argument (i.e., no spaces). For example,
1452 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1453 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1458 ticket/1/attachments
1459 ticket/1/attachments/3
1460 ticket/1/attachments/3/content
1462 ticket/1-3,5-7/history
1466 user/ams,rai,1/rights
1468 For more information:
1470 - rt help <action> (action-specific details)
1471 - rt help <type> (type-specific details)
1479 You can currently perform the following actions on all objects:
1481 - list (list objects matching some condition)
1482 - show (display object details)
1483 - edit (edit object details)
1484 - create (create a new object)
1486 Each type may define actions specific to itself; these are listed in
1487 the help item about that type.
1489 For more information:
1491 - rt help <action> (action-specific details)
1492 - rt help types (a list of possible types)
1499 You can currently operate on the following types of objects:
1506 For more information:
1508 - rt help <type> (type-specific details)
1509 - rt help objects (how to specify objects)
1510 - rt help actions (a list of possible actions)
1517 Tickets are identified by a numeric ID.
1519 The following generic operations may be performed upon tickets:
1526 In addition, the following ticket-specific actions exist:
1535 The following attributes can be used with "rt show" or "rt edit"
1536 to retrieve or edit other information associated with tickets:
1538 links A ticket's relationships with others.
1539 history All of a ticket's transactions.
1540 history/type/<type> Only a particular type of transaction.
1541 history/id/<id> Only the transaction of the specified id.
1542 attachments A list of attachments.
1543 attachments/<id> The metadata for an individual attachment.
1544 attachments/<id>/content The content of an individual attachment.
1552 Users and groups are identified by name or numeric ID.
1554 The following generic operations may be performed upon them:
1561 In addition, the following type-specific actions exist:
1568 The following attributes can be used with "rt show" or "rt edit"
1569 to retrieve or edit other information associated with users and
1572 rights Global rights granted to this user.
1573 rights/<queue> Queue rights for this user.
1580 Queues are identified by name or numeric ID.
1582 Currently, they can be subjected to the following actions:
1597 Terminates the currently established login session. You will need to
1598 provide authentication credentials before you can continue using the
1599 server. (See "rt help config" for details about authentication.)
1610 rt <ls|list|search> [options] "query string"
1612 Displays a list of objects matching the specified conditions.
1613 ("ls", "list", and "search" are synonyms.)
1615 Conditions are expressed in the SQL-like syntax used internally by
1616 RT3. (For more information, see "rt help query".) The query string
1617 must be supplied as one argument.
1619 (Right now, the server doesn't support listing anything but tickets.
1620 Other types will be supported in future; this client will be able to
1621 take advantage of that support without any changes.)
1625 The following options control how much information is displayed
1626 about each matching object:
1628 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
1629 -s Short description.
1630 -l Longer description.
1634 -o +/-<field> Orders the returned list by the specified field.
1635 -S var=val Submits the specified variable with the request.
1636 -t type Specifies the type of object to look for. (The
1637 default is "ticket".)
1641 rt ls "Priority > 5 and Status='new'"
1642 rt ls -o +Subject "Priority > 5 and Status='new'"
1643 rt ls -o -Created "Priority > 5 and Status='new'"
1644 rt ls -i "Priority > 5"|rt edit - set status=resolved
1645 rt ls -t ticket "Subject like '[PATCH]%'"
1654 rt show [options] <object-ids>
1656 Displays details of the specified objects.
1658 For some types, object information is further classified into named
1659 attributes (for example, "1-3/links" is a valid ticket specification
1660 that refers to the links for tickets 1-3). Consult "rt help <type>"
1661 and "rt help objects" for further details.
1663 This command writes a set of forms representing the requested object
1668 - Read IDs from STDIN instead of the command-line.
1669 -t type Specifies object type.
1670 -f a,b,c Restrict the display to the specified fields.
1671 -S var=val Submits the specified variable with the request.
1675 rt show -t ticket -f id,subject,status 1-3
1676 rt show ticket/3/attachments/29
1677 rt show ticket/3/attachments/29/content
1678 rt show ticket/1-3/links
1690 rt edit [options] <object-ids> set field=value [field=value] ...
1691 add field=value [field=value] ...
1692 del field=value [field=value] ...
1694 Edits information corresponding to the specified objects.
1696 If, instead of "edit", an action of "new" or "create" is specified,
1697 then a new object is created. In this case, no numeric object IDs
1698 may be specified, but the syntax and behaviour remain otherwise
1701 This command typically starts an editor to allow you to edit object
1702 data in a form for submission. If you specified enough information
1703 on the command-line, however, it will make the submission directly.
1705 The command line may specify field-values in three different ways.
1706 "set" sets the named field to the given value, "add" adds a value
1707 to a multi-valued field, and "del" deletes the corresponding value.
1708 Each "field=value" specification must be given as a single argument.
1710 For some types, object information is further classified into named
1711 attributes (for example, "1-3/links" is a valid ticket specification
1712 that refers to the links for tickets 1-3). These attributes may also
1713 be edited. Consult "rt help <type>" and "rt help object" for further
1718 - Read numeric IDs from STDIN instead of the command-line.
1719 (Useful with rt ls ... | rt edit -; see examples below.)
1720 -i Read a completed form from STDIN before submitting.
1721 -o Dump the completed form to STDOUT instead of submitting.
1722 -e Allows you to edit the form even if the command-line has
1723 enough information to make a submission directly.
1725 Submits the specified variable with the request.
1726 -t type Specifies object type.
1730 # Interactive (starts $EDITOR with a form).
1735 rt edit ticket/1-3 add cc=foo@example.com set priority=3
1736 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1737 rt edit ticket/4 set priority=3 owner=bar@example.com \
1738 add cc=foo@example.com bcc=quux@example.net
1739 rt create -t ticket subject='new ticket' priority=10 \
1740 add cc=foo@example.com
1750 rt <comment|correspond> [options] <ticket-id>
1752 Adds a comment (or correspondence) to the specified ticket (the only
1753 difference being that comments aren't sent to the requestors.)
1755 This command will typically start an editor and allow you to type a
1756 comment into a form. If, however, you specified all the necessary
1757 information on the command line, it submits the comment directly.
1759 (See "rt help forms" for more information about forms.)
1763 -m <text> Specify comment text.
1764 -a <file> Attach a file to the comment. (May be used more
1765 than once to attach multiple files.)
1766 -c <addrs> A comma-separated list of Cc addresses.
1767 -b <addrs> A comma-separated list of Bcc addresses.
1768 -w <time> Specify the time spent working on this ticket.
1769 -e Starts an editor before the submission, even if
1770 arguments from the command line were sufficient.
1774 rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
1783 rt merge <from-id> <to-id>
1785 Merges the two specified tickets.
1794 rt link [-d] <id-A> <link> <id-B>
1796 Creates (or, with -d, deletes) a link between the specified tickets.
1797 The link can (irrespective of case) be any of:
1799 DependsOn/DependedOnBy: A depends upon B (or vice versa).
1800 RefersTo/ReferredToBy: A refers to B (or vice versa).
1801 MemberOf/HasMember: A is a member of B (or vice versa).
1803 To view a ticket's links, use "rt show ticket/3/links". (See
1804 "rt help ticket" and "rt help show".)
1808 -d Deletes the specified link.
1812 rt link 2 dependson 3
1813 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
1826 RT3 uses an SQL-like syntax to specify object selection constraints.
1827 See the <RT:...> documentation for details.
1829 (XXX: I'm going to have to write it, aren't I?)
1837 This program uses RFC822 header-style forms to represent object data
1838 in a form that's suitable for processing both by humans and scripts.
1840 A form is a set of (field, value) specifications, with some initial
1841 commented text and interspersed blank lines allowed for convenience.
1842 Field names may appear more than once in a form; a comma-separated
1843 list of multiple field values may also be specified directly.
1845 Field values can be wrapped as in RFC822, with leading whitespace.
1846 The longest sequence of leading whitespace common to all the lines
1847 is removed (preserving further indentation). There is no limit on
1848 the length of a value.
1850 Multiple forms are separated by a line containing only "--\n".
1852 (XXX: A more detailed specification will be provided soon. For now,
1853 the server-side syntax checking will suffice.)
1860 Use "rt help <topic>" for help on any of the following subjects:
1862 - tickets, users, groups, queues.
1863 - show, edit, ls/list/search, new/create.
1865 - query (search query syntax)
1866 - forms (form specification)
1868 - objects (how to specify objects)
1869 - types (a list of object types)
1870 - actions/commands (a list of actions)
1871 - usage/syntax (syntax details)
1872 - conf/config/configuration (configuration details)
1873 - examples (a few useful examples)
1881 This section will be filled in with useful examples, once it becomes
1882 more clear what examples may be useful.
1884 For the moment, please consult examples provided with each action.