Add in bash expansions on hostnames

Allow hostnames to be expanded using bash where a { is in the host name definition

Github issue #53
This commit is contained in:
Duncan Ferguson 2016-02-13 08:48:20 +00:00
parent b9738f7142
commit b330457f99
6 changed files with 150 additions and 2 deletions

View file

@ -1,6 +1,7 @@
4.05_01 0000-00-00 Duncan Ferguson <duncan_ferguson@user.sf.net> 4.05_01 0000-00-00 Duncan Ferguson <duncan_ferguson@user.sf.net>
- Failure to find the terminal binary should not be fatal - Failure to find the terminal binary should not be fatal
- Fix processing of '--extra_tag_file' and its configuration item (Github issue #51) - Fix processing of '--extra_tag_file' and its configuration item (Github issue #51)
- Add bash shell expansion on host names containing a '{' character (Github issue #53)
4.05 2015-11-28 Duncan Ferguson <duncan_ferguson@user.sf.net> 4.05 2015-11-28 Duncan Ferguson <duncan_ferguson@user.sf.net>
- Change default key_quit from 'Control-q' to 'Alt-q' (Github issue #50) - Change default key_quit from 'Control-q' to 'Alt-q' (Github issue #50)

View file

@ -153,6 +153,8 @@ sub register_host {
my ( $self, $node, @tags ) = @_; my ( $self, $node, @tags ) = @_;
$self->debug( 2, "Registering node $node on tags:", join( ' ', @tags ) ); $self->debug( 2, "Registering node $node on tags:", join( ' ', @tags ) );
@tags = $self->expand_glob( 'node', $node, @tags );
foreach my $tag (@tags) { foreach my $tag (@tags) {
if ( $self->{tags}->{$tag} ) { if ( $self->{tags}->{$tag} ) {
$self->{tags}->{$tag} $self->{tags}->{$tag}
@ -170,6 +172,11 @@ sub register_host {
sub register_tag { sub register_tag {
my ( $self, $tag, @nodes ) = @_; my ( $self, $tag, @nodes ) = @_;
#warn "b4 nodes=@nodes";
@nodes = $self->expand_glob( 'tag', $tag, @nodes );
#warn "af nodes=@nodes";
$self->debug( 2, "Registering tag $tag: ", join( ' ', @nodes ) ); $self->debug( 2, "Registering tag $tag: ", join( ' ', @nodes ) );
$self->{tags}->{$tag} = \@nodes; $self->{tags}->{$tag} = \@nodes;
@ -177,6 +184,37 @@ sub register_tag {
return $self; return $self;
} }
sub expand_glob {
my ( $self, $type, $name, @items ) = @_;
my @expanded;
# skip expanding anything that appears to have nasty metachars
if ( !grep {m/[\`\!\$;]/} @items ) {
if ( grep {m/[{]/} @items ) {
#@expanded = split / /, `/bin/bash -c 'shopt -s extglob\n echo @items'`;
my $cmd = $self->parent->config->{shell_expansion};
$cmd =~ s/%items%/@items/;
@expanded = split / /, `$cmd`;
chomp(@expanded);
}
else {
@expanded = map { glob $_ } @items;
}
}
else {
warn(
$self->loc(
"Bad characters picked up in [_1] '[_2]': [_3]",
$type, $name, join( ' ', @items )
),
);
}
return @expanded;
}
sub get_tag { sub get_tag {
my ( $self, $tag ) = @_; my ( $self, $tag ) = @_;
@ -287,6 +325,10 @@ Return an array of all available tag names
Returns a hash of all tag data. Returns a hash of all tag data.
=item @tags = $cluster->expand_glob( $type, $name, @items );
Use shell expansion against each item in @items, where $type is either 'node', or 'tag' and $name is the node or tag name. These attributes are presented to the user in the event of an issue with the expanion to track down the source.
=back =back
=head1 AUTHOR =head1 AUTHOR

View file

@ -102,6 +102,8 @@ my %default_config = (
send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu', send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu',
shell_expansion => "/bin/bash -c 'shopt -s extglob\n echo %items%'",
# don't set username here as takes precendence over ssh config # don't set username here as takes precendence over ssh config
user => '', user => '',
); );

View file

@ -563,11 +563,23 @@ would replace the <Alt-n> with the client's name in each window.}
output $self->loc( output $self->loc(
q{All comments (marked by a #) and blank lines are ignored. Tags may be nested, but be aware of using recursive tags as they are not checked for.} q{All comments (marked by a #) and blank lines are ignored. Tags may be nested, but be aware of using recursive tags as they are not checked for.}
); );
output $self->loc(q{Servers can be defined using bash shell expansion:});
output 'C<< webservers websvr{a,b,c} >>';
output $self->loc(q{would be expanded to});
output 'C<< webservers websvra websvrb websvrc >>';
output $self->loc(q{and});
output 'C<< webservers websvr{6..9} >>';
output $self->loc(q{would be expanded to});
output 'C<< webservers websvr6 websvr7 websvr8 websvr9 >>';
output $self->loc(
q{B<NOTE:> this requires [_1] to be installed on your system (see [_2] configuration option },
'/bin/bash', 'C<shell_expansion>'
);
output $self->loc( output $self->loc(
q{Extra cluster files may also be specified either as an option on the command line (see [_1]) or in the user's [_2] file (see [_3] configuration option).}, q{Extra cluster files may also be specified either as an option on the command line (see [_1]) or in the user's [_2] file (see [_3] configuration option).},
'C<cluster-file>', 'C<cluster-file>',
'F<$HOME/.clusterssh/config>', 'F<$HOME/.clusterssh/config>',
'C<extra_cluster_file>' 'L</extra_cluster_file>'
); );
output $self->loc( output $self->loc(
'B<NOTE:> the last tag read overwrites any pre-existing tag of that name.' 'B<NOTE:> the last tag read overwrites any pre-existing tag of that name.'
@ -760,6 +772,13 @@ B<NOTE:> Any "generic" change to the method (e.g., specifying the ssh port to us
q{Number of pixels from the screen's side to reserve when calculating screen geometry for tiling. Setting this to something like 50 will help keep cssh from positioning windows over your window manager's menu bar if it draws one at that side of the screen.} q{Number of pixels from the screen's side to reserve when calculating screen geometry for tiling. Setting this to something like 50 will help keep cssh from positioning windows over your window manager's menu bar if it draws one at that side of the screen.}
); );
output
q{=item shell_expansion = /bin/bash -c 'shopt -s extglob\n echo %items%'"};
output $self->loc(
q{Command used to expand a given string (provided by the macro [_1]) - used for expanding host names when a [_2] is in the name. See [_3]},
'C<%items%>', 'C<{>', 'L<bash/EXPANSION>'
);
output '=item terminal = /path/to/xterm'; output '=item terminal = /path/to/xterm';
output $self->loc(q{Path to the X-Windows terminal used for the client.}); output $self->loc(q{Path to the X-Windows terminal used for the client.});

View file

@ -110,6 +110,8 @@ Readonly::Hash my %default_config => {
send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu', send_menu_xml_file => $ENV{HOME} . '/.clusterssh/send_menu',
shell_expansion => "/bin/bash -c 'shopt -s extglob\n echo %items%'",
# other bits inheritted from App::ClusterSSH::Base # other bits inheritted from App::ClusterSSH::Base
debug => 0, debug => 0,
lang => 'en', lang => 'en',
@ -567,6 +569,7 @@ screen_reserve_top=0
send_menu_xml_file=} . $ENV{HOME} . qq{/.clusterssh/send_menu send_menu_xml_file=} . $ENV{HOME} . qq{/.clusterssh/send_menu
sftp=sftp sftp=sftp
sftp_args= sftp_args=
shell_expansion=/bin/bash -c \'shopt -s extglob\n echo %items%\'
show_history=0 show_history=0
ssh=ssh ssh=ssh
ssh_args= ssh_args=

View file

@ -55,7 +55,8 @@ BEGIN {
use_ok("App::ClusterSSH::Cluster") || BAIL_OUT('failed to use module'); use_ok("App::ClusterSSH::Cluster") || BAIL_OUT('failed to use module');
} }
my $mock_object = Test::ClusterSSH::Mock->new(); my $mock_object = Test::ClusterSSH::Mock->new(
shell_expansion => "/bin/bash -c 'shopt -s extglob\n echo %items%'", );
my $cluster1 = App::ClusterSSH::Cluster->new( parent => $mock_object ); my $cluster1 = App::ClusterSSH::Cluster->new( parent => $mock_object );
isa_ok( $cluster1, 'App::ClusterSSH::Cluster' ); isa_ok( $cluster1, 'App::ClusterSSH::Cluster' );
@ -260,6 +261,86 @@ is( $trap->leaveby, 'return', 'exit okay on get_tag_entries' );
is( $trap->stdout, '', 'no stdout for get_tag_entries' ); is( $trap->stdout, '', 'no stdout for get_tag_entries' );
is( $trap->stderr, '', 'no stderr for get_tag_entries' ); is( $trap->stderr, '', 'no stderr for get_tag_entries' );
# test bash expansion
my @expected = ( 'aa', 'ab', 'ac' );
$cluster1->register_tag( 'glob1', 'a{a,b,c}' );
@got = $cluster2->get_tag('glob1');
is_deeply( \@got, \@expected, 'glob1 expansion, words' )
or diag explain @got;
@expected = ( 'ax', 'ay', 'az' );
$cluster1->register_tag( 'glob2', 'a{x..z}' );
@got = $cluster2->get_tag('glob2');
is_deeply( \@got, \@expected, 'glob2 expansion, words' )
or diag explain @got;
@expected = ( 'b1', 'b2', 'b3' );
$cluster1->register_tag( 'glob3', 'b{1..3}' );
@got = $cluster2->get_tag('glob3');
is_deeply( \@got, \@expected, 'glob3 expansion, number range' )
or diag explain @got;
@expected = ( 'ca', 'cb', 'cc', 'd7', 'd8', 'd9' );
$cluster1->register_tag( 'glob4', 'c{a..c}', 'd{7..9}' );
@got = $cluster2->get_tag('glob4');
is_deeply( \@got, \@expected, 'glob4 expansion, mixed' )
or diag explain @got;
# make sure reasonable expansions get through with no nasty metachars
@expected = ( 'cd..f}', 'c{a..c' );
$cluster1->register_tag( 'glob5', 'c{a..c', 'cd..f}' );
@got = $cluster2->get_tag('glob5');
is_deeply( \@got, \@expected, 'glob5 expansion, mixed' )
or diag explain @got;
@expected = ();
trap {
$cluster1->register_tag( 'glob6', 'c{a..c} ; echo NASTY' );
};
is( $trap->leaveby, 'return', 'didnt die on nasty chars' );
is( $trap->die, undef, 'didnt die on nasty chars' );
is( $trap->stdout, q{}, 'Expecting no STDOUT' );
like(
$trap->stderr,
qr/Bad characters picked up in tag 'glob6':.*/,
'warned on nasty chars'
);
@got = $cluster2->get_tag('glob6');
is_deeply( \@got, \@expected, 'glob6 expansion, nasty chars' )
or diag explain @got;
@expected = ();
trap {
$cluster1->register_tag( 'glob7', 'c{a..b} `echo NASTY`' );
};
is( $trap->leaveby, 'return', 'didnt die on nasty chars' );
is( $trap->die, undef, 'didnt die on nasty chars' );
is( $trap->stdout, q{}, 'Expecting no STDOUT' );
like(
$trap->stderr,
qr/Bad characters picked up in tag 'glob7':.*/,
'warned on nasty chars'
);
@got = $cluster2->get_tag('glob7');
is_deeply( \@got, \@expected, 'glob7 expansion, nasty chars' )
or diag explain @got;
@expected = ();
trap {
$cluster1->register_tag( 'glob8', 'c{a..b} $!', );
};
is( $trap->leaveby, 'return', 'didnt die on nasty chars' );
is( $trap->die, undef, 'didnt die on nasty chars' );
is( $trap->stdout, q{}, 'Expecting no STDOUT' );
like(
$trap->stderr,
qr/Bad characters picked up in tag 'glob8':.*/,
'warned on nasty chars'
);
@got = $cluster2->get_tag('glob8');
is_deeply( \@got, \@expected, 'glob8 expansion, nasty chars' )
or diag explain @got;
done_testing(); done_testing();
sub test_expected { sub test_expected {