X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Fbin%2Frt;h=1757d08c0d6cf83413263b6c4d3a07885d8a5991;hp=d9f8a3f051d9fd6e401a8cfb0cc689f12da195f8;hb=187086c479a09629b7d180eec513fb7657f4e291;hpb=289340780927b5bac2c7604d7317c3063c6dd8cc diff --git a/rt/bin/rt b/rt/bin/rt index d9f8a3f05..1757d08c0 100755 --- a/rt/bin/rt +++ b/rt/bin/rt @@ -1,36 +1,74 @@ #!/usr/bin/perl -w -# BEGIN LICENSE BLOCK -# -# Copyright (c) 1996-2003 Jesse Vincent -# -# (Except where explictly superceded by other copyright notices) -# +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC +# +# +# (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. -# -# 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. -# -# -# END LICENSE BLOCK +# +# 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 +# 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 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 Cwd; use LWP; +use Text::ParseWords; use HTTP::Request::Common; +use HTTP::Headers; +use Term::ReadLine; +use Time::Local; # used in prettyshow +use File::Temp; # We derive configuration information from hardwired defaults, dotfiles, # and the RT* environment variables (in increasing order of precedence). @@ -42,28 +80,40 @@ my $HOME = eval{(getpwuid($<))[7]} || "."; my %config = ( ( - debug => 0, - user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME}, - passwd => undef, - server => 'http://localhost/rt/', + 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', + auth => "rt", ), config_from_file($ENV{RTCONFIG} || ".rtrc"), config_from_env() ); -my $session = new Session("$HOME/.rt_sessions"); + +$config{auth} = "basic" if delete $config{externalauth}; + +my $session = Session->new("$HOME/.rt_sessions"); my $REST = "$config{server}/REST/1.0"; +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 = '[^,]+?'; +my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})'; +my $label = '[^,\\/]+'; +my $labels = "(?:$label,)*$label"; +my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; # Our command line looks like this: # @@ -75,6 +125,7 @@ my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; my %handlers = ( # handler => [ ...aliases... ], version => ["version", "ver"], + shell => ["shell"], logout => ["logout"], help => ["help", "man"], show => ["show", "cat"], @@ -84,47 +135,86 @@ my %handlers = ( 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 = Term::ReadLine->new('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() }) { - my $title = $item->[2]{Title}; - my @titles = ref $title eq 'ARRAY' ? @$title : $title; + if (!%help) { + local $/ = undef; + foreach my $item (@{ Form::parse() }) { + my $title = $item->[2]{Title}; + my @titles = ref $title eq 'ARRAY' ? @$title : $title; - foreach $title (grep $_, @titles) { - $help{$title} = $item->[2]{Text}; + foreach $title (grep $_, @titles) { + $help{$title} = $item->[2]{Text}; + } } } @@ -161,13 +251,22 @@ sub help { } 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; @@ -179,10 +278,17 @@ sub list { $bad = 1, last unless get_var_argument(\%data); } elsif (/^-o$/) { - $orderby = shift @ARGV; + $data{'orderby'} = 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)$/) { @@ -190,6 +296,8 @@ sub list { $bad = 1; last; } $data{fields} = shift @ARGV; + $data{format} = 's' if ! $data{format}; + $rawprint = 1; } elsif (!defined $q && !/^-/) { $q = $_; @@ -200,17 +308,75 @@ sub list { $bad = 1; last; } } + 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/^-/+/; + } elsif ($reverse_sort) { + $data{orderby} =~ s/^\+?(.*)/-$1/; + } $type ||= "ticket"; - unless ($type && defined $q) { + + if (!defined $q ) { + if ( $type eq 'ticket' ) { + $q = $config{query}; + } + else { + $q = ''; + } + } + + if ( $type ne 'ticket' ) { + $rawprint = 1; + } + + unless (defined $q) { my $item = $type ? "query string" : "object type"; whine "No $item specified."; $bad = 1; } - return help("list", $type) if $bad; - my $r = submit("$REST/search/$type", { query => $q, %data, orderby => $orderby || "" }); - print $r->content; + $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; + + print "Query:$q\n" if ! $rawprint; + my $r = submit("$REST/search/$type", { query => $q, %data }); + if ( $rawprint ) { + print $r->content; + } else { + my $forms = Form::parse($r->content); + prettylist ($forms); + } + return 0; } # Displays selected information about a single object. @@ -219,10 +385,12 @@ sub show { 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()); } @@ -231,6 +399,7 @@ sub show { } elsif (/^-([isl])$/) { $data{format} = $1; + $rawprint = 1; } elsif (/^-$/ && !$slurped) { chomp(my @lines = ); @@ -249,9 +418,21 @@ sub show { $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 $_ =~ /\/links/ or $_ !~ /^ticket/; } else { my $datum = /^-/ ? "option" : "argument"; @@ -259,15 +440,36 @@ sub show { $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 @@ -280,7 +482,7 @@ sub show { 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 = (); @@ -289,10 +491,12 @@ sub edit { while (@ARGV) { $_ = shift @ARGV; + s/^#// if /^#\d+/; # get rid of leading hash 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()); } @@ -313,7 +517,7 @@ sub edit { 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; @@ -331,7 +535,7 @@ sub edit { 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); @@ -344,6 +548,9 @@ sub edit { } $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; } @@ -369,20 +576,25 @@ sub edit { 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 = ; } - elsif ($edit || %add || %del || !$cl) { + elsif ($edit || %add || %del || !$cl || @new_objects) { my $r = submit("$REST/show", { id => \@objects, format => 'l' }); $text = $r->content; } @@ -451,45 +663,125 @@ sub edit { if ($output) { print $text; - exit; + 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; } 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. 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; @@ -497,7 +789,7 @@ sub comment { if (/^-e$/) { $edit = 1; } - elsif (/^-[abcmw]$/) { + elsif (/^-(?:[abcmws]|ct)$/) { unless (@ARGV) { whine "No argument specified with $_."; $bad = 1; last; @@ -506,15 +798,27 @@ sub comment { if (/-a/) { unless (-f $ARGV[0] && -r $ARGV[0]) { whine "Cannot read attachment: '$ARGV[0]'."; - exit -1; + return 0; } 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; } - elsif (/-m/) { $msg = shift @ARGV } + elsif (/-m/) { + $msg = shift @ARGV; + if ( $msg =~ /^-$/ ) { + undef $msg; + while () { $msg .= $_ } + } + } elsif (/-w/) { $wtime = shift @ARGV } } elsif (!$id && m|^(?:ticket/)?($idlist)$|) { @@ -531,11 +835,12 @@ sub comment { 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 = [ "", - [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Text" ], + [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ], { Ticket => $id, Action => $action, @@ -543,37 +848,31 @@ sub comment { Bcc => [ @bcc ], Attachment => [ @files ], TimeWorked => $wtime || '', + 'Content-Type' => $content_type || 'text/plain', Text => $msg || '', + 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); - exit 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) { - exit; - } - @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; @@ -583,8 +882,9 @@ sub comment { } $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. @@ -595,6 +895,7 @@ sub merge { while (@ARGV) { $_ = shift @ARGV; + s/^#// if /^#\d+/; # get rid of leading hash if (/^\d+$/) { push @id, $_; @@ -610,16 +911,20 @@ sub merge { 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); @@ -629,21 +934,21 @@ sub link { 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."; - $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); @@ -653,10 +958,50 @@ sub link { 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; +} - my $r = submit("$REST/ticket/link", \%data); +# 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/$id/take", \%data); print $r->content; + return 0; } # Grant/revoke a user's rights. @@ -664,11 +1009,8 @@ sub link { sub grant { my ($cmd) = @_; - my $revoke = 0; - while (@ARGV) { - } - - $revoke = 1 if $cmd->{action} eq 'revoke'; + whine "$cmd is unimplemented."; + return 1; } # Client <-> Server communication. @@ -681,7 +1023,8 @@ sub grant { 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? $data = []; @@ -707,7 +1050,23 @@ sub submit { } # Should we send authentication information to start a new session? - if (!defined $session->cookie) { + my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted'; + 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}; push @$data, ( user => $config{user} ); push @$data, ( pass => $config{passwd} || read_passwd() ); } @@ -720,6 +1079,7 @@ sub submit { $req = GET($uri); } $session->add_cookie_header($req); + $req->header(%$h) if %$h; # Then we send the request and parse the response. DEBUG(3, $req->as_string); @@ -733,11 +1093,11 @@ sub submit { my ($head, $text) = split /\n\n/, $res->content, 2; my ($status, @headers) = split /\n/, $head; - $text =~ s/\n*$/\n/; + $text =~ s/\n*$/\n/ if ($text); # "RT/3.0.1 401 Credentials required" - if ($status !~ m#^RT/\d+(?:\.\d+)+(?:-?\w+)? (\d+) ([\w\s]+)$#) { - warn "rt: Malformed RT response from $config{server}.\n"; + if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) { + warn "rt: Malformed RT response from $server.\n"; warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3; exit -1; } @@ -770,7 +1130,7 @@ sub submit { # For anything else, we just die. elsif ($res->code != 409) { warn "rt: ", $res->content; - exit; + #exit; } } } @@ -812,7 +1172,7 @@ sub submit { sub cookie { my ($self) = @_; my $cookie = $self->{sids}{$s}{$u}; - return defined $cookie ? "RT_SID=$cookie" : undef; + return defined $cookie ? "RT_SID_$cookie" : undef; } # Deletes the current session cookie. @@ -835,7 +1195,7 @@ sub submit { my ($self, $response) = @_; my $cookie = $response->header("Set-Cookie"); - if (defined $cookie && $cookie =~ /^RT_SID=([0-9A-Fa-f]+);/) { + if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) { $self->{sids}{$s}{$u} = $1; } } @@ -844,44 +1204,40 @@ sub submit { sub load { my ($self, $file) = @_; $file ||= $self->{file}; - local *F; - - open(F, $file) && do { - $self->{file} = $file; - my $sids = $self->{sids} = {}; - while () { - chomp; - next if /^$/ || /^#/; - next unless m#^https?://[^ ]+ \w+ [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 { @@ -905,7 +1261,7 @@ sub submit { 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: @@ -1027,7 +1383,7 @@ sub Form::compose { $line .= ",\n$sp$v"; } else { - $line = $line ? "$line, $v" : "$key: $v"; + $line = $line ? "$line,$v" : "$key: $v"; } } @@ -1061,7 +1417,8 @@ sub Form::compose { sub config_from_env { my %env; - foreach my $k ("DEBUG", "USER", "PASSWD", "SERVER") { + foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) { + if (exists $ENV{"RT$k"}) { $env{lc $k} = $ENV{"RT$k"}; } @@ -1094,7 +1451,7 @@ sub config_from_file { } # 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 $_); } } @@ -1106,20 +1463,21 @@ sub config_from_file { sub parse_config_file { my %cfg; my ($file) = @_; + local $_; # $_ may be aliased to a constant, from line 1163 - open(CFG, $file) && do { - while () { - chomp; - next if (/^#/ || /^\s*$/); + open( my $handle, '<', $file ) or return; - if (/^(user|passwd|server)\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; } @@ -1131,7 +1489,7 @@ sub whine { my $sub = (caller(1))[3]; $sub =~ s/^main:://; warn "rt: $sub: @_\n"; - return; + return 0; } sub read_passwd { @@ -1149,18 +1507,58 @@ sub read_passwd { 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 = ; 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; } @@ -1182,33 +1580,82 @@ sub vpush { } } +# 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+$//; - push @words, split /\s*,\s*/, $line; + 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 = "'"; + } + if ($strip) { + $quoted =~ s/\\([\\$quote])/$1/g if $quote; + push @words, $quoted; + } else { + push @words, "$quote$quoted$quote"; + } + } } - 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 /\s*,\s*/, $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 { @@ -1255,7 +1702,130 @@ sub is_object_spec { $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__ @@ -1264,10 +1834,7 @@ Title: intro 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 @@ -1279,9 +1846,10 @@ Text: 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) @@ -1296,6 +1864,8 @@ Text: Syntax: rt [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 @@ -1306,6 +1876,10 @@ Text: "rt help ". 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 " [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 @@ -1320,6 +1894,7 @@ Text: - 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) -- @@ -1334,18 +1909,35 @@ Text: 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 to RT server. - - user RT username. - - passwd RT user's password. + - server URL to RT server. + - user RT username. + - passwd RT user's password. + - query Default RT Query for list action + - orderby Default RT order for list action + - queue Default RT Queue for list action + - auth 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. + 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 @@ -1353,10 +1945,13 @@ Text: - RTUSER - RTPASSWD + - RTAUTH - RTSERVER - RTDEBUG Numeric debug level. (Set to 3 for full logs.) - RTCONFIG Specifies a name other than ".rtrc" for the configuration file. + - RTQUERY Default RT Query for rt list + - RTORDERBY Default order for rt list -- @@ -1380,10 +1975,14 @@ Text: be used to specify more than one object of the same type. Note that the list must be a single argument (i.e., no spaces). For example, "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-20". + 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/ + Examples: + 1 # the same as ticket/1 ticket/1 ticket/1/attachments ticket/1/attachments/3 @@ -1392,8 +1991,6 @@ Text: ticket/1-3,5-7/history user/ams - user/ams/rights - user/ams,rai,1/rights For more information: @@ -1421,6 +2018,22 @@ Text: - rt help (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 @@ -1459,6 +2072,13 @@ Text: - merge - comment - correspond + - take + - steal + - untake + - give + - resolve + - delete + - subject Attributes: @@ -1488,20 +2108,6 @@ Text: - 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 rights for this user. - -- Title: queue @@ -1517,6 +2123,83 @@ Text: -- +Title: subject +Text: + + Syntax: + + rt subject + + Change the subject of a ticket whose ticket id is given. + +-- + +Title: give +Text: + + Syntax: + + rt give + + Give a ticket whose ticket id is given to another user. + +-- + +Title: steal +Text: + + rt steal + + Steal a ticket whose ticket id is given, i.e. set the owner to myself. + +-- + +Title: take +Text: + + Syntax: + + rt take + + Take a ticket whose ticket id is given, i.e. set the owner to myself. + +-- + +Title: untake +Text: + + Syntax: + + rt untake + + Untake a ticket whose ticket id is given, i.e. set the owner to Nobody. + +-- + +Title: resolve +Title: res +Text: + + Syntax: + + rt resolve + + Resolves a ticket whose ticket id is given. + +-- + +Title: delete +Title: del +Text: + + Syntax: + + rt delete + + Deletes a ticket whose ticket id is given. + +-- + Title: logout Text: @@ -1542,37 +2225,47 @@ Text: 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. + + 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". - (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 query string is absent, we limit to privileged ones on users and + user defined ones on groups automatically. Options: 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 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 +/- 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 + rt ls -t queue 'Name = General' + rt ls -t user 'EmailAddress like foo@bar.com' + rt ls -t group 'Name like foo' -- @@ -1590,11 +2283,23 @@ Text: that refers to the links for tickets 1-3). Consult "rt help " 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. @@ -1606,7 +2311,10 @@ Text: 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 -- @@ -1623,6 +2331,8 @@ Text: 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 @@ -1654,19 +2364,21 @@ Text: -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 + 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 -- @@ -1691,17 +2403,20 @@ Text: Options: -m Specify comment text. + -ct Specify content-type of comment text. -a Attach a file to the comment. (May be used more than once to attach multiple files.) -c A comma-separated list of Cc addresses. -b A comma-separated list of Bcc addresses. + -s Set a new status for the ticket (default will + leave the status unchanged) -w