#!@PERL@ -w
-# BEGIN LICENSE BLOCK
+# BEGIN BPS TAGGED BLOCK {{{
#
-# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# COPYRIGHT:
#
-# (Except where explictly superceded by other copyright notices)
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
-# Unless otherwise specified, all modifications, corrections or
-# extensions to this work which alter its source code become the
-# property of Best Practical Solutions, LLC when submitted for
-# inclusion in the work.
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
#
+# CONTRIBUTION SUBMISSION POLICY:
#
-# END LICENSE BLOCK
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+# Designed and implemented for Best Practical Solutions, LLC by
+# Abhijit Menon-Sen <ams@wiw.org>
use strict;
use Cwd;
use LWP;
+use Text::ParseWords;
use HTTP::Request::Common;
+use HTTP::Headers;
+use Term::ReadLine;
+use Time::Local; # used in prettyshow
+
+# strong (GSSAPI based) authentication is supported if the server does provide
+# it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed
+# it can be suppressed by setting externalauth=0 (default is undef)
+eval { require GSSAPI };
+my $no_strong_auth = 'missing perl module GSSAPI';
+if ( ! $@ ) {
+ eval {require LWP::Authen::Negotiate};
+ $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0;
+}
# We derive configuration information from hardwired defaults, dotfiles,
# and the RT* environment variables (in increasing order of precedence).
|| ".";
my %config = (
(
- debug => 0,
- user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
- passwd => undef,
- server => 'http://localhost/rt/',
- query => undef,
- orderby => undef,
+ debug => 0,
+ user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME},
+ passwd => undef,
+ server => 'http://localhost/',
+ query => "Status!='resolved' and Status!='rejected'",
+ orderby => 'id',
+ queue => undef,
+# to protect against unlimited searches a better choice would be
+# queue => 'Unknown_Queue',
+# setting externalauth => undef will try GSSAPI auth if the corresponding perl
+# modules are installed, externalauth => 0 is the backward compatible choice
+ externalauth => 0,
),
config_from_file($ENV{RTCONFIG} || ".rtrc"),
config_from_env()
);
my $session = new Session("$HOME/.rt_sessions");
my $REST = "$config{server}/REST/1.0";
+$no_strong_auth = 'switched off by externalauth=0'
+ if defined $config{externalauth};
+
+
+my $prompt = 'rt> ';
sub whine;
sub DEBUG { warn @_ if $config{debug} >= shift }
# These regexes are used by command handlers to parse arguments.
# (XXX: Ask Autrijus how i18n changes these definitions.)
-my $name = '[\w.-]+';
-my $field = '[a-zA-Z][a-zA-Z0-9_-]*';
-my $label = '[a-zA-Z0-9@_.+-]+';
-my $labels = "(?:$label,)*$label";
-my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
+my $name = '[\w.-]+';
+my $CF_name = '[\sa-z0-9_ :()/-]+';
+my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
+my $label = '[a-zA-Z0-9@_.+-]+';
+my $labels = "(?:$label,)*$label";
+my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
# Our command line looks like this:
#
my %handlers = (
# handler => [ ...aliases... ],
version => ["version", "ver"],
+ shell => ["shell"],
logout => ["logout"],
help => ["help", "man"],
show => ["show", "cat"],
link => ["link", "ln"],
merge => ["merge"],
grant => ["grant", "revoke"],
+ take => ["take", "steal", "untake"],
+ quit => ["quit", "exit"],
+ setcommand => ["del", "delete", "give", "res", "resolve",
+ "subject"],
);
-# Once we find and call an appropriate handler, we're done.
-
-my (%actions, $action);
+my %actions;
foreach my $fn (keys %handlers) {
foreach my $alias (@{ $handlers{$fn} }) {
$actions{$alias} = \&{"$fn"};
}
}
-if (@ARGV && exists $actions{$ARGV[0]}) {
- $action = shift @ARGV;
+
+# Once we find and call an appropriate handler, we're done.
+
+sub handler {
+ my $action;
+
+ push @ARGV, 'shell' if (!@ARGV); # default to shell mode
+ shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt'
+ if (@ARGV && exists $actions{$ARGV[0]}) {
+ $action = shift @ARGV;
+ return $actions{$action}->($action);
+ }
+ else {
+ print STDERR "rt: Unknown command '@ARGV'.\n";
+ print STDERR "rt: For help, run 'rt help'.\n";
+ return 1;
+ }
}
-$actions{$action || "help"}->($action || ());
-exit;
+
+exit handler();
# Handler functions.
# ------------------
#
# The following subs are handlers for each entry in %actions.
+sub shell {
+ $|=1;
+ my $term = new Term::ReadLine 'RT CLI';
+ while ( defined ($_ = $term->readline($prompt)) ) {
+ next if /^#/ || /^\s*$/;
+
+ @ARGV = shellwords($_);
+ handler();
+ }
+}
+
sub version {
print "rt $VERSION\n";
+ return 0;
}
sub logout {
submit("$REST/logout") if defined $session->cookie;
+ return 0;
}
+sub quit {
+ logout();
+ exit;
+}
+
+my %help;
sub help {
- my ($action, $type) = @_;
- my (%help, $key);
+ my ($action, $type, $rv) = @_;
+ $rv = defined $rv ? $rv : 0;
+ my $key;
# What help topics do we know about?
- local $/ = undef;
- foreach my $item (@{ Form::parse(<DATA>) }) {
- my $title = $item->[2]{Title};
- my @titles = ref $title eq 'ARRAY' ? @$title : $title;
+ if (!%help) {
+ local $/ = undef;
+ foreach my $item (@{ Form::parse(<DATA>) }) {
+ my $title = $item->[2]{Title};
+ my @titles = ref $title eq 'ARRAY' ? @$title : $title;
foreach $title (grep $_, @titles) {
$help{$title} = $item->[2]{Text};
}
+ }
}
# What does the user want help with?
}
print STDERR $help{$key}, "\n\n";
+ return $rv;
}
# Displays a list of objects that match some specified condition.
sub list {
- my ($q, $type, %data, $orderby);
+ my ($q, $type, %data);
+ my $orderby = $config{orderby};
+
if ($config{orderby}) {
$data{orderby} = $config{orderby};
}
my $bad = 0;
+ my $rawprint = 0;
+ my $reverse_sort = 0;
+ my $queue = $config{queue};
while (@ARGV) {
$_ = shift @ARGV;
}
elsif (/^-([isl])$/) {
$data{format} = $1;
+ $rawprint = 1;
+ }
+ elsif (/^-q$/) {
+ $queue = shift @ARGV;
+ }
+ elsif (/^-r$/) {
+ $reverse_sort = 1;
}
elsif (/^-f$/) {
if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) {
$bad = 1; last;
}
$data{fields} = shift @ARGV;
+ $data{format} = 's' if ! $data{format};
+ $rawprint = 1;
}
elsif (!defined $q && !/^-/) {
$q = $_;
$bad = 1; last;
}
}
-
+ if ( ! $rawprint and ! exists $data{format} ) {
+ $data{format} = 'l';
+ }
+ if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
+ $data{orderby} =~ s/^-/+/;
+ } elsif ($reverse_sort) {
+ $data{orderby} =~ s/^\+?(.*)/-$1/;
+ }
+
if (!defined $q) {
$q = $config{query};
}
+ $q =~ s/^#//; # get rid of leading hash
+ if ($q =~ /^\d+$/) {
+ # only digits, must be an id, formulate a correct query
+ $q = "id=$q" if $q =~ /^\d+$/;
+ } else {
+ # a string only, take it as an owner or requestor (quoting done later)
+ $q = "(Owner=$q or Requestor like $q) and $config{query}"
+ if $q =~ /^[\w\-]+$/;
+ # always add a query for a specific queue or (comma separated) queues
+ $queue =~ s/,/ or Queue=/g if $queue;
+ $q .= " and (Queue=$queue)" if $queue and $q and $q !~ /Queue\s*=/i
+ and $q !~ /id\s*=/i;
+ }
+ # correctly quote strings in a query
+ $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
+
$type ||= "ticket";
unless ($type && defined $q) {
my $item = $type ? "query string" : "object type";
whine "No $item specified.";
$bad = 1;
}
- return help("list", $type) if $bad;
+ #return help("list", $type) if $bad;
+ return suggest_help("list", $type, $bad) if $bad;
+ print "Query:$q\n" if ! $rawprint;
my $r = submit("$REST/search/$type", { query => $q, %data });
- print $r->content;
+ if ( $rawprint ) {
+ print $r->content;
+ } else {
+ my $forms = Form::parse($r->content);
+ prettylist ($forms);
+ }
+ return 0;
}
# Displays selected information about a single object.
my ($type, @objects, %data);
my $slurped = 0;
my $bad = 0;
+ my $rawprint = 0;
+ my $histspec;
while (@ARGV) {
$_ = shift @ARGV;
-
+ s/^#// if /^#\d+/; # get rid of leading hash
if (/^-t$/) {
$bad = 1, last unless defined($type = get_type_argument());
}
}
elsif (/^-([isl])$/) {
$data{format} = $1;
+ $rawprint = 1;
}
elsif (/^-$/ && !$slurped) {
chomp(my @lines = <STDIN>);
$bad = 1; last;
}
$data{fields} = shift @ARGV;
+ # option f requires short raw listing format
+ $data{format} = 's';
+ $rawprint = 1;
+ }
+ elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
+ push @objects, $spc2;
+ $histspec = is_object_spec("ticket/$_/history", $type);
+ }
+ elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) {
+ push @objects, $spc3;
+ $rawprint = 1 if $_ =~ /\/content$/;
}
elsif (my $spec = is_object_spec($_, $type)) {
push @objects, $spec;
+ $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
}
else {
my $datum = /^-/ ? "option" : "argument";
$bad = 1; last;
}
}
+ if ( ! $rawprint ) {
+ push @objects, $histspec if $histspec;
+ $data{format} = 'l' if ! exists $data{format};
+ }
unless (@objects) {
whine "No objects specified.";
$bad = 1;
}
- return help("show", $type) if $bad;
+ #return help("show", $type) if $bad;
+ return suggest_help("show", $type, $bad) if $bad;
my $r = submit("$REST/show", { id => \@objects, %data });
- print $r->content;
+ my $c = $r->content;
+ # if this isn't a text reply, remove the trailing newline so we
+ # don't corrupt things like tarballs when people do
+ # show ticket/id/attachments/id/content > foo.tar.gz
+ if ($r->content_type !~ /^text\//) {
+ chomp($c);
+ $rawprint = 1;
+ }
+ if ( $rawprint ) {
+ print $c;
+ } else {
+ # I do not know how to get more than one form correctly returned
+ $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg;
+ my $forms = Form::parse($c);
+ prettyshow ($forms);
+ }
+ return 0;
}
# To create a new object, we ask the server for a form with the defaults
while (@ARGV) {
$_ = shift @ARGV;
+ s/^#// if /^#\d+/; # get rid of leading hash
if (/^-e$/) { $edit = 1 }
elsif (/^-i$/) { $input = 1 }
elsif (/^set$/i) {
my $vars = 0;
- while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/) {
+ while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) {
my ($key, $op, $val) = ($1, $2, $3);
my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del;
my $vars = 0;
my $hash = ($_ eq "add") ? \%add : \%del;
- while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/) {
+ while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) {
my ($key, $val) = ($1, $2);
vpush($hash, lc $key, $val);
}
$cl = $vars;
}
+ elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) {
+ push @objects, $spc2;
+ }
elsif (my $spec = is_object_spec($_, $type)) {
push @objects, $spec;
}
whine "What type of object do you want to create?";
$bad = 1;
}
- @objects = ("$type/new");
+ @objects = ("$type/new") if defined($type);
}
- return help($action, $type) if $bad;
+ #return help($action, $type) if $bad;
+ return suggest_help($action, $type, $bad) if $bad;
# We need a form to make changes to. We usually ask the server for
# one, but we can avoid that if we are fed one on STDIN, or if the
# user doesn't want to edit the form by hand, and the command line
- # specifies only simple variable assignments.
+ # specifies only simple variable assignments. We *should* get a
+ # form if we're creating a new ticket, so that the default values
+ # get filled in properly.
+
+ my @new_objects = grep /\/new$/, @objects;
if ($input) {
local $/ = undef;
$text = <STDIN>;
}
- elsif ($edit || %add || %del || !$cl) {
+ elsif ($edit || %add || %del || !$cl || @new_objects) {
my $r = submit("$REST/show", { id => \@objects, format => 'l' });
$text = $r->content;
}
if ($output) {
print $text;
- exit;
+ return 0;
}
my $synerr = 0;
}
else {
print $r->content;
- exit -1;
+ return 0;
}
}
print $r->content;
}
+ return 0;
+}
+
+# handler for special edit commands. A valid edit command is constructed and
+# further work is delegated to the edit handler
+
+sub setcommand {
+ my ($action) = @_;
+ my ($id, $bad, $what);
+ if ( @ARGV ) {
+ $_ = shift @ARGV;
+ $id = $1 if (m|^(?:ticket/)?($idlist)$|);
+ }
+ if ( ! $id ) {
+ $bad = 1;
+ whine "No ticket number specified.";
+ }
+ if ( @ARGV ) {
+ if ($action eq 'subject') {
+ my $subject = '"'.join (" ", @ARGV).'"';
+ @ARGV = ();
+ $what = "subject=$subject";
+ } elsif ($action eq 'give') {
+ my $owner = shift @ARGV;
+ $what = "owner=$owner";
+ }
+ } else {
+ if ( $action eq 'delete' or $action eq 'del' ) {
+ $what = "status=deleted";
+ } elsif ($action eq 'resolve' or $action eq 'res' ) {
+ $what = "status=resolved";
+ } elsif ($action eq 'take' ) {
+ $what = "owner=$config{user}";
+ } elsif ($action eq 'untake') {
+ $what = "owner=Nobody";
+ }
+ }
+ if (@ARGV) {
+ $bad = 1;
+ whine "Extraneous arguments for action $action: @ARGV.";
+ }
+ if ( ! $what ) {
+ $bad = 1;
+ whine "unrecognized action $action.";
+ }
+ return help("edit", undef, $bad) if $bad;
+ @ARGV = ( $id, "set", $what );
+ print "Executing: rt edit @ARGV\n";
+ return edit("edit");
}
# We roll "comment" and "correspond" into the same handler.
if (/-a/) {
unless (-f $ARGV[0] && -r $ARGV[0]) {
whine "Cannot read attachment: '$ARGV[0]'.";
- exit -1;
+ return 0;
}
push @files, shift @ARGV;
}
whine "No object specified.";
$bad = 1;
}
- return help($action, "ticket") if $bad;
+ #return help($action, "ticket") if $bad;
+ return suggest_help($action, "ticket") if $bad;
my $form = [
"",
Attachment => [ @files ],
TimeWorked => $wtime || '',
Text => $msg || '',
+ Status => ''
}
];
do {
my $ntext = vi($text);
- exit if ($error && $ntext eq $text);
+ return if ($error && $ntext eq $text);
$text = $ntext;
$form = Form::parse($text);
$error = 0;
goto NEXT;
}
elsif (!@$o) {
- exit;
+ return 0;
}
@files = @{ vsplit($k->{Attachment}) };
}
$data{content} = $text;
- my $r = submit("$REST/ticket/comment/$id", \%data);
+ my $r = submit("$REST/ticket/$id/comment", \%data);
print $r->content;
+ return 0;
}
# Merge one ticket into another.
while (@ARGV) {
$_ = shift @ARGV;
+ s/^#// if /^#\d+/; # get rid of leading hash
if (/^\d+$/) {
push @id, $_;
whine "Too $evil arguments specified.";
$bad = 1;
}
- return help("merge", "ticket") if $bad;
+ #return help("merge", "ticket") if $bad;
+ return suggest_help("merge", "ticket", $bad) if $bad;
- my $r = submit("$REST/ticket/merge/$id[0]", {into => $id[1]});
+ my $r = submit("$REST/ticket/$id[0]/merge/$id[1]");
print $r->content;
+ return 0;
}
# Link one ticket to another.
sub link {
my ($bad, $del, %data) = (0, 0, ());
+ my $type;
+
my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo
ReferredToBy HasMember MemberOf);
if (/^-d$/) {
$del = 1;
}
+ elsif (/^-t$/) {
+ $bad = 1, last unless defined($type = get_type_argument());
+ }
else {
whine "Unrecognised option: '$_'.";
$bad = 1; last;
}
}
-
+
+ $type = "ticket" unless $type; # default type to tickets
+
if (@ARGV == 3) {
my ($from, $rel, $to) = @ARGV;
if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
my $bad = $from =~ /^\d+$/ ? $to : $from;
- whine "Invalid ticket ID '$bad' specified.";
+ whine "Invalid $type ID '$bad' specified.";
$bad = 1;
}
- unless (exists $ltypes{lc $rel}) {
- whine "Invalid relationship '$rel' specified.";
+ if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
+ whine "Invalid link '$rel' for type $type specified.";
$bad = 1;
}
%data = (id => $from, rel => $rel, to => $to, del => $del);
whine "Too $bad arguments specified.";
$bad = 1;
}
- return help("link", "ticket") if $bad;
+ return suggest_help("link", $type, $bad) if $bad;
+
+ my $r = submit("$REST/$type/link", \%data);
+ print $r->content;
+ return 0;
+}
+
+# Take/steal a ticket
+sub take {
+ my ($cmd) = @_;
+ my ($bad, %data) = (0, ());
+
+ my $id;
+
+ # get the ticket id
+ if (@ARGV == 1) {
+ ($id) = @ARGV;
+ unless ($id =~ /^\d+$/) {
+ whine "Invalid ticket ID $id specified.";
+ $bad = 1;
+ }
+ my $form = [
+ "",
+ [ "Ticket", "Action" ],
+ {
+ Ticket => $id,
+ Action => $cmd,
+ Status => '',
+ }
+ ];
+
+ my $text = Form::compose([ $form ]);
+ $data{content} = $text;
+ }
+ else {
+ $bad = @ARGV < 1 ? "few" : "many";
+ whine "Too $bad arguments specified.";
+ $bad = 1;
+ }
+ return suggest_help("take", "ticket", $bad) if $bad;
- my $r = submit("$REST/ticket/link", \%data);
+ my $r = submit("$REST/ticket/$id/take", \%data);
print $r->content;
+ return 0;
}
# Grant/revoke a user's rights.
}
$revoke = 1 if $cmd->{action} eq 'revoke';
+ return 0;
}
# Client <-> Server communication.
my ($uri, $content) = @_;
my ($req, $data);
my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+ my $h = HTTP::Headers->new;
# Did the caller specify any data to send with the request?
$data = [];
}
# Should we send authentication information to start a new session?
- if (!defined $session->cookie) {
- push @$data, ( user => $config{user} );
- push @$data, ( pass => $config{passwd} || read_passwd() );
+ my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
+ (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
+ if ($config{externalauth}) {
+ $h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
+ print " Password will be sent to $server $how\n",
+ " Press CTRL-C now if you do not want to continue\n"
+ if ! $config{passwd};
+ } elsif ( $no_strong_auth ) {
+ if (!defined $session->cookie) {
+ print " Strong encryption not available, $no_strong_auth\n",
+ " Password will be sent to $server $how\n",
+ " Press CTRL-C now if you do not want to continue\n"
+ if ! $config{passwd};
+ push @$data, ( user => $config{user} );
+ push @$data, ( pass => $config{passwd} || read_passwd() );
+ }
}
# Now, we construct the request.
$req = GET($uri);
}
$session->add_cookie_header($req);
+ if ($config{externalauth}) {
+ $req->header(%$h);
+ }
# Then we send the request and parse the response.
DEBUG(3, $req->as_string);
$text =~ s/\n*$/\n/ if ($text);
# "RT/3.0.1 401 Credentials required"
- if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
+ if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
warn "rt: Malformed RT response from $config{server}.\n";
warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
exit -1;
# For anything else, we just die.
elsif ($res->code != 409) {
warn "rt: ", $res->content;
- exit;
+ #exit;
}
}
}
sub Form::parse {
my $state = 0;
my @forms = ();
- my @lines = split /\n/, $_[0];
+ my @lines = split /\n/, $_[0] if $_[0];
my ($c, $o, $k, $e) = ("", [], {}, "");
LINE:
sub config_from_env {
my %env;
- foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER", "QUERY", "ORDERBY") {
+ foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
+
if (exists $ENV{"RT$k"}) {
$env{lc $k} = $ENV{"RT$k"};
}
sub parse_config_file {
my %cfg;
my ($file) = @_;
+ local $_; # $_ may be aliased to a constant, from line 1163
open(CFG, $file) && do {
while (<CFG>) {
chomp;
next if (/^#/ || /^\s*$/);
- if (/^(user|passwd|server|query|orderby)\s+(.*)\s?$/) {
+ if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
$cfg{$1} = $2;
}
else {
my $sub = (caller(1))[3];
$sub =~ s/^main:://;
warn "rt: $sub: @_\n";
- return;
+ return 0;
}
sub read_passwd {
my @values = ref $val eq 'ARRAY' ? @$val : $val;
foreach my $line (map {split /\n/} @values) {
- # XXX: This should become a real parser, Ã la Text::ParseWords.
+ # XXX: This should become a real parser, Ã la Text::ParseWords.
$line =~ s/^\s+//;
$line =~ s/\s+$//;
- push @words, split /\s*,\s*/, $line;
+ my ( $a, $b ) = split /,/, $line, 2;
+
+ while ($a) {
+ no warnings 'uninitialized';
+ if ( $a =~ /^'/ ) {
+ my $s = $a;
+ while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/
+ && $a =~ /(\\)+'$/ )) {
+ ( $a, $b ) = split /,/, $b, 2;
+ $s .= ',' . $a;
+ }
+ push @words, $s;
+ }
+ elsif ( $a =~ /^q{/ ) {
+ my $s = $a;
+ while ( $a !~ /}$/ ) {
+ ( $a, $b ) =
+ split /,/, $b, 2;
+ $s .= ',' . $a;
+ }
+ $s =~ s/^q{/'/;
+ $s =~ s/}/'/;
+ push @words, $s;
+ }
+ else {
+ push @words, $a;
+ }
+ ( $a, $b ) = split /,/, $b, 2;
+ }
+
+
}
return \@words;
}
+# WARN: this code is duplicated in lib/RT/Interface/REST.pm
+# change both functions at once
sub expand_list {
my ($list) = @_;
- my ($elt, @elts, %elts);
- foreach $elt (split /,/, $list) {
- if ($elt =~ /^(\d+)-(\d+)$/) { push @elts, ($1..$2) }
- else { push @elts, $elt }
+ my @elts;
+ foreach (split /,/, $list) {
+ push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
}
- @elts{@elts}=();
- return sort {$a<=>$b} keys %elts;
+ return map $_->[0], # schwartzian transform
+ sort {
+ defined $a->[1] && defined $b->[1]?
+ # both numbers
+ $a->[1] <=> $b->[1]
+ :!defined $a->[1] && !defined $b->[1]?
+ # both letters
+ $a->[2] cmp $b->[2]
+ # mix, number must be first
+ :defined $a->[1]? -1: 1
+ }
+ map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ],
+ @elts;
}
sub get_type_argument {
$spec =~ s|^(?:$type/)?|$type/| if defined $type;
return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o);
- return;
+ return 0;
+}
+
+sub suggest_help {
+ my ($action, $type, $rv) = @_;
+
+ print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action;
+ print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type;
+ return $rv;
+}
+
+sub str2time {
+ # simplified procedure for parsing date, avoid loading Date::Parse
+ my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5,
+ Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11);
+ $_ = shift;
+ my ($mon, $day, $hr, $min, $sec, $yr, $monstr);
+ if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) {
+ ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6);
+ $mon = $month{$monstr} if exists $month{$monstr};
+ } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) {
+ ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6);
+ }
+ if ( $yr and defined $mon and $day and defined $hr and defined $sec ) {
+ return timelocal($sec,$min,$hr,$day,$mon,$yr);
+ } else {
+ print "Unknown date format in parsedate: $_\n";
+ return undef;
+ }
+}
+
+sub date_diff {
+ my ($old, $new) = @_;
+ $new = time() if ! $new;
+ $old = str2time($old) if $old !~ /^\d+$/;
+ $new = str2time($new) if $new !~ /^\d+$/;
+ return "???" if ! $old or ! $new;
+
+ my %seconds = (min => 60,
+ hr => 60*60,
+ day => 60*60*24,
+ wk => 60*60*24*7,
+ mth => 60*60*24*30,
+ yr => 60*60*24*365);
+
+ my $diff = $new - $old;
+ my $what = 'sec';
+ my $howmuch = $diff;
+ for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) {
+ last if $diff < $seconds{$_};
+ $what = $_;
+ $howmuch = int($diff/$seconds{$_});
+ }
+ return "$howmuch $what";
+}
+
+sub prettyshow {
+ my $forms = shift;
+ my ($form) = grep { exists $_->[2]->{Queue} } @$forms;
+ my $k = $form->[2];
+ # dates are in local time zone
+ if ( $k ) {
+ print "Date: $k->{Created}\n";
+ print "From: $k->{Requestors}\n";
+ print "Cc: $k->{Cc}\n" if $k->{Cc};
+ print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc};
+ print "X-Queue: $k->{Queue}\n";
+ print "Subject: [rt #$k->{id}] $k->{Subject}\n\n";
+ }
+ # dates in these attributes are in GMT and will be converted
+ foreach my $form (@$forms) {
+ my ($c, $o, $k, $e) = @$form;
+ next if ! $k->{id} or exists $k->{Queue};
+ if ( exists $k->{Created} ) {
+ my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/);
+ $m--;
+ my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y));
+ if ( exists $k->{Description} ) {
+ print "===> $k->{Description} on $created\n";
+ }
+ }
+ print "$k->{Content}\n" if exists $k->{Content} and
+ $k->{Content} !~ /to have no content$/ and
+ $k->{Type} ne 'EmailRecord';
+ print "$k->{Attachments}\n" if exists $k->{Attachments} and
+ $k->{Attachments};
+ }
+}
+
+sub prettylist {
+ my $forms = shift;
+ my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n";
+ $heading .= '-' x 80 . "\n";
+ my (@open, @me);
+ foreach my $form (@$forms) {
+ my ($c, $o, $k, $e) = @$form;
+ next if ! $k->{id};
+ print $heading if $heading;
+ $heading = '';
+ my $id = $k->{id};
+ $id =~ s!^ticket/!!;
+ my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner};
+ $owner = substr($owner, 0, 5);
+ my $queue = substr($k->{Queue}, 0, 5);
+ my $subject = substr($k->{Subject}, 0, 30);
+ my $age = date_diff($k->{Created});
+ my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told});
+ my $status = substr($k->{Status}, 0, 6);
+ my $requestor = substr($k->{Requestors}, 0, 9);
+ my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n",
+ $id, $owner, $queue, $age, $told, $status, $requestor, $subject;
+ if ( $k->{Owner} eq 'Nobody' ) {
+ push @open, $line;
+ } elsif ($k->{Owner} eq $config{user} ) {
+ push @me, $line;
+ } else {
+ print $line;
+ }
+ }
+ print "No matches found\n" if $heading;
+ printf "========== my %2d open tickets ==========\n", scalar @me if @me;
+ print @me if @me;
+ printf "========== %2d unowned tickets ==========\n", scalar @open if @open;
+ print @open if @open;
}
__DATA__
Title: introduction
Text:
- ** THIS IS AN UNSUPPORTED PREVIEW RELEASE **
- ** PLEASE REPORT BUGS TO rt-bugs@fsck.com **
-
- This is a command-line interface to RT 3.
+ This is a command-line interface to RT 3.0 or newer.
It allows you to interact with an RT server over HTTP, and offers an
interface to RT's functionality that is better-suited to automation
For more information:
- - rt help actions (a list of possible actions)
- - rt help objects (how to specify objects)
- rt help usage (syntax information)
+ - rt help objects (how to specify objects)
+ - rt help actions (a list of possible actions)
+ - rt help types (a list of object types)
- rt help config (configuration details)
- rt help examples (a few useful examples)
Syntax:
rt <action> [options] [arguments]
+ or
+ rt shell
Each invocation of this program must specify an action (e.g. "edit",
"create"), options to modify behaviour, and other arguments required
"rt help <action>". Some actions may be referred to by more than one
name ("create" is the same as "new", for example).
+ You may also call "rt shell", which will give you an 'rt>' prompt at
+ which you can issue commands of the form "<action> [options]
+ [arguments]". See "rt help shell" for details.
+
Objects are identified by a type and an ID (which can be a name or a
number, depending on the type). For some actions, the object type is
implied (you can only comment on tickets); for others, the user must
- rt help objects (how to specify objects)
- rt help actions (a list of actions)
- rt help types (a list of object types)
+ - rt help shell (how to use the shell)
--
- passwd <passwd> RT user's password.
- query <RT Query> Default RT Query for list action
- orderby <order> Default RT order for list action
+ - queue <queuename> Default RT Queue for list action
+ - externalauth <0|1> Use HTTP Basic authentication
+ explicitely setting externalauth to 0 inhibits also GSSAPI based
+ authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed
Blank and #-commented lines are ignored.
+ Sample configuration file contents:
+
+ server https://rt.somewhere.com/
+ # more than one queue can be given (by adding a query expression)
+ queue helpdesk or queue=support
+ query Status != resolved and Owner=myaccount
+
+
Environment variables:
The following environment variables override any corresponding
- RTUSER
- RTPASSWD
+ - RTEXTERNALAUTH
- RTSERVER
- RTDEBUG Numeric debug level. (Set to 3 for full logs.)
- RTCONFIG Specifies a name other than ".rtrc" for the
"user/root,1-3,5,7-10,ams" is a list of ten users; the same list
can also be written as "user/ams,root,1,2,3,5,7,8-10".
+ If just a number is given as object specification it will be
+ interpreted as ticket/<number>
+
Examples:
+ 1 # the same as ticket/1
ticket/1
ticket/1/attachments
ticket/1/attachments/3
- rt help <action> (action-specific details)
- rt help types (a list of possible types)
+ The following actions on tickets are also possible:
+
+ - comment Add comments to a ticket
+ - correspond Add comments to a ticket
+ - merge Merge one ticket into another
+ - link Link one ticket to another
+ - take Take a ticket (steal and untake are possible as well)
+
+ For several edit set subcommands that are frequently used abbreviations
+ have been introduced. These abbreviations are:
+
+ - delete or del delete a ticket (edit set status=deleted)
+ - resolve or res resolve a ticket (edit set status=resolved)
+ - subject change subject of ticket (edit set subject=string)
+ - give give a ticket to somebody (edit set owner=user)
+
--
Title: types
- merge
- comment
- correspond
+ - take
+ - steal
+ - untake
+ - give
+ - resolve
+ - delete
+ - subject
Attributes:
--
+Title: subject
+Text:
+
+ Syntax:
+
+ rt subject <id> <new subject text>
+
+ Change the subject of a ticket whose ticket id is given.
+
+--
+
+Title: give
+Text:
+
+ Syntax:
+
+ rt give <id> <accountname>
+
+ Give a ticket whose ticket id is given to another user.
+
+--
+
+Title: steal
+Text:
+
+ rt steal <id>
+
+ Steal a ticket whose ticket id is given, i.e. set the owner to myself.
+
+--
+
+Title: take
+Text:
+
+ Syntax:
+
+ rt take <id>
+
+ Take a ticket whose ticket id is given, i.e. set the owner to myself.
+
+--
+
+Title: untake
+Text:
+
+ Syntax:
+
+ rt untake <id>
+
+ Untake a ticket whose ticket id is given, i.e. set the owner to Nobody.
+
+--
+
+Title: resolve
+Title: res
+Text:
+
+ Syntax:
+
+ rt resolve <id>
+
+ Resolves a ticket whose ticket id is given.
+
+--
+
+Title: delete
+Title: del
+Text:
+
+ Syntax:
+
+ rt delete <id>
+
+ Deletes a ticket whose ticket id is given.
+
+--
+
Title: logout
Text:
The following options control how much information is displayed
about each matching object:
- -i Numeric IDs only. (Useful for |rt edit -; see examples.)
- -s Short description.
- -l Longer description.
+ -i Numeric IDs only. (Useful for |rt edit -; see examples.)
+ -s Short description.
+ -l Longer description.
+ -f <field[s] Display only the fields listed and the ticket id
In addition,
- -o +/-<field> Orders the returned list by the specified field.
- -S var=val Submits the specified variable with the request.
- -t type Specifies the type of object to look for. (The
- default is "ticket".)
+ -o +/-<field> Orders the returned list by the specified field.
+ -r reversed order (useful if a default was given)
+ -q queue[s] restricts the query to the queue[s] given
+ multiple queues are separated by comma
+ -S var=val Submits the specified variable with the request.
+ -t type Specifies the type of object to look for. (The
+ default is "ticket".)
Examples:
- rt ls "Priority > 5 and Status='new'"
- rt ls -o +Subject "Priority > 5 and Status='new'"
- rt ls -o -Created "Priority > 5 and Status='new'"
+ rt ls "Priority > 5 and Status=new"
+ rt ls -o +Subject "Priority > 5 and Status=new"
+ rt ls -o -Created "Priority > 5 and Status=new"
rt ls -i "Priority > 5"|rt edit - set status=resolved
rt ls -t ticket "Subject like '[PATCH]%'"
+ rt ls -q systems
+ rt ls -f owner,subject
--
that refers to the links for tickets 1-3). Consult "rt help <type>"
and "rt help objects" for further details.
+ If only a number is given it will be interpreted as the objects
+ ticket/number and ticket/number/history
+
This command writes a set of forms representing the requested object
data to STDOUT.
Options:
+ The following options control how much information is displayed
+ about each matching object:
+
+ Without any formatting options prettyprinted output is generated.
+ Giving any of the two options below reverts to raw output.
+ -s Short description (history and attachments only).
+ -l Longer description (history and attachments only).
+
+ In addition,
- Read IDs from STDIN instead of the command-line.
-t type Specifies object type.
-f a,b,c Restrict the display to the specified fields.
rt show ticket/3/attachments/29
rt show ticket/3/attachments/29/content
rt show ticket/1-3/links
+ rt show ticket/3/history
+ rt show -l ticket/3/history
rt show -t user 2
+ rt show 2
--
Edits information corresponding to the specified objects.
+ A purely numeric object id nnn is translated into ticket/nnn
+
If, instead of "edit", an action of "new" or "create" is specified,
then a new object is created. In this case, no numeric object IDs
may be specified, but the syntax and behaviour remain otherwise
rt create -t ticket
# Non-interactive.
- rt edit ticket/1-3 add cc=foo@example.com set priority=3
+ rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved
rt edit ticket/4 set priority=3 owner=bar@example.com \
add cc=foo@example.com bcc=quux@example.net
- rt create -t ticket subject='new ticket' priority=10 \
+ rt create -t ticket set subject='new ticket' priority=10 \
add cc=foo@example.com
--
Examples:
- rt comment -t 'Not worth fixing.' -a stddisclaimer.h 23
+ rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23
--
rt merge <from-id> <to-id>
- Merges the two specified tickets.
+ Merges the first ticket specified into the second ticket specified.
--
Syntax:
- rt link [-d] <id-A> <relationship> <id-B>
+ rt link [-d] <id-A> <link> <id-B>
Creates (or, with -d, deletes) a link between the specified tickets.
- The relationship can (irrespective of case) be any of:
+ The link can (irrespective of case) be any of:
DependsOn/DependedOnBy: A depends upon B (or vice versa).
RefersTo/ReferredToBy: A refers to B (or vice versa).
MemberOf/HasMember: A is a member of B (or vice versa).
- To view a ticket's relationships, use "rt show ticket/3/links". (See
+ To view a ticket's links, use "rt show ticket/3/links". (See
"rt help ticket" and "rt help show".)
Options:
(XXX: I'm going to have to write it, aren't I?)
+ Until it exists here a short description of important constructs:
+
+ The two simple forms of query expressions are the constructs
+ Attribute like Value and
+ Attribute = Value or Attribute != Value
+
+ Whether attributes can be matched using like or using = is built into RT.
+ The attributes id, Queue, Owner Priority and Status require the = or !=
+ tests.
+
+ If Value is a string it must be quoted and may contain the wildcard
+ character %. If the string does not contain white space, the quoting
+ may however be omitted, it will be added automatically when parsing
+ the input.
+
+ Simple query expressions can be combined using and, or and parentheses
+ can be used to group expressions.
+
+ As a special case a standalone string (which would not form a correct
+ query) is transformed into (Owner='string' or Requestor like 'string%')
+ and added to the default query, i.e. the query is narrowed down.
+
+ If no Queue=name clause is contained in the query, a default clause
+ Queue=$config{queue} is added.
+
+ Examples:
+ Status!='resolved' and Status!='rejected'
+ (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved'
+
--
Title: form
Title: topics
Text:
- Use "rt help <topic>" for help on any of the following subjects:
+ Syntax:
+
+ rt help <topic>
+
+ Get help on any of the following subjects:
- tickets, users, groups, queues.
- show, edit, ls/list/search, new/create.
Title: examples
Text:
- This section will be filled in with useful examples, once it becomes
- more clear what examples may be useful.
+ some useful examples
+
+ All the following list requests will be restricted to the default queue.
+ That can be changed by adding the option -q queuename
+
+ List all tickets that are not rejected/resolved
+ rt ls
+ List all tickets that are new and do not have an owner
+ rt ls "status=new and owner=nobody"
+ List all tickets which I have sent or of which I am the owner
+ rt ls myaccount
+ List all attributes for the ticket 6977 (ls -l instead of ls)
+ rt ls -l 6977
+ Show the content of ticket 6977
+ rt show 6977
+ Show all attributes in the ticket and in the history of the ticket
+ rt show -l 6977
+ Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's)
+ rt comment 6977
+ This will open an editor and lets you add text (attribute Text:)
+ Other attributes may be changed as well, but usually don't do that.
+ Correspond a ticket (like comment, but mail is also sent to requestors)
+ rt correspond 6977
+ Edit a ticket (generic change, interactive using the editor)
+ rt edit 6977
+ Change the owner of a ticket non interactively
+ rt edit 6977 set owner=myaccount
+ or
+ rt give 6977 account
+ or
+ rt take 6977
+ Change the status of a ticket
+ rt edit 6977 set status=resolved
+ or
+ rt resolve 6977
+ Change the status of all tickets I own to resolved !!!
+ rt ls -i owner=myaccount | rt edit - set status=resolved
+
+--
+
+Title: shell
+Text:
+
+ Syntax:
+
+ rt shell
+
+ Opens an interactive shell, at which you can issue commands of
+ the form "<action> [options] [arguments]".
+
+ To exit the shell, type "quit" or "exit".
- For the moment, please consult examples provided with each action.
+ Commands can be given at the shell in the same form as they would
+ be given at the command line without the leading 'rt' invocation.
+
+ Example:
+ $ rt shell
+ rt> create -t ticket set subject='new' add cc=foo@example.com
+ # Ticket 8 created.
+ rt> quit
+ $
+
+--
+
+Title: take
+Title: untake
+Title: steal
+Text:
+
+ Syntax:
+
+ rt <take|untake|steal> <ticket-id>
+
+ Sets the owner of the specified ticket to the current user,
+ assuming said user has the bits to do so, or releases the
+ ticket.
+
+ 'Take' is used on tickets which are not currently owned
+ (Owner: Nobody), 'steal' is used on tickets which *are*
+ currently owned, and 'untake' is used to "release" a ticket
+ (reset its Owner to Nobody). 'Take' cannot be used on
+ tickets which are currently owned.
+
+ Example:
+ alice$ rt create -t ticket set subject="New ticket"
+ # Ticket 7 created.
+ alice$ rt take 7
+ # Owner changed from Nobody to alice
+ alice$ su bob
+ bob$ rt steal 7
+ # Owner changed from alice to bob
+ bob$ rt untake 7
+ # Owner changed from bob to Nobody
--
+
+Title: quit
+Title: exit
+Text:
+
+ Use "quit" or "exit" to leave the shell. Only valid within shell
+ mode.
+
+ Example:
+ $ rt shell
+ rt> quit
+ $