4 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
6 # (Except where explictly superceded by other copyright notices)
8 # This work is made available to you under the terms of Version 2 of
9 # the GNU General Public License. A copy of that license should have
10 # been provided with this software, but in any event can be snarfed
13 # This work is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 # General Public License for more details.
18 # Unless otherwise specified, all modifications, corrections or
19 # extensions to this work which alter its source code become the
20 # property of Best Practical Solutions, LLC when submitted for
21 # inclusion in the work.
28 # This program is intentionally written to have as few non-core module
29 # dependencies as possible. It should stay that way.
33 use HTTP::Request::Common;
35 # We derive configuration information from hardwired defaults, dotfiles,
36 # and the RT* environment variables (in increasing order of precedence).
37 # Session information is stored in ~/.rt_sessions.
40 my $HOME = eval{(getpwuid($<))[7]}
41 || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH}
46 user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
48 server => 'http://localhost/rt/',
52 config_from_file($ENV{RTCONFIG} || ".rtrc"),
55 my $session = new Session("$HOME/.rt_sessions");
56 my $REST = "$config{server}/REST/1.0";
59 sub DEBUG { warn @_ if $config{debug} >= shift }
61 # These regexes are used by command handlers to parse arguments.
62 # (XXX: Ask Autrijus how i18n changes these definitions.)
65 my $field = '[a-zA-Z][a-zA-Z0-9_-]*';
66 my $label = '[a-zA-Z0-9@_.+-]+';
67 my $labels = "(?:$label,)*$label";
68 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
70 # Our command line looks like this:
72 # rt <action> [options] [arguments]
74 # We'll parse just enough of it to decide upon an action to perform, and
75 # leave the rest to per-action handlers to interpret appropriately.
78 # handler => [ ...aliases... ],
79 version => ["version", "ver"],
81 help => ["help", "man"],
82 show => ["show", "cat"],
83 edit => ["create", "edit", "new", "ed"],
84 list => ["search", "list", "ls"],
85 comment => ["comment", "correspond"],
86 link => ["link", "ln"],
88 grant => ["grant", "revoke"],
91 # Once we find and call an appropriate handler, we're done.
93 my (%actions, $action);
94 foreach my $fn (keys %handlers) {
95 foreach my $alias (@{ $handlers{$fn} }) {
96 $actions{$alias} = \&{"$fn"};
99 if (@ARGV && exists $actions{$ARGV[0]}) {
100 $action = shift @ARGV;
102 $actions{$action || "help"}->($action || ());
108 # The following subs are handlers for each entry in %actions.
111 print "rt $VERSION\n";
115 submit("$REST/logout") if defined $session->cookie;
119 my ($action, $type) = @_;
122 # What help topics do we know about?
124 foreach my $item (@{ Form::parse(<DATA>) }) {
125 my $title = $item->[2]{Title};
126 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
128 foreach $title (grep $_, @titles) {
129 $help{$title} = $item->[2]{Text};
133 # What does the user want help with?
134 undef $action if ($action && $actions{$action} eq \&help);
135 unless ($action || $type) {
136 # If we don't know, we'll look for clues in @ARGV.
138 if (exists $help{$_}) { $key = $_; last; }
141 # Tolerate possibly plural words.
143 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
148 if ($type && $action) {
149 $key = "$type.$action";
151 $key ||= $type || $action || "introduction";
153 # Find a suitable topic to display.
154 while (!exists $help{$key}) {
155 if ($type && $action) {
156 if ($key eq "$type.$action") { $key = $action; }
157 elsif ($key eq $action) { $key = $type; }
158 else { $key = "introduction"; }
161 $key = "introduction";
165 print STDERR $help{$key}, "\n\n";
168 # Displays a list of objects that match some specified condition.
171 my ($q, $type, %data, $orderby);
172 if ($config{orderby}) {
173 $data{orderby} = $config{orderby};
181 $bad = 1, last unless defined($type = get_type_argument());
184 $bad = 1, last unless get_var_argument(\%data);
187 $data{'orderby'} = shift @ARGV;
189 elsif (/^-([isl])$/) {
193 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
194 whine "No valid field list in '-f $ARGV[0]'.";
197 $data{fields} = shift @ARGV;
199 elsif (!defined $q && !/^-/) {
203 my $datum = /^-/ ? "option" : "argument";
204 whine "Unrecognised $datum '$_'.";
214 unless ($type && defined $q) {
215 my $item = $type ? "query string" : "object type";
216 whine "No $item specified.";
219 return help("list", $type) if $bad;
221 my $r = submit("$REST/search/$type", { query => $q, %data });
225 # Displays selected information about a single object.
228 my ($type, @objects, %data);
236 $bad = 1, last unless defined($type = get_type_argument());
239 $bad = 1, last unless get_var_argument(\%data);
241 elsif (/^-([isl])$/) {
244 elsif (/^-$/ && !$slurped) {
245 chomp(my @lines = <STDIN>);
247 unless (is_object_spec($_, $type)) {
248 whine "Invalid object on STDIN: '$_'.";
256 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
257 whine "No valid field list in '-f $ARGV[0]'.";
260 $data{fields} = shift @ARGV;
262 elsif (my $spec = is_object_spec($_, $type)) {
263 push @objects, $spec;
266 my $datum = /^-/ ? "option" : "argument";
267 whine "Unrecognised $datum '$_'.";
273 whine "No objects specified.";
276 return help("show", $type) if $bad;
278 my $r = submit("$REST/show", { id => \@objects, %data });
282 # To create a new object, we ask the server for a form with the defaults
283 # filled in, allow the user to edit it, and send the form back.
285 # To edit an object, we must ask the server for a form representing that
286 # object, make changes requested by the user (either on the command line
287 # or interactively via $EDITOR), and send the form back.
291 my (%data, $type, @objects);
292 my ($cl, $text, $edit, $input, $output);
294 use vars qw(%set %add %del);
295 %set = %add = %del = ();
302 if (/^-e$/) { $edit = 1 }
303 elsif (/^-i$/) { $input = 1 }
304 elsif (/^-o$/) { $output = 1 }
306 $bad = 1, last unless defined($type = get_type_argument());
309 $bad = 1, last unless get_var_argument(\%data);
311 elsif (/^-$/ && !($slurped || $input)) {
312 chomp(my @lines = <STDIN>);
314 unless (is_object_spec($_, $type)) {
315 whine "Invalid object on STDIN: '$_'.";
325 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
326 my ($key, $op, $val) = ($1, $2, $3);
327 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
329 vpush($hash, lc $key, $val);
334 whine "No variables to set.";
339 elsif (/^(?:add|del)$/i) {
341 my $hash = ($_ eq "add") ? \%add : \%del;
343 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
344 my ($key, $val) = ($1, $2);
346 vpush($hash, lc $key, $val);
351 whine "No variables to set.";
356 elsif (my $spec = is_object_spec($_, $type)) {
357 push @objects, $spec;
360 my $datum = /^-/ ? "option" : "argument";
361 whine "Unrecognised $datum '$_'.";
366 if ($action =~ /^ed(?:it)?$/) {
368 whine "No objects specified.";
374 whine "You shouldn't specify objects as arguments to $action.";
378 whine "What type of object do you want to create?";
381 @objects = ("$type/new");
383 return help($action, $type) if $bad;
385 # We need a form to make changes to. We usually ask the server for
386 # one, but we can avoid that if we are fed one on STDIN, or if the
387 # user doesn't want to edit the form by hand, and the command line
388 # specifies only simple variable assignments.
394 elsif ($edit || %add || %del || !$cl) {
395 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
399 # If any changes were specified on the command line, apply them.
402 # We're updating forms from the server.
403 my $forms = Form::parse($text);
405 foreach my $form (@$forms) {
406 my ($c, $o, $k, $e) = @$form;
409 next if ($e || !@$o);
415 # Make changes to existing fields.
417 if (exists $add{lc $key}) {
418 $val = delete $add{lc $key};
419 vpush($k, $key, $val);
420 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
422 if (exists $del{lc $key}) {
423 $val = delete $del{lc $key};
424 my %val = map {$_=>1} @{ vsplit($val) };
425 $k->{$key} = vsplit($k->{$key});
426 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
428 if (exists $set{lc $key}) {
429 $k->{$key} = delete $set{lc $key};
433 # Then update the others.
434 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
435 foreach $key (keys %add) {
436 vpush($k, $key, $add{$key});
437 $k->{$key} = vsplit($k->{$key});
439 push @$o, (keys %add, keys %set);
442 $text = Form::compose($forms);
445 # We're rolling our own set of forms.
448 my ($type, $ids, $args) =
449 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
452 foreach my $obj (expand_list($ids)) {
453 my %set = (%set, id => "$type/$obj$args");
454 push @forms, ["", [keys %set], \%set];
457 $text = Form::compose(\@forms);
469 # We'll let the user edit the form before sending it to the server,
470 # unless we have enough information to submit it non-interactively.
471 if ($edit || (!$input && !$cl)) {
472 my $newtext = vi($text);
473 # We won't resubmit a bad form unless it was changed.
474 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
478 my $r = submit("$REST/edit", {content => $text, %data});
479 if ($r->code == 409) {
480 # If we submitted a bad form, we'll give the user a chance
481 # to correct it and resubmit.
482 if ($edit || (!$input && !$cl)) {
496 # We roll "comment" and "correspond" into the same handler.
500 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
509 elsif (/^-[abcmw]$/) {
511 whine "No argument specified with $_.";
516 unless (-f $ARGV[0] && -r $ARGV[0]) {
517 whine "Cannot read attachment: '$ARGV[0]'.";
520 push @files, shift @ARGV;
523 my $a = $_ eq "-b" ? \@bcc : \@cc;
524 @$a = split /\s*,\s*/, shift @ARGV;
528 if ( $msg =~ /^-$/ ) {
530 while (<STDIN>) { $msg .= $_ }
534 elsif (/-w/) { $wtime = shift @ARGV }
536 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
540 my $datum = /^-/ ? "option" : "argument";
541 whine "Unrecognised $datum '$_'.";
547 whine "No object specified.";
550 return help($action, "ticket") if $bad;
554 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
560 Attachment => [ @files ],
561 TimeWorked => $wtime || '',
566 my $text = Form::compose([ $form ]);
568 if ($edit || !$msg) {
573 my $ntext = vi($text);
574 exit if ($error && $ntext eq $text);
576 $form = Form::parse($text);
579 ($c, $o, $k, $e) = @{ $form->[0] };
582 $c = "# Syntax error.";
588 @files = @{ vsplit($k->{Attachment}) };
591 $text = Form::compose([[$c, $o, $k, $e]]);
596 foreach my $file (@files) {
597 $data{"attachment_$i"} = bless([ $file ], "Attachment");
600 $data{content} = $text;
602 my $r = submit("$REST/ticket/comment/$id", \%data);
606 # Merge one ticket into another.
619 whine "Unrecognised argument: '$_'.";
625 my $evil = @id > 2 ? "many" : "few";
626 whine "Too $evil arguments specified.";
629 return help("merge", "ticket") if $bad;
631 my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
635 # Link one ticket to another.
638 my ($bad, $del, %data) = (0, 0, ());
639 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
640 ReferredToBy HasMember MemberOf);
642 while (@ARGV && $ARGV[0] =~ /^-/) {
649 whine "Unrecognised option: '$_'.";
655 my ($from, $rel, $to) = @ARGV;
656 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
657 my $bad = $from =~ /^\d+$/ ? $to : $from;
658 whine "Invalid ticket ID '$bad' specified.";
661 unless (exists $ltypes{lc $rel}) {
662 whine "Invalid relationship '$rel' specified.";
665 %data = (id => $from, rel => $rel, to => $to, del => $del);
668 my $bad = @ARGV < 3 ? "few" : "many";
669 whine "Too $bad arguments specified.";
672 return help("link", "ticket") if $bad;
674 my $r = submit("$REST/ticket/link", \%data);
678 # Grant/revoke a user's rights.
687 $revoke = 1 if $cmd->{action} eq 'revoke';
690 # Client <-> Server communication.
691 # --------------------------------
693 # This function composes and sends an HTTP request to the RT server, and
694 # interprets the response. It takes a request URI, and optional request
695 # data (a string, or a reference to a set of key-value pairs).
698 my ($uri, $content) = @_;
700 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
702 # Did the caller specify any data to send with the request?
704 if (defined $content) {
705 unless (ref $content) {
706 # If it's just a string, make sure LWP handles it properly.
707 # (By pretending that it's a file!)
708 $content = [ content => [undef, "", Content => $content] ];
710 elsif (ref $content eq 'HASH') {
712 foreach my $k (keys %$content) {
713 if (ref $content->{$k} eq 'ARRAY') {
714 foreach my $v (@{ $content->{$k} }) {
718 else { push @data, $k, $content->{$k} }
725 # Should we send authentication information to start a new session?
726 if (!defined $session->cookie) {
727 push @$data, ( user => $config{user} );
728 push @$data, ( pass => $config{passwd} || read_passwd() );
731 # Now, we construct the request.
733 $req = POST($uri, $data, Content_Type => 'form-data');
738 $session->add_cookie_header($req);
740 # Then we send the request and parse the response.
741 DEBUG(3, $req->as_string);
742 my $res = $ua->request($req);
743 DEBUG(3, $res->as_string);
745 if ($res->is_success) {
746 # The content of the response we get from the RT server consists
747 # of an HTTP-like status line followed by optional header lines,
748 # a blank line, and arbitrary text.
750 my ($head, $text) = split /\n\n/, $res->content, 2;
751 my ($status, @headers) = split /\n/, $head;
752 $text =~ s/\n*$/\n/ if ($text);
754 # "RT/3.0.1 401 Credentials required"
755 if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
756 warn "rt: Malformed RT response from $config{server}.\n";
757 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
761 # Our caller can pretend that the server returned a custom HTTP
762 # response code and message. (Doing that directly is apparently
763 # not sufficiently portable and uncomplicated.)
766 $res->content($text);
767 $session->update($res) if ($res->is_success || $res->code != 401);
769 if (!$res->is_success) {
770 # We can deal with authentication failures ourselves. Either
771 # we sent invalid credentials, or our session has expired.
772 if ($res->code == 401) {
774 if (exists $d{user}) {
775 warn "rt: Incorrect username or password.\n";
778 elsif ($req->header("Cookie")) {
779 # We'll retry the request with credentials, unless
780 # we only wanted to logout in the first place.
782 return submit(@_) unless $uri eq "$REST/logout";
785 # Conflicts should be dealt with by the handler and user.
786 # For anything else, we just die.
787 elsif ($res->code != 409) {
788 warn "rt: ", $res->content;
794 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
801 # Session management.
802 # -------------------
804 # Maintains a list of active sessions in the ~/.rt_sessions file.
809 # Initialises the session cache.
811 my ($class, $file) = @_;
813 file => $file || "$HOME/.rt_sessions",
817 # The current session is identified by the currently configured
819 ($s, $u) = @config{"server", "user"};
827 # Returns the current session cookie.
830 my $cookie = $self->{sids}{$s}{$u};
831 return defined $cookie ? "RT_SID_$cookie" : undef;
834 # Deletes the current session cookie.
837 delete $self->{sids}{$s}{$u};
840 # Adds a Cookie header to an outgoing HTTP request.
841 sub add_cookie_header {
842 my ($self, $request) = @_;
843 my $cookie = $self->cookie();
845 $request->header(Cookie => $cookie) if defined $cookie;
848 # Extracts the Set-Cookie header from an HTTP response, and updates
849 # session information accordingly.
851 my ($self, $response) = @_;
852 my $cookie = $response->header("Set-Cookie");
854 if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) {
855 $self->{sids}{$s}{$u} = $1;
859 # Loads the session cache from the specified file.
861 my ($self, $file) = @_;
862 $file ||= $self->{file};
865 open(F, $file) && do {
866 $self->{file} = $file;
867 my $sids = $self->{sids} = {};
870 next if /^$/ || /^#/;
871 next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
872 my ($server, $user, $cookie) = split / /, $_;
873 $sids->{$server}{$user} = $cookie;
880 # Writes the current session cache to the specified file.
882 my ($self, $file) = shift;
883 $file ||= $self->{file};
886 open(F, ">$file") && do {
887 my $sids = $self->{sids};
888 foreach my $server (keys %$sids) {
889 foreach my $user (keys %{ $sids->{$server} }) {
890 my $sid = $sids->{$server}{$user};
892 print F "$server $user $sid\n";
912 # Forms are RFC822-style sets of (field, value) specifications with some
913 # initial comments and interspersed blank lines allowed for convenience.
914 # Sets of forms are separated by --\n (in a cheap parody of MIME).
916 # Each form is parsed into an array with four elements: commented text
917 # at the start of the form, an array with the order of keys, a hash with
918 # key/value pairs, and optional error text if the form syntax was wrong.
920 # Returns a reference to an array of parsed forms.
924 my @lines = split /\n/, $_[0];
925 my ($c, $o, $k, $e) = ("", [], {}, "");
929 my $line = shift @lines;
931 next LINE if $line eq '';
934 # We reached the end of one form. We'll ignore it if it was
935 # empty, and store it otherwise, errors and all.
936 if ($e || $c || @$o) {
937 push @forms, [ $c, $o, $k, $e ];
938 $c = ""; $o = []; $k = {}; $e = "";
942 elsif ($state != -1) {
943 if ($state == 0 && $line =~ /^#/) {
944 # Read an optional block of comments (only) at the start
948 while (@lines && $lines[0] =~ /^#/) {
949 $c .= "\n".shift @lines;
953 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
954 # Read a field: value specification.
958 # Read continuation lines, if any.
959 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
960 push @v, shift @lines;
962 pop @v while (@v && $v[-1] eq '');
964 # Strip longest common leading indent from text.
966 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
967 $ws = $ls if (!$ws || length($ls) < length($ws));
971 push(@$o, $f) unless exists $k->{$f};
972 vpush($k, $f, join("\n", @v));
976 elsif ($line !~ /^#/) {
977 # We've found a syntax error, so we'll reconstruct the
978 # form parsed thus far, and add an error marker. (>>)
980 $e = Form::compose([[ "", $o, $k, "" ]]);
981 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
985 # We saw a syntax error earlier, so we'll accumulate the
986 # contents of this form until the end.
990 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
992 foreach my $l (keys %$k) {
993 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
999 # Returns text representing a set of forms.
1004 foreach my $form (@$forms) {
1005 my ($c, $o, $k, $e) = @$form;
1018 foreach my $key (@$o) {
1021 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1023 $sp = " "x(length("$key: "));
1024 $sp = " "x4 if length($sp) > 16;
1026 foreach $v (@values) {
1032 push @lines, "$line\n\n";
1035 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1038 push @lines, "$key: $v\n\n";
1041 length($line)+length($v)-rindex($line, "\n") >= 70)
1043 $line .= ",\n$sp$v";
1046 $line = $line ? "$line, $v" : "$key: $v";
1050 $line = "$key:" unless @values;
1052 if ($line =~ /\n/) {
1053 if (@lines && $lines[-1] !~ /\n\n$/) {
1058 push @lines, "$line\n";
1062 $text .= join "", @lines;
1070 return join "\n--\n\n", @text;
1076 # Returns configuration information from the environment.
1077 sub config_from_env {
1080 foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
1081 if (exists $ENV{"RT$k"}) {
1082 $env{lc $k} = $ENV{"RT$k"};
1089 # Finds a suitable configuration file and returns information from it.
1090 sub config_from_file {
1094 # We'll use an absolute path if we were given one.
1095 return parse_config_file($rc);
1098 # Otherwise we'll use the first file we can find in the current
1099 # directory, or in one of its (increasingly distant) ancestors.
1101 my @dirs = split /\//, cwd;
1103 my $file = join('/', @dirs, $rc);
1105 return parse_config_file($file);
1108 # Remove the last directory component each time.
1112 # Still nothing? We'll fall back to some likely defaults.
1113 for ("$HOME/$rc", "/etc/rt.conf") {
1114 return parse_config_file($_) if (-r $_);
1121 # Makes a hash of the specified configuration file.
1122 sub parse_config_file {
1126 open(CFG, $file) && do {
1129 next if (/^#/ || /^\s*$/);
1131 if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
1135 die "rt: $file:$.: unknown configuration directive.\n";
1147 my $sub = (caller(1))[3];
1148 $sub =~ s/^main:://;
1149 warn "rt: $sub: @_\n";
1154 eval 'require Term::ReadKey';
1156 die "No password specified (and Term::ReadKey not installed).\n";
1160 Term::ReadKey::ReadMode('noecho');
1161 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1162 Term::ReadKey::ReadMode('restore');
1170 my $file = "/tmp/rt.form.$$";
1171 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1176 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1177 system($editor, $file) && die "Couldn't run $editor.\n";
1178 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1184 # Add a value to a (possibly multi-valued) hash key.
1186 my ($hash, $key, $val) = @_;
1187 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1189 if (exists $hash->{$key}) {
1190 unless (ref $hash->{$key} eq 'ARRAY') {
1191 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1192 $hash->{$key} = \@v;
1194 push @{ $hash->{$key} }, @val;
1197 $hash->{$key} = $val;
1201 # "Normalise" a hash key that's known to be multi-valued.
1205 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1207 foreach my $line (map {split /\n/} @values) {
1208 # XXX: This should become a real parser, Ã la Text::ParseWords.
1211 push @words, split /\s*,\s*/, $line;
1219 my ($elt, @elts, %elts);
1221 foreach $elt (split /,/, $list) {
1222 if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
1223 else { push @elts, $elt }
1227 return sort {$a<=>$b} keys %elts;
1230 sub get_type_argument {
1234 $type = shift @ARGV;
1235 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1236 # We want whine to mention our caller, not us.
1237 @_ = ("Invalid type '$type' specified.");
1242 @_ = ("No type argument specified with -t.");
1246 $type =~ s/s$//; # "Plural". Ugh.
1250 sub get_var_argument {
1254 my $kv = shift @ARGV;
1255 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1256 push @{ $data->{$k} }, $v;
1259 @_ = ("Invalid variable specification: '$kv'.");
1264 @_ = ("No variable argument specified with -S.");
1269 sub is_object_spec {
1270 my ($spec, $type) = @_;
1272 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1273 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1283 ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1284 ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
1286 This is a command-line interface to RT 3.
1288 It allows you to interact with an RT server over HTTP, and offers an
1289 interface to RT's functionality that is better-suited to automation
1290 and integration with other tools.
1292 In general, each invocation of this program should specify an action
1293 to perform on one or more objects, and any other arguments required
1294 to complete the desired action.
1296 For more information:
1298 - rt help actions (a list of possible actions)
1299 - rt help objects (how to specify objects)
1300 - rt help usage (syntax information)
1302 - rt help config (configuration details)
1303 - rt help examples (a few useful examples)
1304 - rt help topics (a list of help topics)
1314 rt <action> [options] [arguments]
1316 Each invocation of this program must specify an action (e.g. "edit",
1317 "create"), options to modify behaviour, and other arguments required
1318 by the specified action. (For example, most actions expect a list of
1319 numeric object IDs to act upon.)
1321 The details of the syntax and arguments for each action are given by
1322 "rt help <action>". Some actions may be referred to by more than one
1323 name ("create" is the same as "new", for example).
1325 Objects are identified by a type and an ID (which can be a name or a
1326 number, depending on the type). For some actions, the object type is
1327 implied (you can only comment on tickets); for others, the user must
1328 specify it explicitly. See "rt help objects" for details.
1330 In syntax descriptions, mandatory arguments that must be replaced by
1331 appropriate value are enclosed in <>, and optional arguments are
1332 indicated by [] (for example, <action> and [options] above).
1334 For more information:
1336 - rt help objects (how to specify objects)
1337 - rt help actions (a list of actions)
1338 - rt help types (a list of object types)
1344 Title: configuration
1347 This program has two major sources of configuration information: its
1348 configuration files, and the environment.
1350 The program looks for configuration directives in a file named .rtrc
1351 (or $RTCONFIG; see below) in the current directory, and then in more
1352 distant ancestors, until it reaches /. If no suitable configuration
1353 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1355 Configuration directives:
1357 The following directives may occur, one per line:
1359 - server <URL> URL to RT server.
1360 - user <username> RT username.
1361 - passwd <passwd> RT user's password.
1362 - query <RT Query> Default RT Query for list action
1363 - orderby <order> Default RT order for list action
1365 Blank and #-commented lines are ignored.
1367 Environment variables:
1369 The following environment variables override any corresponding
1370 values defined in configuration files:
1375 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1376 - RTCONFIG Specifies a name other than ".rtrc" for the
1378 - RTQUERY Default RT Query for rt list
1379 - RTORDERBY Default order for rt list
1388 <type>/<id>[/<attributes>]
1390 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1391 ID. Some types of objects can also be identified by name (like users
1392 and queues). Furthermore, objects may have named attributes (such as
1393 "ticket/1/history").
1395 An object specification is like a path in a virtual filesystem, with
1396 object types as top-level directories, object IDs as subdirectories,
1397 and named attributes as further subdirectories.
1399 A comma-separated list of names, numeric IDs, or numeric ranges can
1400 be used to specify more than one object of the same type. Note that
1401 the list must be a single argument (i.e., no spaces). For example,
1402 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1403 can also be written as "user/ams,root,1,2,3,5,7,8-10".
1408 ticket/1/attachments
1409 ticket/1/attachments/3
1410 ticket/1/attachments/3/content
1412 ticket/1-3,5-7/history
1416 user/ams,rai,1/rights
1418 For more information:
1420 - rt help <action> (action-specific details)
1421 - rt help <type> (type-specific details)
1429 You can currently perform the following actions on all objects:
1431 - list (list objects matching some condition)
1432 - show (display object details)
1433 - edit (edit object details)
1434 - create (create a new object)
1436 Each type may define actions specific to itself; these are listed in
1437 the help item about that type.
1439 For more information:
1441 - rt help <action> (action-specific details)
1442 - rt help types (a list of possible types)
1449 You can currently operate on the following types of objects:
1456 For more information:
1458 - rt help <type> (type-specific details)
1459 - rt help objects (how to specify objects)
1460 - rt help actions (a list of possible actions)
1467 Tickets are identified by a numeric ID.
1469 The following generic operations may be performed upon tickets:
1476 In addition, the following ticket-specific actions exist:
1485 The following attributes can be used with "rt show" or "rt edit"
1486 to retrieve or edit other information associated with tickets:
1488 links A ticket's relationships with others.
1489 history All of a ticket's transactions.
1490 history/type/<type> Only a particular type of transaction.
1491 history/id/<id> Only the transaction of the specified id.
1492 attachments A list of attachments.
1493 attachments/<id> The metadata for an individual attachment.
1494 attachments/<id>/content The content of an individual attachment.
1502 Users and groups are identified by name or numeric ID.
1504 The following generic operations may be performed upon them:
1511 In addition, the following type-specific actions exist:
1518 The following attributes can be used with "rt show" or "rt edit"
1519 to retrieve or edit other information associated with users and
1522 rights Global rights granted to this user.
1523 rights/<queue> Queue rights for this user.
1530 Queues are identified by name or numeric ID.
1532 Currently, they can be subjected to the following actions:
1547 Terminates the currently established login session. You will need to
1548 provide authentication credentials before you can continue using the
1549 server. (See "rt help config" for details about authentication.)
1560 rt <ls|list|search> [options] "query string"
1562 Displays a list of objects matching the specified conditions.
1563 ("ls", "list", and "search" are synonyms.)
1565 Conditions are expressed in the SQL-like syntax used internally by
1566 RT3. (For more information, see "rt help query".) The query string
1567 must be supplied as one argument.
1569 (Right now, the server doesn't support listing anything but tickets.
1570 Other types will be supported in future; this client will be able to
1571 take advantage of that support without any changes.)
1575 The following options control how much information is displayed
1576 about each matching object:
1578 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
1579 -s Short description.
1580 -l Longer description.
1584 -o +/-<field> Orders the returned list by the specified field.
1585 -S var=val Submits the specified variable with the request.
1586 -t type Specifies the type of object to look for. (The
1587 default is "ticket".)
1591 rt ls "Priority > 5 and Status='new'"
1592 rt ls -o +Subject "Priority > 5 and Status='new'"
1593 rt ls -o -Created "Priority > 5 and Status='new'"
1594 rt ls -i "Priority > 5"|rt edit - set status=resolved
1595 rt ls -t ticket "Subject like '[PATCH]%'"
1604 rt show [options] <object-ids>
1606 Displays details of the specified objects.
1608 For some types, object information is further classified into named
1609 attributes (for example, "1-3/links" is a valid ticket specification
1610 that refers to the links for tickets 1-3). Consult "rt help <type>"
1611 and "rt help objects" for further details.
1613 This command writes a set of forms representing the requested object
1618 - Read IDs from STDIN instead of the command-line.
1619 -t type Specifies object type.
1620 -f a,b,c Restrict the display to the specified fields.
1621 -S var=val Submits the specified variable with the request.
1625 rt show -t ticket -f id,subject,status 1-3
1626 rt show ticket/3/attachments/29
1627 rt show ticket/3/attachments/29/content
1628 rt show ticket/1-3/links
1640 rt edit [options] <object-ids> set field=value [field=value] ...
1641 add field=value [field=value] ...
1642 del field=value [field=value] ...
1644 Edits information corresponding to the specified objects.
1646 If, instead of "edit", an action of "new" or "create" is specified,
1647 then a new object is created. In this case, no numeric object IDs
1648 may be specified, but the syntax and behaviour remain otherwise
1651 This command typically starts an editor to allow you to edit object
1652 data in a form for submission. If you specified enough information
1653 on the command-line, however, it will make the submission directly.
1655 The command line may specify field-values in three different ways.
1656 "set" sets the named field to the given value, "add" adds a value
1657 to a multi-valued field, and "del" deletes the corresponding value.
1658 Each "field=value" specification must be given as a single argument.
1660 For some types, object information is further classified into named
1661 attributes (for example, "1-3/links" is a valid ticket specification
1662 that refers to the links for tickets 1-3). These attributes may also
1663 be edited. Consult "rt help <type>" and "rt help object" for further
1668 - Read numeric IDs from STDIN instead of the command-line.
1669 (Useful with rt ls ... | rt edit -; see examples below.)
1670 -i Read a completed form from STDIN before submitting.
1671 -o Dump the completed form to STDOUT instead of submitting.
1672 -e Allows you to edit the form even if the command-line has
1673 enough information to make a submission directly.
1675 Submits the specified variable with the request.
1676 -t type Specifies object type.
1680 # Interactive (starts $EDITOR with a form).
1685 rt edit ticket/1-3 add cc=foo@example.com set priority=3
1686 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1687 rt edit ticket/4 set priority=3 owner=bar@example.com \
1688 add cc=foo@example.com bcc=quux@example.net
1689 rt create -t ticket subject='new ticket' priority=10 \
1690 add cc=foo@example.com
1700 rt <comment|correspond> [options] <ticket-id>
1702 Adds a comment (or correspondence) to the specified ticket (the only
1703 difference being that comments aren't sent to the requestors.)
1705 This command will typically start an editor and allow you to type a
1706 comment into a form. If, however, you specified all the necessary
1707 information on the command line, it submits the comment directly.
1709 (See "rt help forms" for more information about forms.)
1713 -m <text> Specify comment text.
1714 -a <file> Attach a file to the comment. (May be used more
1715 than once to attach multiple files.)
1716 -c <addrs> A comma-separated list of Cc addresses.
1717 -b <addrs> A comma-separated list of Bcc addresses.
1718 -w <time> Specify the time spent working on this ticket.
1719 -e Starts an editor before the submission, even if
1720 arguments from the command line were sufficient.
1724 rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
1733 rt merge <from-id> <to-id>
1735 Merges the two specified tickets.
1744 rt link [-d] <id-A> <relationship> <id-B>
1746 Creates (or, with -d, deletes) a link between the specified tickets.
1747 The relationship can (irrespective of case) be any of:
1749 DependsOn/DependedOnBy: A depends upon B (or vice versa).
1750 RefersTo/ReferredToBy: A refers to B (or vice versa).
1751 MemberOf/HasMember: A is a member of B (or vice versa).
1753 To view a ticket's relationships, use "rt show ticket/3/links". (See
1754 "rt help ticket" and "rt help show".)
1758 -d Deletes the specified link.
1762 rt link 2 dependson 3
1763 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
1776 RT3 uses an SQL-like syntax to specify object selection constraints.
1777 See the <RT:...> documentation for details.
1779 (XXX: I'm going to have to write it, aren't I?)
1787 This program uses RFC822 header-style forms to represent object data
1788 in a form that's suitable for processing both by humans and scripts.
1790 A form is a set of (field, value) specifications, with some initial
1791 commented text and interspersed blank lines allowed for convenience.
1792 Field names may appear more than once in a form; a comma-separated
1793 list of multiple field values may also be specified directly.
1795 Field values can be wrapped as in RFC822, with leading whitespace.
1796 The longest sequence of leading whitespace common to all the lines
1797 is removed (preserving further indentation). There is no limit on
1798 the length of a value.
1800 Multiple forms are separated by a line containing only "--\n".
1802 (XXX: A more detailed specification will be provided soon. For now,
1803 the server-side syntax checking will suffice.)
1810 Use "rt help <topic>" for help on any of the following subjects:
1812 - tickets, users, groups, queues.
1813 - show, edit, ls/list/search, new/create.
1815 - query (search query syntax)
1816 - forms (form specification)
1818 - objects (how to specify objects)
1819 - types (a list of object types)
1820 - actions/commands (a list of actions)
1821 - usage/syntax (syntax details)
1822 - conf/config/configuration (configuration details)
1823 - examples (a few useful examples)
1831 This section will be filled in with useful examples, once it becomes
1832 more clear what examples may be useful.
1834 For the moment, please consult examples provided with each action.