version up
[mrsh.git] / mrsh
blob99658381cc90d05c18c34de4ce712e90590a81f9
1 #!/usr/bin/perl
3 use strict;
4 use warnings;
5 use Getopt::Long; Getopt::Long::Configure("bundling"); # make switches case sensitive (and turn on bundling)
6 use Pod::Usage;
8 use App::MrShell;
10 my @hosts;
11 my $conf = "$ENV{HOME}/.mrshrc";
12 my $log;
13 my $shell;
14 my $trunc;
15 my $debug;
16 my $no_esc;
17 my $show_groups_and_exit;
19 my @orig_ARGV = @ARGV;
20 MAGICALLY_UNDERSTAND_HOSTGROUPS: {
21 GetOptions(
22 "groups|list|g" => \$show_groups_and_exit,
23 "version|v" => sub { print "This is Mr. Shell version $App::MrShell::VERSION.\n\n"; pod2usage() },
24 "host|H=s@" => \@hosts,
25 "conf|c:s" => \$conf, # optional value
26 "noesc|N" => \$no_esc,
27 "log|l=s" => \$log,
28 "trunc|t" => \$trunc, # whether to truncate the log file
29 'debug|d:i' => \$debug,
30 "shell|s=s" => \$shell,
31 "help|h" => sub { pod2usage(-verbose=>1) },
33 ) or pod2usage(); do{ warn "ERROR: command required\n"; pod2usage()} unless @ARGV or $show_groups_and_exit;
35 no warnings 'uninitialized'; ## no critic: this is a stupid warning anyway
36 if( $ARGV[0] =~ m/^\@/ ) {
37 while( $ARGV[0] =~ m/^\@/) {
38 @orig_ARGV = map { $_ eq $ARGV[0] ? ('-H'=>$_) : ($_) } @orig_ARGV;
39 shift @ARGV;
42 @ARGV = @orig_ARGV; ## no critic: no I really really meant to change the global one, thanks
43 redo MAGICALLY_UNDERSTAND_HOSTGROUPS;
47 my $mrsh = App::MrShell->new->set_usage_error("pod2usage");
49 $mrsh->read_config($conf) if $conf;
50 show_groups_and_exit() if $show_groups_and_exit;
51 $mrsh->set_logfile_option($log, $trunc) if $log;
52 $mrsh->set_shell_command_option($shell) if $shell;
53 $mrsh->set_debug_option($debug) if defined $debug;
54 $mrsh->set_no_command_escapes_option if $no_esc;
56 $mrsh->set_hosts(@hosts) # tell Mr. Shell where to run things
57 ->queue_command(@ARGV) # queue a command for whatever hosts are set
58 ->run_queue; # tell POE to do what POE does
60 sub show_groups_and_exit {
61 my %groups = $mrsh->groups;
62 my @groups = sort keys %groups;
64 if( my @h = grep {s/^@//} @hosts ) { ## no critic: bah
65 @groups = @h;
68 unless(@groups) {
69 print "(no gropus to show)\n";
70 exit 0;
73 my $len = length $groups[0];
74 for(@groups) { $len = length $_ if length $_ > $len }
76 $len ++;
77 for(@groups) {
78 printf '%-*s %s', $len, "$_:", "@{ $groups{$_} }\n";
81 exit 0;
84 __END__
86 =head1 NAME
88 mrsh - Mr. Shell runs a command on multiple hosts
90 =head1 SYNOPSIS
92 mrsh [options] [--] command
93 --version -v: print the version, help, and exit
94 --help -h: print extended help and exit
96 --host -H: specify a host or group to run commands on
97 --conf -c: specify config file location, or skip configs
98 --log -l: specify a logfile location
99 --trunc -t: overwrite logfile, rather than append
100 --shell -s: change the (remote-)shell command
101 --noesc -N: do not escape the sub commands during host-routing mode
102 --groups -g: show groups and exit
103 --list : (nickname for --groups)
104 -- : not strictly an option, but good to put before commands
106 =head1 DESCRIPTION
108 The B<-H> has some special magic concerning L</[groups]>. If a group is
109 specified before any other options or options arguments arguments (but possibly
110 after other groups), it will automatically be expanded to have an imaginary
111 B<-H> before it. Example:
113 # list /tmp on all the hosts in @gr1
114 mrsh @gr1 -- ls -al /tmp
116 # list /tmp on all the hosts in @gr1 with a logfile
117 mrsh --log logfile @gr1 -- ls -al /tmp
119 =over
121 =item B<-->
123 Sometimes the option processor get confused by switches and options for the
124 command being sent to the remote hosts. B<--> tells the options parser to quit
125 looking.
127 =item B<--groups> B<-g> B<--list>
129 List the groups and their hosts and exit. Will use groups listed in B<-H>
130 switches if applicable (ignoring B<-H> arguments that are not groups).
132 =item B<--host> B<-H>
134 Names of hosts or L</[groups]> upon which to run commands. Groups are prefixed
135 with an C<@> character and can only be specified in the configuration file (see
136 L</CONFIG FILE>).
138 Host and group specifications that overlap are reduced to a unique set, so if
139 C<@localhosts> contains C<host1> and C<@desktops> contains C<host1>, and for
140 whatever reason C<-H host1> is also specified on the command line ... C<host1>
141 will only appear in the hosts list just the one time.
143 Hostnames may be subtracted from any lists provided (via groups or B<-H>) by
144 prefixing the hostname (but not group) with C<->.
146 For instance, if C<@hosts> contains a host named C<host1>> and C<host1>> is
147 unavailable, users might type something like this:
149 mrsh @hosts -H-host1 uptime
151 =item B<--conf> B<-c>
153 By default, L<mrsh> will look for C<.mrshrc> the user's home directory. Users
154 may change the location with this switch. The switch takes an optoinal
155 argument, the location. When a location is not specified, it disables the
156 loading of any config files.
158 Caveat: careful that -c doesn't slurp up the next word on the command line. It
159 wants to eat your arguments.
161 =item B<--log> B<-l> B<--trunc> B<-t>
163 L<mrsh> doesn't keep any logs by default. Users may specify a logfile location
164 to start logging. Logs will be appended (even between runs) unless the truncate
165 option is specified -- in which case, the logfile will simply be overwritten
166 instead.
168 =item B<--shell> B<-s>
170 By default, L<mrsh> uses the following command as the shell command.
172 ssh -o BatchMode yes -o StrictHostKeyChecking no -o ConnectTimeout 20 [%u]-l []%u %h
174 The C<%h> will be replaced by the hostname(s) during execution
175 (see L</COMMAND ESCAPES>).
177 Almost any shell command will work, see C<t/05_touch_things.t> in the
178 distribution for using perl as a "shell" to touch files. Arguments to B<-s> are
179 space delimited but understand very simple quoting:
181 =item B<--noesc> B<-N>
183 During host routing mode, L<mrsh> will escape spaces and backslashes in a way
184 that openssh (L<http://openssh.com/>) will understand correctly. That behavior
185 can be completely disabled with this option.
187 =back
189 =head1 COMMAND ESCAPES
191 These things will be replaced before forking the commands on the remote hosts.
192 There aren't many of these yet, but there will likely be more in the future.
194 =over
196 =item B<%c>
198 The command number.
200 =item B<%h>
202 The hostname. The hostname escape supports a special host routing protocol.
203 Hostnames that contain the routing character will be expanded to magically
204 create sub-commands as needed to connect I<through> hosts while executing
205 commands.
207 When expanding a host route, all C<%h> will be replaced with the elements of
208 the command array up to that escape, plus the hostname, for each host in the
209 hosts route.
211 This expansion also optionally (see B<-N> above) expands spaces and slashes to
212 escaped values compatible with openssh (L<http://openssh.com/>).
214 This is perhaps more clear by example.
216 Let's say this is the command in question.
218 ssh -o 'BatchMode Yes' %h 'ls -ald /tmp/'
220 And let's say our hostname is C<corky!wisp>, then the command becomes:
222 ssh -o 'BatchMode Yes' corky ssh -o 'Batchmode\ Yes' wisp 'ls\\ -ald\\ /tmp'
224 =item B<%u>
226 Replaced with the username, if applicable. When hostname contains an C<@>
227 character, for example C<jettero@corky>, the portion before the C<@> is
228 considered a username.
230 =item conditional replacement
232 If an element in a command exists in the form C<[%u]-l>, then the argument
233 C<-l> will only appear in the argument list when C<%u> has a value. If an
234 arguemnt of the form C<[]%u> (C<[%u]%u> works identically), it will only appear
235 in the argument list when C<%u> has a value.
237 The following command is expanded as follows for C<jettero@corky> and C<corky>
238 respectively.
240 ssh [%u]-l []%u %h
241 ssh -l jettero corky # for jettero@corky
242 ssh corky # for corky
244 =back
246 =head1 CONFIG FILE
248 The config file is loaded using the L<Config::Tiny> module, which supports
249 basic "standard" C<.ini> files. L<mrsh> reads two sections for values and
250 ignores all values it doesn't understand.
252 =head2 B<[options]>
254 =over
256 =item B<default-hosts>
258 When no hosts are specified for a command, L<mrsh> will seek to use these hosts
259 and L</[groups]> instead.
261 =item B<shell-command>
263 This is the above B<-s> setting, which allows changing the shell command.
265 =item B<no-command-escapes>
267 This is the above B<-N> setting, which disables escaping of arguments during
268 host-routing mode.
270 =back
272 =head2 B<[groups]>
274 The B<[groups]> section can contain as many hostname values as ... your platform
275 as memory. Groups are expanded by pre-fixing with an C<@> character when passing
276 hostnames to B<-H> or via the C<default-hosts> option above.
278 Hosts and host routes are space separated.
280 If a group contains references to other groups, it will automatically be
281 recursively replaced. This recursion naively assumes there are no loops, but
282 automatically stops about 30 deep. If you get unexpected results, this could
283 be the problem.
285 =head2 EXAMPLE CONFIG
287 [options]
288 default-hosts = @debian-desktops
289 shell-command = ssh -o 'BatchMode Yes' [%u]-l []%u %h
291 [groups]
292 debian-desktops = wisp corky razor
294 =head1 AUTHOR
296 Paul Miller C<< <jettero@cpan.org >>
298 L<http://github.com/jettero>
300 =head1 THANKS
302 Dennis Boone -- L<http://github.com/drboone>
304 =head1 COPYRIGHT
306 Copyright 2009-2010 - Paul Miller
308 Released as GPL, like the original Mr. Shell circa 1997.
310 =head1 SEE ALSO
312 ssh(1), perl(1), L<App::MrShell>, L<POE>, L<POE::Wheel::Run>
314 =cut