#!/usr/bin/perl -w
# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
-#
+#
+# This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
+# <sales@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
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
-#
+#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
-#
+#
# 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:
-#
+#
# (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
# 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 warnings;
+
+if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) {
+ require Pod::Usage;
+ print Pod::Usage::pod2usage( { verbose => 2 } );
+ exit;
+}
# This program is intentionally written to have as few non-core module
# dependencies as possible. It should stay that way.
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;
-}
+use File::Temp;
# We derive configuration information from hardwired defaults, dotfiles,
# and the RT* environment variables (in increasing order of precedence).
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,
+ auth => "rt",
),
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};
+$config{auth} = "basic" if delete $config{externalauth};
+
+my $session = Session->new("$HOME/.rt_sessions");
+my $REST = "$config{server}/REST/1.0";
my $prompt = 'rt> ';
# (XXX: Ask Autrijus how i18n changes these definitions.)
my $name = '[\w.-]+';
-my $CF_name = '[\sa-z0-9_ :()/-]+';
+my $CF_name = '[^,]+?';
my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})';
-my $label = '[a-zA-Z0-9@_.+-]+';
+my $label = '[^,\\/]+';
my $labels = "(?:$label,)*$label";
my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+';
sub shell {
$|=1;
- my $term = new Term::ReadLine 'RT CLI';
+ my $term = Term::ReadLine->new('RT CLI');
while ( defined ($_ = $term->readline($prompt)) ) {
next if /^#/ || /^\s*$/;
}
if ( ! $rawprint and ! exists $data{format} ) {
$data{format} = 'l';
+ $data{fields} = 'subject,status,queue,created,told,owner,requestors';
}
if ( $reverse_sort and $data{orderby} =~ /^-/ ) {
$data{orderby} =~ s/^-/+/;
$data{orderby} =~ s/^\+?(.*)/-$1/;
}
- if (!defined $q) {
- $q = $config{query};
+ $type ||= "ticket";
+
+ if (!defined $q ) {
+ if ( $type eq 'ticket' ) {
+ $q = $config{query};
+ }
+ else {
+ $q = '';
+ }
}
-
- $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;
+
+ if ( $type ne 'ticket' ) {
+ $rawprint = 1;
}
- # correctly quote strings in a query
- $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g;
- $type ||= "ticket";
- unless ($type && defined $q) {
+ unless (defined $q) {
my $item = $type ? "query string" : "object type";
whine "No $item specified.";
$bad = 1;
}
+
+ $q =~ s/^#//; # get rid of leading hash
+ if ( $type eq 'ticket' ) {
+ 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;
+ }
+
#return help("list", $type) if $bad;
return suggest_help("list", $type, $bad) if $bad;
}
elsif (my $spec = is_object_spec($_, $type)) {
push @objects, $spec;
- $rawprint = 1 if $_ =~ /\/content$/ or $_ !~ /^ticket/;
+ $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/;
}
else {
my $datum = /^-/ ? "option" : "argument";
sub edit {
my ($action) = @_;
my (%data, $type, @objects);
- my ($cl, $text, $edit, $input, $output);
+ my ($cl, $text, $edit, $input, $output, $content_type);
use vars qw(%set %add %del);
%set = %add = %del = ();
if (/^-e$/) { $edit = 1 }
elsif (/^-i$/) { $input = 1 }
elsif (/^-o$/) { $output = 1 }
+ elsif (/^-ct$/) { $content_type = shift @ARGV }
elsif (/^-t$/) {
$bad = 1, last unless defined($type = get_type_argument());
}
return 0;
}
+ my @files;
+ @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'};
+
my $synerr = 0;
EDIT:
# We'll let the user edit the form before sending it to the server,
# unless we have enough information to submit it non-interactively.
+ if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) {
+ $text .= "Content-Type: $content_type\n"
+ if $content_type and $content_type ne "text/plain";
+ }
+
if ($edit || (!$input && !$cl)) {
- my $newtext = vi($text);
+ my ($newtext) = vi_form_while(
+ $text,
+ sub {
+ my ($text, $form) = @_;
+ return 1 unless exists $form->[2]{'Attachment'};
+
+ foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+ return (0, "File '$f' doesn't exist") unless -f $f;
+ }
+ @files = @{ vsplit($form->[2]{'Attachment'}) };
+ return 1;
+ },
+ );
+ return $newtext unless $newtext;
# We won't resubmit a bad form unless it was changed.
$text = ($synerr && $newtext eq $text) ? undef : $newtext;
}
+ delete @data{ grep /^attachment_\d+$/, keys %data };
+ my $i = 1;
+ foreach my $file (@files) {
+ $data{"attachment_$i"} = bless([ $file ], "Attachment");
+ $i++;
+ }
+
if ($text) {
my $r = submit("$REST/edit", {content => $text, %data});
if ($r->code == 409) {
# If we submitted a bad form, we'll give the user a chance
# to correct it and resubmit.
if ($edit || (!$input && !$cl)) {
- $text = $r->content;
+ my $content = $r->content . "\n";
+ $content =~ s/^(?!#)/# /mg;
+ $text = $content . $text;
$synerr = 1;
goto EDIT;
}
sub comment {
my ($action) = @_;
- my (%data, $id, @files, @bcc, @cc, $msg, $wtime, $edit);
+ my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit);
my $bad = 0;
+ my $status = '';
while (@ARGV) {
$_ = shift @ARGV;
if (/^-e$/) {
$edit = 1;
}
- elsif (/^-[abcmw]$/) {
+ elsif (/^-(?:[abcmws]|ct)$/) {
unless (@ARGV) {
whine "No argument specified with $_.";
$bad = 1; last;
}
push @files, shift @ARGV;
}
+ elsif (/-ct/) {
+ $content_type = shift @ARGV;
+ }
+ elsif (/-s/) {
+ $status = shift @ARGV;
+ }
elsif (/-([bc])/) {
my $a = $_ eq "-b" ? \@bcc : \@cc;
@$a = split /\s*,\s*/, shift @ARGV;
while (<STDIN>) { $msg .= $_ }
}
}
-
elsif (/-w/) { $wtime = shift @ARGV }
}
elsif (!$id && m|^(?:ticket/)?($idlist)$|) {
my $form = [
"",
- [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ],
+ [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ],
{
Ticket => $id,
Action => $action,
Bcc => [ @bcc ],
Attachment => [ @files ],
TimeWorked => $wtime || '',
+ 'Content-Type' => $content_type || 'text/plain',
Text => $msg || '',
- Status => ''
+ Status => $status
}
];
+ if ($status ne '') {
+ push(@{$form->[1]}, "Status");
+ }
my $text = Form::compose([ $form ]);
if ($edit || !$msg) {
- my $error = 0;
- my ($c, $o, $k, $e);
-
- do {
- my $ntext = vi($text);
- return if ($error && $ntext eq $text);
- $text = $ntext;
- $form = Form::parse($text);
- $error = 0;
-
- ($c, $o, $k, $e) = @{ $form->[0] };
- if ($e) {
- $error = 1;
- $c = "# Syntax error.";
- goto NEXT;
- }
- elsif (!@$o) {
- return 0;
- }
- @files = @{ vsplit($k->{Attachment}) };
-
- NEXT:
- $text = Form::compose([[$c, $o, $k, $e]]);
- } while ($error);
+ my ($tmp) = vi_form_while(
+ $text,
+ sub {
+ my ($text, $form) = @_;
+ foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) {
+ return (0, "File '$f' doesn't exist") unless -f $f;
+ }
+ @files = @{ vsplit($form->[2]{'Attachment'}) };
+ return 1;
+ },
+ );
+ return $tmp unless $tmp;
+ $text = $tmp;
}
my $i = 1;
if (@ARGV == 3) {
my ($from, $rel, $to) = @ARGV;
- if ($from !~ /^\d+$/ || $to !~ /^\d+$/) {
- my $bad = $from =~ /^\d+$/ ? $to : $from;
- whine "Invalid $type ID '$bad' specified.";
- $bad = 1;
- }
if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) {
whine "Invalid link '$rel' for type $type specified.";
$bad = 1;
sub grant {
my ($cmd) = @_;
- my $revoke = 0;
- while (@ARGV) {
- }
-
- $revoke = 1 if $cmd->{action} eq 'revoke';
- return 0;
+ whine "$cmd is unimplemented.";
+ return 1;
}
# Client <-> Server communication.
sub submit {
my ($uri, $content) = @_;
my ($req, $data);
- my $ua = new LWP::UserAgent(agent => "RT/3.0b", env_proxy => 1);
+ my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1);
my $h = HTTP::Headers->new;
# Did the caller specify any data to send with the request?
# Should we send authentication information to start a new session?
my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted';
- (my $server = $config{server}) =~ s/^.*\/\/([^\/]+)\/?/$1/;
- if ($config{externalauth}) {
+ my($server) = $config{server} =~ m{^.*//([^/]+)};
+
+ if ($config{auth} eq "gssapi") {
+ die "GSSAPI support not available; failed to load perl module GSSAPI:\n$@\n"
+ unless eval { require GSSAPI; 1 };
+ die "GSSAPI support not available; failed to load perl module LWP::Authen::Negotiate:\n$@\n"
+ unless eval { require LWP::Authen::Negotiate; 1 };
+ } elsif ($config{auth} eq "basic") {
+ print " Password will be sent to $server $how\n",
+ " Press CTRL-C now if you do not want to continue\n"
+ if ! $config{passwd};
$h->authorization_basic($config{user}, $config{passwd} || read_passwd() );
+ } elsif ( !defined $session->cookie ) {
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() );
- }
+ 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);
- }
+ $req->header(%$h) if %$h;
# Then we send the request and parse the response.
DEBUG(3, $req->as_string);
# "RT/3.0.1 401 Credentials required"
if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) {
- warn "rt: Malformed RT response from $config{server}.\n";
+ warn "rt: Malformed RT response from $server.\n";
warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3;
exit -1;
}
sub load {
my ($self, $file) = @_;
$file ||= $self->{file};
- local *F;
-
- open(F, $file) && do {
- $self->{file} = $file;
- my $sids = $self->{sids} = {};
- while (<F>) {
- chomp;
- next if /^$/ || /^#/;
- next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
- my ($server, $user, $cookie) = split / /, $_;
- $sids->{$server}{$user} = $cookie;
- }
- return 1;
- };
- return 0;
+
+ open( my $handle, '<', $file ) or return 0;
+
+ $self->{file} = $file;
+ my $sids = $self->{sids} = {};
+ while (<$handle>) {
+ chomp;
+ next if /^$/ || /^#/;
+ next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#;
+ my ($server, $user, $cookie) = split / /, $_;
+ $sids->{$server}{$user} = $cookie;
+ }
+ return 1;
}
# Writes the current session cache to the specified file.
sub save {
my ($self, $file) = shift;
$file ||= $self->{file};
- local *F;
-
- open(F, ">$file") && do {
- my $sids = $self->{sids};
- foreach my $server (keys %$sids) {
- foreach my $user (keys %{ $sids->{$server} }) {
- my $sid = $sids->{$server}{$user};
- if (defined $sid) {
- print F "$server $user $sid\n";
- }
+
+ open( my $handle, '>', "$file" ) or return 0;
+
+ my $sids = $self->{sids};
+ foreach my $server (keys %$sids) {
+ foreach my $user (keys %{ $sids->{$server} }) {
+ my $sid = $sids->{$server}{$user};
+ if (defined $sid) {
+ print $handle "$server $user $sid\n";
}
}
- close(F);
- chmod 0600, $file;
- return 1;
- };
- return 0;
+ }
+ close($handle);
+ chmod 0600, $file;
+ return 1;
}
sub DESTROY {
$line .= ",\n$sp$v";
}
else {
- $line = $line ? "$line, $v" : "$key: $v";
+ $line = $line ? "$line,$v" : "$key: $v";
}
}
sub config_from_env {
my %env;
- foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
+ foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) {
if (exists $ENV{"RT$k"}) {
$env{lc $k} = $ENV{"RT$k"};
}
# Still nothing? We'll fall back to some likely defaults.
- for ("$HOME/$rc", "/etc/rt.conf") {
+ for ("$HOME/$rc", "/opt/rt3/local/etc/rt.conf", "/etc/rt.conf") {
return parse_config_file($_) if (-r $_);
}
}
my ($file) = @_;
local $_; # $_ may be aliased to a constant, from line 1163
- open(CFG, $file) && do {
- while (<CFG>) {
- chomp;
- next if (/^#/ || /^\s*$/);
+ open( my $handle, '<', $file ) or return;
- if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
- $cfg{$1} = $2;
- }
- else {
- die "rt: $file:$.: unknown configuration directive.\n";
- }
+ while (<$handle>) {
+ chomp;
+ next if (/^#/ || /^\s*$/);
+
+ if (/^(externalauth|auth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) {
+ $cfg{$1} = $2;
+ }
+ else {
+ die "rt: $file:$.: unknown configuration directive.\n";
}
- };
+ }
return %cfg;
}
return $passwd;
}
+sub vi_form_while {
+ my $text = shift;
+ my $cb = shift;
+
+ my $error = 0;
+ my ($c, $o, $k, $e);
+ do {
+ my $ntext = vi($text);
+ return undef if ($error && $ntext eq $text);
+
+ $text = $ntext;
+
+ my $form = Form::parse($text);
+ $error = 0;
+ ($c, $o, $k, $e) = @{ $form->[0] };
+ if ( $e ) {
+ $error = 1;
+ $c = "# Syntax error.";
+ goto NEXT;
+ }
+ elsif (!@$o) {
+ return 0;
+ }
+
+ my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] );
+ unless ( $status ) {
+ $error = 1;
+ $c = "# $msg";
+ }
+
+ NEXT:
+ $text = Form::compose([[$c, $o, $k, $e]]);
+ } while ($error);
+
+ return $text;
+}
+
sub vi {
my ($text) = @_;
- my $file = "/tmp/rt.form.$$";
my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi";
- local *F;
local $/ = undef;
- open(F, ">$file") || die "$file: $!\n"; print F $text; close(F);
- system($editor, $file) && die "Couldn't run $editor.\n";
- open(F, $file) || die "$file: $!\n"; $text = <F>; close(F);
- unlink($file);
+ my $handle = File::Temp->new;
+ print $handle $text;
+ close($handle);
+
+ system($editor, $handle->filename) && die "Couldn't run $editor.\n";
+
+ open( $handle, '<', $handle->filename ) or die "$handle: $!\n";
+ $text = <$handle>;
+ close($handle);
return $text;
}
}
}
+# WARNING: this code is duplicated in lib/RT/Interface/REST.pm
+# If you change one, change both functions at once
# "Normalise" a hash key that's known to be multi-valued.
sub vsplit {
- my ($val) = @_;
- my ($word, @words);
- 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.
- $line =~ s/^\s+//;
- $line =~ s/\s+$//;
- 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;
+ my ($val, $strip) = @_;
+ my @words;
+ my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val);
+
+ foreach my $line (@values) {
+ while ($line =~ /\S/) {
+ $line =~ s/^
+ \s* # Trim leading whitespace
+ (?:
+ (") # Quoted string
+ ((?>[^\\"]*(?:\\.[^\\"]*)*))"
+ |
+ (') # Single-quoted string
+ ((?>[^\\']*(?:\\.[^\\']*)*))'
+ |
+ q\{(.*?)\} # A perl-ish q{} string; this does
+ # no paren balancing, however, and
+ # only exists for back-compat
+ |
+ (.*?) # Anything else, until the next comma
+ )
+ \s* # Trim trailing whitespace
+ (?:
+ \Z # Finish at end-of-line
+ |
+ , # Or a comma
+ )
+ //xs or last; # There should be no way this match
+ # fails, but add a failsafe to
+ # prevent infinite-looping if it
+ # somehow does.
+ my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6));
+ # Only unquote the quote character, or the backslash -- and
+ # only if we were originally quoted..
+ if ($5) {
+ $quoted =~ s/([\\'])/\\$1/g;
+ $quote = "'";
}
- elsif ( $a =~ /^q{/ ) {
- my $s = $a;
- while ( $a !~ /}$/ ) {
- ( $a, $b ) =
- split /,/, $b, 2;
- $s .= ',' . $a;
- }
- $s =~ s/^q{/'/;
- $s =~ s/}/'/;
- push @words, $s;
+ if ($strip) {
+ $quoted =~ s/\\([\\$quote])/$1/g if $quote;
+ push @words, $quoted;
+ } else {
+ push @words, "$quote$quoted$quote";
}
- else {
- push @words, $a;
- }
- ( $a, $b ) = split /,/, $b, 2;
}
-
-
}
-
return \@words;
}
my ($list) = @_;
my @elts;
- foreach (split /,/, $list) {
+ foreach (split /\s*,\s*/, $list) {
push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_;
}
}
print "$k->{Content}\n" if exists $k->{Content} and
$k->{Content} !~ /to have no content$/ and
- $k->{Type} ne 'EmailRecord';
+ ($k->{Type}||'') ne 'EmailRecord';
print "$k->{Attachments}\n" if exists $k->{Attachments} and
$k->{Attachments};
}
The program looks for configuration directives in a file named .rtrc
(or $RTCONFIG; see below) in the current directory, and then in more
distant ancestors, until it reaches /. If no suitable configuration
- files are found, it will also check for ~/.rtrc and /etc/rt.conf.
+ files are found, it will also check for ~/.rtrc, /opt/rt3/local/etc/rt.conf
+ and /etc/rt.conf.
Configuration directives:
The following directives may occur, one per line:
- - server <URL> URL to RT server.
- - user <username> RT username.
- - 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
+ - server <URL> URL to RT server.
+ - user <username> RT username.
+ - 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
+ - auth <rt|basic|gssapi> Method to authenticate via; "basic"
+ means HTTP Basic authentication, "gssapi" means
+ Kerberos credentials, if your RT is configured
+ with $WebRemoteUserAuth. For backwards
+ compatibility, "externalauth 1" means "auth basic"
Blank and #-commented lines are ignored.
- RTUSER
- RTPASSWD
- - RTEXTERNALAUTH
+ - RTAUTH
- RTSERVER
- RTDEBUG Numeric debug level. (Set to 3 for full logs.)
- RTCONFIG Specifies a name other than ".rtrc" for the
ticket/1-3,5-7/history
user/ams
- user/ams/rights
- user/ams,rai,1/rights
For more information:
- edit
- create
- In addition, the following type-specific actions exist:
-
- - grant
- - revoke
-
- Attributes:
-
- The following attributes can be used with "rt show" or "rt edit"
- to retrieve or edit other information associated with users and
- groups:
-
- rights Global rights granted to this user.
- rights/<queue> Queue rights for this user.
-
--
Title: queue
Displays a list of objects matching the specified conditions.
("ls", "list", and "search" are synonyms.)
- Conditions are expressed in the SQL-like syntax used internally by
- RT3. (For more information, see "rt help query".) The query string
- must be supplied as one argument.
+ The query string must be supplied as one argument.
- (Right now, the server doesn't support listing anything but tickets.
- Other types will be supported in future; this client will be able to
- take advantage of that support without any changes.)
+ if on tickets, query is in the SQL-like syntax used internally by
+ RT. (For more information, see "rt help query".), otherwise, query
+ is plain string with format "FIELD OP VALUE", e.g. "Name = General".
+
+ if query string is absent, we limit to privileged ones on users and
+ user defined ones on groups automatically.
Options:
rt ls -t ticket "Subject like '[PATCH]%'"
rt ls -q systems
rt ls -f owner,subject
+ rt ls -t queue 'Name = General'
+ rt ls -t user 'EmailAddress like foo@bar.com'
+ rt ls -t group 'Name like foo'
--
-S var=val
Submits the specified variable with the request.
-t type Specifies object type.
+ -ct content-type Specifies content type of message(tickets only).
Examples:
# Interactive (starts $EDITOR with a form).
rt edit ticket/3
rt create -t ticket
+ rt create -t ticket -ct text/html
# Non-interactive.
rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow
Options:
-m <text> Specify comment text.
+ -ct <content-type> Specify content-type of comment text.
-a <file> Attach a file to the comment. (May be used more
than once to attach multiple files.)
-c <addrs> A comma-separated list of Cc addresses.
-b <addrs> A comma-separated list of Bcc addresses.
+ -s <status> Set a new status for the ticket (default will
+ leave the status unchanged)
-w <time> Specify the time spent working on this ticket.
-e Starts an editor before the submission, even if
arguments from the command line were sufficient.
--
-Title: grant
-Title: revoke
-Text:
-
---
-
Title: query
Text:
- RT3 uses an SQL-like syntax to specify object selection constraints.
+ RT uses an SQL-like syntax to specify object selection constraints.
See the <RT:...> documentation for details.
(XXX: I'm going to have to write it, aren't I?)
$ rt shell
rt> quit
$
+
+__END__
+
+=head1 NAME
+
+rt - command-line interface to RT 3.0 or newer
+
+=head1 SYNOPSIS
+
+ rt help
+
+=head1 DESCRIPTION
+
+This script allows you to interact with an RT server over HTTP, and offers an
+interface to RT's functionality that is better-suited to automation and
+integration with other tools.
+
+In general, each invocation of this program should specify an action to
+perform on one or more objects, and any other arguments required to complete
+the desired action.
+