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/',
50 config_from_file($ENV{RTCONFIG} || ".rtrc"),
53 my $session = new Session("$HOME/.rt_sessions");
54 my $REST = "$config{server}/REST/1.0";
57 sub DEBUG { warn @_ if $config{debug} >= shift }
59 # These regexes are used by command handlers to parse arguments.
60 # (XXX: Ask Autrijus how i18n changes these definitions.)
63 my $field = '[a-zA-Z][a-zA-Z0-9_-]*';
64 my $label = '[a-zA-Z0-9@_.+-]+';
65 my $labels = "(?:$label,)*$label";
66 my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
68 # Our command line looks like this:
70 # rt <action> [options] [arguments]
72 # We'll parse just enough of it to decide upon an action to perform, and
73 # leave the rest to per-action handlers to interpret appropriately.
76 # handler => [ ...aliases... ],
77 version => ["version", "ver"],
79 help => ["help", "man"],
80 show => ["show", "cat"],
81 edit => ["create", "edit", "new", "ed"],
82 list => ["search", "list", "ls"],
83 comment => ["comment", "correspond"],
84 link => ["link", "ln"],
86 grant => ["grant", "revoke"],
89 # Once we find and call an appropriate handler, we're done.
91 my (%actions, $action);
92 foreach my $fn (keys %handlers) {
93 foreach my $alias (@{ $handlers{$fn} }) {
94 $actions{$alias} = \&{"$fn"};
97 if (@ARGV && exists $actions{$ARGV[0]}) {
98 $action = shift @ARGV;
100 $actions{$action || "help"}->($action || ());
106 # The following subs are handlers for each entry in %actions.
109 print "rt $VERSION\n";
113 submit("$REST/logout") if defined $session->cookie;
117 my ($action, $type) = @_;
120 # What help topics do we know about?
122 foreach my $item (@{ Form::parse(<DATA>) }) {
123 my $title = $item->[2]{Title};
124 my @titles = ref $title eq 'ARRAY' ? @$title : $title;
126 foreach $title (grep $_, @titles) {
127 $help{$title} = $item->[2]{Text};
131 # What does the user want help with?
132 undef $action if ($action && $actions{$action} eq \&help);
133 unless ($action || $type) {
134 # If we don't know, we'll look for clues in @ARGV.
136 if (exists $help{$_}) { $key = $_; last; }
139 # Tolerate possibly plural words.
141 if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; }
146 if ($type && $action) {
147 $key = "$type.$action";
149 $key ||= $type || $action || "introduction";
151 # Find a suitable topic to display.
152 while (!exists $help{$key}) {
153 if ($type && $action) {
154 if ($key eq "$type.$action") { $key = $action; }
155 elsif ($key eq $action) { $key = $type; }
156 else { $key = "introduction"; }
159 $key = "introduction";
163 print STDERR $help{$key}, "\n\n";
166 # Displays a list of objects that match some specified condition.
169 my ($q, $type, %data, $orderby);
176 $bad = 1, last unless defined($type = get_type_argument());
179 $bad = 1, last unless get_var_argument(\%data);
182 $orderby = shift @ARGV;
184 elsif (/^-([isl])$/) {
188 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
189 whine "No valid field list in '-f $ARGV[0]'.";
192 $data{fields} = shift @ARGV;
194 elsif (!defined $q && !/^-/) {
198 my $datum = /^-/ ? "option" : "argument";
199 whine "Unrecognised $datum '$_'.";
205 unless ($type && defined $q) {
206 my $item = $type ? "query string" : "object type";
207 whine "No $item specified.";
210 return help("list", $type) if $bad;
212 my $r = submit("$REST/search/$type", { query => $q, %data, orderby => $orderby || "" });
216 # Displays selected information about a single object.
219 my ($type, @objects, %data);
227 $bad = 1, last unless defined($type = get_type_argument());
230 $bad = 1, last unless get_var_argument(\%data);
232 elsif (/^-([isl])$/) {
235 elsif (/^-$/ && !$slurped) {
236 chomp(my @lines = <STDIN>);
238 unless (is_object_spec($_, $type)) {
239 whine "Invalid object on STDIN: '$_'.";
247 if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
248 whine "No valid field list in '-f $ARGV[0]'.";
251 $data{fields} = shift @ARGV;
253 elsif (my $spec = is_object_spec($_, $type)) {
254 push @objects, $spec;
257 my $datum = /^-/ ? "option" : "argument";
258 whine "Unrecognised $datum '$_'.";
264 whine "No objects specified.";
267 return help("show", $type) if $bad;
269 my $r = submit("$REST/show", { id => \@objects, %data });
273 # To create a new object, we ask the server for a form with the defaults
274 # filled in, allow the user to edit it, and send the form back.
276 # To edit an object, we must ask the server for a form representing that
277 # object, make changes requested by the user (either on the command line
278 # or interactively via $EDITOR), and send the form back.
282 my (%data, $type, @objects);
283 my ($cl, $text, $edit, $input, $output);
285 use vars qw(%set %add %del);
286 %set = %add = %del = ();
293 if (/^-e$/) { $edit = 1 }
294 elsif (/^-i$/) { $input = 1 }
295 elsif (/^-o$/) { $output = 1 }
297 $bad = 1, last unless defined($type = get_type_argument());
300 $bad = 1, last unless get_var_argument(\%data);
302 elsif (/^-$/ && !($slurped || $input)) {
303 chomp(my @lines = <STDIN>);
305 unless (is_object_spec($_, $type)) {
306 whine "Invalid object on STDIN: '$_'.";
316 while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
317 my ($key, $op, $val) = ($1, $2, $3);
318 my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
320 vpush($hash, lc $key, $val);
325 whine "No variables to set.";
330 elsif (/^(?:add|del)$/i) {
332 my $hash = ($_ eq "add") ? \%add : \%del;
334 while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
335 my ($key, $val) = ($1, $2);
337 vpush($hash, lc $key, $val);
342 whine "No variables to set.";
347 elsif (my $spec = is_object_spec($_, $type)) {
348 push @objects, $spec;
351 my $datum = /^-/ ? "option" : "argument";
352 whine "Unrecognised $datum '$_'.";
357 if ($action =~ /^ed(?:it)?$/) {
359 whine "No objects specified.";
365 whine "You shouldn't specify objects as arguments to $action.";
369 whine "What type of object do you want to create?";
372 @objects = ("$type/new");
374 return help($action, $type) if $bad;
376 # We need a form to make changes to. We usually ask the server for
377 # one, but we can avoid that if we are fed one on STDIN, or if the
378 # user doesn't want to edit the form by hand, and the command line
379 # specifies only simple variable assignments.
385 elsif ($edit || %add || %del || !$cl) {
386 my $r = submit("$REST/show", { id => \@objects, format => 'l' });
390 # If any changes were specified on the command line, apply them.
393 # We're updating forms from the server.
394 my $forms = Form::parse($text);
396 foreach my $form (@$forms) {
397 my ($c, $o, $k, $e) = @$form;
400 next if ($e || !@$o);
406 # Make changes to existing fields.
408 if (exists $add{lc $key}) {
409 $val = delete $add{lc $key};
410 vpush($k, $key, $val);
411 $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/;
413 if (exists $del{lc $key}) {
414 $val = delete $del{lc $key};
415 my %val = map {$_=>1} @{ vsplit($val) };
416 $k->{$key} = vsplit($k->{$key});
417 @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}};
419 if (exists $set{lc $key}) {
420 $k->{$key} = delete $set{lc $key};
424 # Then update the others.
425 foreach $key (keys %set) { vpush($k, $key, $set{$key}) }
426 foreach $key (keys %add) {
427 vpush($k, $key, $add{$key});
428 $k->{$key} = vsplit($k->{$key});
430 push @$o, (keys %add, keys %set);
433 $text = Form::compose($forms);
436 # We're rolling our own set of forms.
439 my ($type, $ids, $args) =
440 m{^($name)/($idlist|$labels)(?:(/.*))?$}o;
443 foreach my $obj (expand_list($ids)) {
444 my %set = (%set, id => "$type/$obj$args");
445 push @forms, ["", [keys %set], \%set];
448 $text = Form::compose(\@forms);
460 # We'll let the user edit the form before sending it to the server,
461 # unless we have enough information to submit it non-interactively.
462 if ($edit || (!$input && !$cl)) {
463 my $newtext = vi($text);
464 # We won't resubmit a bad form unless it was changed.
465 $text = ($synerr && $newtext eq $text) ? undef : $newtext;
469 my $r = submit("$REST/edit", {content => $text, %data});
470 if ($r->code == 409) {
471 # If we submitted a bad form, we'll give the user a chance
472 # to correct it and resubmit.
473 if ($edit || (!$input && !$cl)) {
487 # We roll "comment" and "correspond" into the same handler.
491 my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
500 elsif (/^-[abcmw]$/) {
502 whine "No argument specified with $_.";
507 unless (-f $ARGV[0] && -r $ARGV[0]) {
508 whine "Cannot read attachment: '$ARGV[0]'.";
511 push @files, shift @ARGV;
514 my $a = $_ eq "-b" ? \@bcc : \@cc;
515 @$a = split /\s*,\s*/, shift @ARGV;
517 elsif (/-m/) { $msg = shift @ARGV }
518 elsif (/-w/) { $wtime = shift @ARGV }
520 elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
524 my $datum = /^-/ ? "option" : "argument";
525 whine "Unrecognised $datum '$_'.";
531 whine "No object specified.";
534 return help($action, "ticket") if $bad;
538 [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
544 Attachment => [ @files ],
545 TimeWorked => $wtime || '',
550 my $text = Form::compose([ $form ]);
552 if ($edit || !$msg) {
557 my $ntext = vi($text);
558 exit if ($error && $ntext eq $text);
560 $form = Form::parse($text);
563 ($c, $o, $k, $e) = @{ $form->[0] };
566 $c = "# Syntax error.";
572 @files = @{ vsplit($k->{Attachment}) };
575 $text = Form::compose([[$c, $o, $k, $e]]);
580 foreach my $file (@files) {
581 $data{"attachment_$i"} = bless([ $file ], "Attachment");
584 $data{content} = $text;
586 my $r = submit("$REST/ticket/comment/$id", \%data);
590 # Merge one ticket into another.
603 whine "Unrecognised argument: '$_'.";
609 my $evil = @id > 2 ? "many" : "few";
610 whine "Too $evil arguments specified.";
613 return help("merge", "ticket") if $bad;
615 my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
619 # Link one ticket to another.
622 my ($bad, $del, %data) = (0, 0, ());
623 my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
624 ReferredToBy HasMember MemberOf);
626 while (@ARGV && $ARGV[0] =~ /^-/) {
633 whine "Unrecognised option: '$_'.";
639 my ($from, $rel, $to) = @ARGV;
640 if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
641 my $bad = $from =~ /^\d+$/ ? $to : $from;
642 whine "Invalid ticket ID '$bad' specified.";
645 unless (exists $ltypes{lc $rel}) {
646 whine "Invalid relationship '$rel' specified.";
649 %data = (id => $from, rel => $rel, to => $to, del => $del);
652 my $bad = @ARGV < 3 ? "few" : "many";
653 whine "Too $bad arguments specified.";
656 return help("link", "ticket") if $bad;
658 my $r = submit("$REST/ticket/link", \%data);
662 # Grant/revoke a user's rights.
671 $revoke = 1 if $cmd->{action} eq 'revoke';
674 # Client <-> Server communication.
675 # --------------------------------
677 # This function composes and sends an HTTP request to the RT server, and
678 # interprets the response. It takes a request URI, and optional request
679 # data (a string, or a reference to a set of key-value pairs).
682 my ($uri, $content) = @_;
684 my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
686 # Did the caller specify any data to send with the request?
688 if (defined $content) {
689 unless (ref $content) {
690 # If it's just a string, make sure LWP handles it properly.
691 # (By pretending that it's a file!)
692 $content = [ content => [undef, "", Content => $content] ];
694 elsif (ref $content eq 'HASH') {
696 foreach my $k (keys %$content) {
697 if (ref $content->{$k} eq 'ARRAY') {
698 foreach my $v (@{ $content->{$k} }) {
702 else { push @data, $k, $content->{$k} }
709 # Should we send authentication information to start a new session?
710 if (!defined $session->cookie) {
711 push @$data, ( user => $config{user} );
712 push @$data, ( pass => $config{passwd} || read_passwd() );
715 # Now, we construct the request.
717 $req = POST($uri, $data, Content_Type => 'form-data');
722 $session->add_cookie_header($req);
724 # Then we send the request and parse the response.
725 DEBUG(3, $req->as_string);
726 my $res = $ua->request($req);
727 DEBUG(3, $res->as_string);
729 if ($res->is_success) {
730 # The content of the response we get from the RT server consists
731 # of an HTTP-like status line followed by optional header lines,
732 # a blank line, and arbitrary text.
734 my ($head, $text) = split /\n\n/, $res->content, 2;
735 my ($status, @headers) = split /\n/, $head;
738 # "RT/3.0.1 401 Credentials required"
739 if ($status !~ m#^RT/\d+(?:\.\d+)+(?:-?\w+)? (\d+) ([\w\s]+)$#) {
740 warn "rt: Malformed RT response from $config{server}.\n";
741 warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
745 # Our caller can pretend that the server returned a custom HTTP
746 # response code and message. (Doing that directly is apparently
747 # not sufficiently portable and uncomplicated.)
750 $res->content($text);
751 $session->update($res) if ($res->is_success || $res->code != 401);
753 if (!$res->is_success) {
754 # We can deal with authentication failures ourselves. Either
755 # we sent invalid credentials, or our session has expired.
756 if ($res->code == 401) {
758 if (exists $d{user}) {
759 warn "rt: Incorrect username or password.\n";
762 elsif ($req->header("Cookie")) {
763 # We'll retry the request with credentials, unless
764 # we only wanted to logout in the first place.
766 return submit(@_) unless $uri eq "$REST/logout";
769 # Conflicts should be dealt with by the handler and user.
770 # For anything else, we just die.
771 elsif ($res->code != 409) {
772 warn "rt: ", $res->content;
778 warn "rt: Server error: ", $res->message, " (", $res->code, ")\n";
785 # Session management.
786 # -------------------
788 # Maintains a list of active sessions in the ~/.rt_sessions file.
793 # Initialises the session cache.
795 my ($class, $file) = @_;
797 file => $file || "$HOME/.rt_sessions",
801 # The current session is identified by the currently configured
803 ($s, $u) = @config{"server", "user"};
811 # Returns the current session cookie.
814 my $cookie = $self->{sids}{$s}{$u};
815 return defined $cookie ? "RT_SID=$cookie" : undef;
818 # Deletes the current session cookie.
821 delete $self->{sids}{$s}{$u};
824 # Adds a Cookie header to an outgoing HTTP request.
825 sub add_cookie_header {
826 my ($self, $request) = @_;
827 my $cookie = $self->cookie();
829 $request->header(Cookie => $cookie) if defined $cookie;
832 # Extracts the Set-Cookie header from an HTTP response, and updates
833 # session information accordingly.
835 my ($self, $response) = @_;
836 my $cookie = $response->header("Set-Cookie");
838 if (defined $cookie && $cookie =~ /^RT_SID=([0-9A-Fa-f]+);/) {
839 $self->{sids}{$s}{$u} = $1;
843 # Loads the session cache from the specified file.
845 my ($self, $file) = @_;
846 $file ||= $self->{file};
849 open(F, $file) && do {
850 $self->{file} = $file;
851 my $sids = $self->{sids} = {};
854 next if /^$/ || /^#/;
855 next unless m#^https?://[^ ]+ \w+ [0-9A-Fa-f]+$#;
856 my ($server, $user, $cookie) = split / /, $_;
857 $sids->{$server}{$user} = $cookie;
864 # Writes the current session cache to the specified file.
866 my ($self, $file) = shift;
867 $file ||= $self->{file};
870 open(F, ">$file") && do {
871 my $sids = $self->{sids};
872 foreach my $server (keys %$sids) {
873 foreach my $user (keys %{ $sids->{$server} }) {
874 my $sid = $sids->{$server}{$user};
876 print F "$server $user $sid\n";
896 # Forms are RFC822-style sets of (field, value) specifications with some
897 # initial comments and interspersed blank lines allowed for convenience.
898 # Sets of forms are separated by --\n (in a cheap parody of MIME).
900 # Each form is parsed into an array with four elements: commented text
901 # at the start of the form, an array with the order of keys, a hash with
902 # key/value pairs, and optional error text if the form syntax was wrong.
904 # Returns a reference to an array of parsed forms.
908 my @lines = split /\n/, $_[0];
909 my ($c, $o, $k, $e) = ("", [], {}, "");
913 my $line = shift @lines;
915 next LINE if $line eq '';
918 # We reached the end of one form. We'll ignore it if it was
919 # empty, and store it otherwise, errors and all.
920 if ($e || $c || @$o) {
921 push @forms, [ $c, $o, $k, $e ];
922 $c = ""; $o = []; $k = {}; $e = "";
926 elsif ($state != -1) {
927 if ($state == 0 && $line =~ /^#/) {
928 # Read an optional block of comments (only) at the start
932 while (@lines && $lines[0] =~ /^#/) {
933 $c .= "\n".shift @lines;
937 elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) {
938 # Read a field: value specification.
942 # Read continuation lines, if any.
943 while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) {
944 push @v, shift @lines;
946 pop @v while (@v && $v[-1] eq '');
948 # Strip longest common leading indent from text.
950 foreach my $ls (map {/^(\s+)/} @v[1..$#v]) {
951 $ws = $ls if (!$ws || length($ls) < length($ws));
955 push(@$o, $f) unless exists $k->{$f};
956 vpush($k, $f, join("\n", @v));
960 elsif ($line !~ /^#/) {
961 # We've found a syntax error, so we'll reconstruct the
962 # form parsed thus far, and add an error marker. (>>)
964 $e = Form::compose([[ "", $o, $k, "" ]]);
965 $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n";
969 # We saw a syntax error earlier, so we'll accumulate the
970 # contents of this form until the end.
974 push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o);
976 foreach my $l (keys %$k) {
977 $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY');
983 # Returns text representing a set of forms.
988 foreach my $form (@$forms) {
989 my ($c, $o, $k, $e) = @$form;
1002 foreach my $key (@$o) {
1005 my @values = ref $v eq 'ARRAY' ? @$v : $v;
1007 $sp = " "x(length("$key: "));
1008 $sp = " "x4 if length($sp) > 16;
1010 foreach $v (@values) {
1016 push @lines, "$line\n\n";
1019 elsif (@lines && $lines[-1] !~ /\n\n$/) {
1022 push @lines, "$key: $v\n\n";
1025 length($line)+length($v)-rindex($line, "\n") >= 70)
1027 $line .= ",\n$sp$v";
1030 $line = $line ? "$line, $v" : "$key: $v";
1034 $line = "$key:" unless @values;
1036 if ($line =~ /\n/) {
1037 if (@lines && $lines[-1] !~ /\n\n$/) {
1042 push @lines, "$line\n";
1046 $text .= join "", @lines;
1054 return join "\n--\n\n", @text;
1060 # Returns configuration information from the environment.
1061 sub config_from_env {
1064 foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER") {
1065 if (exists $ENV{"RT$k"}) {
1066 $env{lc $k} = $ENV{"RT$k"};
1073 # Finds a suitable configuration file and returns information from it.
1074 sub config_from_file {
1078 # We'll use an absolute path if we were given one.
1079 return parse_config_file($rc);
1082 # Otherwise we'll use the first file we can find in the current
1083 # directory, or in one of its (increasingly distant) ancestors.
1085 my @dirs = split /\//, cwd;
1087 my $file = join('/', @dirs, $rc);
1089 return parse_config_file($file);
1092 # Remove the last directory component each time.
1096 # Still nothing? We'll fall back to some likely defaults.
1097 for ("$HOME/$rc", "/etc/rt.conf") {
1098 return parse_config_file($_) if (-r $_);
1105 # Makes a hash of the specified configuration file.
1106 sub parse_config_file {
1110 open(CFG, $file) && do {
1113 next if (/^#/ || /^\s*$/);
1115 if (/^(user|passwd|server)\s+([^ ]+)$/) {
1119 die "rt: $file:$.: unknown configuration directive.\n";
1131 my $sub = (caller(1))[3];
1132 $sub =~ s/^main:://;
1133 warn "rt: $sub: @_\n";
1138 eval 'require Term::ReadKey';
1140 die "No password specified (and Term::ReadKey not installed).\n";
1144 Term::ReadKey::ReadMode('noecho');
1145 chomp(my $passwd = Term::ReadKey::ReadLine(0));
1146 Term::ReadKey::ReadMode('restore');
1154 my $file = "/tmp/rt.form.$$";
1155 my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
1160 open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
1161 system($editor, $file) && die "Couldn't run $editor.\n";
1162 open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
1168 # Add a value to a (possibly multi-valued) hash key.
1170 my ($hash, $key, $val) = @_;
1171 my @val = ref $val eq 'ARRAY' ? @$val : $val;
1173 if (exists $hash->{$key}) {
1174 unless (ref $hash->{$key} eq 'ARRAY') {
1175 my @v = $hash->{$key} ne '' ? $hash->{$key} : ();
1176 $hash->{$key} = \@v;
1178 push @{ $hash->{$key} }, @val;
1181 $hash->{$key} = $val;
1185 # "Normalise" a hash key that's known to be multi-valued.
1189 my @values = ref $val eq 'ARRAY' ? @$val : $val;
1191 foreach my $line (map {split /\n/} @values) {
1192 # XXX: This should become a real parser, Ã la Text::ParseWords.
1195 push @words, split /\s*,\s*/, $line;
1203 my ($elt, @elts, %elts);
1205 foreach $elt (split /,/, $list) {
1206 if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
1207 else { push @elts, $elt }
1211 return sort {$a<=>$b} keys %elts;
1214 sub get_type_argument {
1218 $type = shift @ARGV;
1219 unless ($type =~ /^[A-Za-z0-9_.-]+$/) {
1220 # We want whine to mention our caller, not us.
1221 @_ = ("Invalid type '$type' specified.");
1226 @_ = ("No type argument specified with -t.");
1230 $type =~ s/s$//; # "Plural". Ugh.
1234 sub get_var_argument {
1238 my $kv = shift @ARGV;
1239 if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) {
1240 push @{ $data->{$k} }, $v;
1243 @_ = ("Invalid variable specification: '$kv'.");
1248 @_ = ("No variable argument specified with -S.");
1253 sub is_object_spec {
1254 my ($spec, $type) = @_;
1256 $spec =~ s|^(?:$type/)?|$type/| if defined $type;
1257 return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
1267 ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
1268 ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
1270 This is a command-line interface to RT 3.
1272 It allows you to interact with an RT server over HTTP, and offers an
1273 interface to RT's functionality that is better-suited to automation
1274 and integration with other tools.
1276 In general, each invocation of this program should specify an action
1277 to perform on one or more objects, and any other arguments required
1278 to complete the desired action.
1280 For more information:
1282 - rt help actions (a list of possible actions)
1283 - rt help objects (how to specify objects)
1284 - rt help usage (syntax information)
1286 - rt help config (configuration details)
1287 - rt help examples (a few useful examples)
1288 - rt help topics (a list of help topics)
1298 rt <action> [options] [arguments]
1300 Each invocation of this program must specify an action (e.g. "edit",
1301 "create"), options to modify behaviour, and other arguments required
1302 by the specified action. (For example, most actions expect a list of
1303 numeric object IDs to act upon.)
1305 The details of the syntax and arguments for each action are given by
1306 "rt help <action>". Some actions may be referred to by more than one
1307 name ("create" is the same as "new", for example).
1309 Objects are identified by a type and an ID (which can be a name or a
1310 number, depending on the type). For some actions, the object type is
1311 implied (you can only comment on tickets); for others, the user must
1312 specify it explicitly. See "rt help objects" for details.
1314 In syntax descriptions, mandatory arguments that must be replaced by
1315 appropriate value are enclosed in <>, and optional arguments are
1316 indicated by [] (for example, <action> and [options] above).
1318 For more information:
1320 - rt help objects (how to specify objects)
1321 - rt help actions (a list of actions)
1322 - rt help types (a list of object types)
1328 Title: configuration
1331 This program has two major sources of configuration information: its
1332 configuration files, and the environment.
1334 The program looks for configuration directives in a file named .rtrc
1335 (or $RTCONFIG; see below) in the current directory, and then in more
1336 distant ancestors, until it reaches /. If no suitable configuration
1337 files are found, it will also check for ~/.rtrc and /etc/rt.conf.
1339 Configuration directives:
1341 The following directives may occur, one per line:
1343 - server <URL> URL to RT server.
1344 - user <username> RT username.
1345 - passwd <passwd> RT user's password.
1347 Blank and #-commented lines are ignored.
1349 Environment variables:
1351 The following environment variables override any corresponding
1352 values defined in configuration files:
1357 - RTDEBUG Numeric debug level. (Set to 3 for full logs.)
1358 - RTCONFIG Specifies a name other than ".rtrc" for the
1368 <type>/<id>[/<attributes>]
1370 Every object in RT has a type (e.g. "ticket", "queue") and a numeric
1371 ID. Some types of objects can also be identified by name (like users
1372 and queues). Furthermore, objects may have named attributes (such as
1373 "ticket/1/history").
1375 An object specification is like a path in a virtual filesystem, with
1376 object types as top-level directories, object IDs as subdirectories,
1377 and named attributes as further subdirectories.
1379 A comma-separated list of names, numeric IDs, or numeric ranges can
1380 be used to specify more than one object of the same type. Note that
1381 the list must be a single argument (i.e., no spaces). For example,
1382 "user/root,1-3,5,7-10,ams" is a list of ten users; the same list
1383 can also be written as "user/ams,root,1,2,3,5,7,8-20".
1388 ticket/1/attachments
1389 ticket/1/attachments/3
1390 ticket/1/attachments/3/content
1392 ticket/1-3,5-7/history
1396 user/ams,rai,1/rights
1398 For more information:
1400 - rt help <action> (action-specific details)
1401 - rt help <type> (type-specific details)
1409 You can currently perform the following actions on all objects:
1411 - list (list objects matching some condition)
1412 - show (display object details)
1413 - edit (edit object details)
1414 - create (create a new object)
1416 Each type may define actions specific to itself; these are listed in
1417 the help item about that type.
1419 For more information:
1421 - rt help <action> (action-specific details)
1422 - rt help types (a list of possible types)
1429 You can currently operate on the following types of objects:
1436 For more information:
1438 - rt help <type> (type-specific details)
1439 - rt help objects (how to specify objects)
1440 - rt help actions (a list of possible actions)
1447 Tickets are identified by a numeric ID.
1449 The following generic operations may be performed upon tickets:
1456 In addition, the following ticket-specific actions exist:
1465 The following attributes can be used with "rt show" or "rt edit"
1466 to retrieve or edit other information associated with tickets:
1468 links A ticket's relationships with others.
1469 history All of a ticket's transactions.
1470 history/type/<type> Only a particular type of transaction.
1471 history/id/<id> Only the transaction of the specified id.
1472 attachments A list of attachments.
1473 attachments/<id> The metadata for an individual attachment.
1474 attachments/<id>/content The content of an individual attachment.
1482 Users and groups are identified by name or numeric ID.
1484 The following generic operations may be performed upon them:
1491 In addition, the following type-specific actions exist:
1498 The following attributes can be used with "rt show" or "rt edit"
1499 to retrieve or edit other information associated with users and
1502 rights Global rights granted to this user.
1503 rights/<queue> Queue rights for this user.
1510 Queues are identified by name or numeric ID.
1512 Currently, they can be subjected to the following actions:
1527 Terminates the currently established login session. You will need to
1528 provide authentication credentials before you can continue using the
1529 server. (See "rt help config" for details about authentication.)
1540 rt <ls|list|search> [options] "query string"
1542 Displays a list of objects matching the specified conditions.
1543 ("ls", "list", and "search" are synonyms.)
1545 Conditions are expressed in the SQL-like syntax used internally by
1546 RT3. (For more information, see "rt help query".) The query string
1547 must be supplied as one argument.
1549 (Right now, the server doesn't support listing anything but tickets.
1550 Other types will be supported in future; this client will be able to
1551 take advantage of that support without any changes.)
1555 The following options control how much information is displayed
1556 about each matching object:
1558 -i Numeric IDs only. (Useful for |rt edit -; see examples.)
1559 -s Short description.
1560 -l Longer description.
1564 -o +/-<field> Orders the returned list by the specified field.
1565 -S var=val Submits the specified variable with the request.
1566 -t type Specifies the type of object to look for. (The
1567 default is "ticket".)
1571 rt ls "Priority > 5 and Status='new'"
1572 rt ls -o +Subject "Priority > 5 and Status='new'"
1573 rt ls -o -Created "Priority > 5 and Status='new'"
1574 rt ls -i "Priority > 5"|rt edit - set status=resolved
1575 rt ls -t ticket "Subject like '[PATCH]%'"
1584 rt show [options] <object-ids>
1586 Displays details of the specified objects.
1588 For some types, object information is further classified into named
1589 attributes (for example, "1-3/links" is a valid ticket specification
1590 that refers to the links for tickets 1-3). Consult "rt help <type>"
1591 and "rt help objects" for further details.
1593 This command writes a set of forms representing the requested object
1598 - Read IDs from STDIN instead of the command-line.
1599 -t type Specifies object type.
1600 -f a,b,c Restrict the display to the specified fields.
1601 -S var=val Submits the specified variable with the request.
1605 rt show -t ticket -f id,subject,status 1-3
1606 rt show ticket/3/attachments/29
1607 rt show ticket/3/attachments/29/content
1608 rt show ticket/1-3/links
1620 rt edit [options] <object-ids> set field=value [field=value] ...
1621 add field=value [field=value] ...
1622 del field=value [field=value] ...
1624 Edits information corresponding to the specified objects.
1626 If, instead of "edit", an action of "new" or "create" is specified,
1627 then a new object is created. In this case, no numeric object IDs
1628 may be specified, but the syntax and behaviour remain otherwise
1631 This command typically starts an editor to allow you to edit object
1632 data in a form for submission. If you specified enough information
1633 on the command-line, however, it will make the submission directly.
1635 The command line may specify field-values in three different ways.
1636 "set" sets the named field to the given value, "add" adds a value
1637 to a multi-valued field, and "del" deletes the corresponding value.
1638 Each "field=value" specification must be given as a single argument.
1640 For some types, object information is further classified into named
1641 attributes (for example, "1-3/links" is a valid ticket specification
1642 that refers to the links for tickets 1-3). These attributes may also
1643 be edited. Consult "rt help <type>" and "rt help object" for further
1648 - Read numeric IDs from STDIN instead of the command-line.
1649 (Useful with rt ls ... | rt edit -; see examples below.)
1650 -i Read a completed form from STDIN before submitting.
1651 -o Dump the completed form to STDOUT instead of submitting.
1652 -e Allows you to edit the form even if the command-line has
1653 enough information to make a submission directly.
1655 Submits the specified variable with the request.
1656 -t type Specifies object type.
1660 # Interactive (starts $EDITOR with a form).
1665 rt edit ticket/1-3 add cc=foo@example.com set priority=3
1666 rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
1667 rt edit ticket/4 set priority=3 owner=bar@example.com \
1668 add cc=foo@example.com bcc=quux@example.net
1669 rt create -t ticket subject='new ticket' priority=10 \
1670 add cc=foo@example.com
1680 rt <comment|correspond> [options] <ticket-id>
1682 Adds a comment (or correspondence) to the specified ticket (the only
1683 difference being that comments aren't sent to the requestors.)
1685 This command will typically start an editor and allow you to type a
1686 comment into a form. If, however, you specified all the necessary
1687 information on the command line, it submits the comment directly.
1689 (See "rt help forms" for more information about forms.)
1693 -m <text> Specify comment text.
1694 -a <file> Attach a file to the comment. (May be used more
1695 than once to attach multiple files.)
1696 -c <addrs> A comma-separated list of Cc addresses.
1697 -b <addrs> A comma-separated list of Bcc addresses.
1698 -w <time> Specify the time spent working on this ticket.
1699 -e Starts an editor before the submission, even if
1700 arguments from the command line were sufficient.
1704 rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
1713 rt merge <from-id> <to-id>
1715 Merges the two specified tickets.
1724 rt link [-d] <id-A> <relationship> <id-B>
1726 Creates (or, with -d, deletes) a link between the specified tickets.
1727 The relationship can (irrespective of case) be any of:
1729 DependsOn/DependedOnBy: A depends upon B (or vice versa).
1730 RefersTo/ReferredToBy: A refers to B (or vice versa).
1731 MemberOf/HasMember: A is a member of B (or vice versa).
1733 To view a ticket's relationships, use "rt show ticket/3/links". (See
1734 "rt help ticket" and "rt help show".)
1738 -d Deletes the specified link.
1742 rt link 2 dependson 3
1743 rt link -d 4 referredtoby 6 # 6 no longer refers to 4
1756 RT3 uses an SQL-like syntax to specify object selection constraints.
1757 See the <RT:...> documentation for details.
1759 (XXX: I'm going to have to write it, aren't I?)
1767 This program uses RFC822 header-style forms to represent object data
1768 in a form that's suitable for processing both by humans and scripts.
1770 A form is a set of (field, value) specifications, with some initial
1771 commented text and interspersed blank lines allowed for convenience.
1772 Field names may appear more than once in a form; a comma-separated
1773 list of multiple field values may also be specified directly.
1775 Field values can be wrapped as in RFC822, with leading whitespace.
1776 The longest sequence of leading whitespace common to all the lines
1777 is removed (preserving further indentation). There is no limit on
1778 the length of a value.
1780 Multiple forms are separated by a line containing only "--\n".
1782 (XXX: A more detailed specification will be provided soon. For now,
1783 the server-side syntax checking will suffice.)
1790 Use "rt help <topic>" for help on any of the following subjects:
1792 - tickets, users, groups, queues.
1793 - show, edit, ls/list/search, new/create.
1795 - query (search query syntax)
1796 - forms (form specification)
1798 - objects (how to specify objects)
1799 - types (a list of object types)
1800 - actions/commands (a list of actions)
1801 - usage/syntax (syntax details)
1802 - conf/config/configuration (configuration details)
1803 - examples (a few useful examples)
1811 This section will be filled in with useful examples, once it becomes
1812 more clear what examples may be useful.
1814 For the moment, please consult examples provided with each action.