Skip to content

Instantly share code, notes, and snippets.

@kckrinke
Created August 18, 2017 06:41
Show Gist options
  • Save kckrinke/f1d202bdbda1e417508acb40741b76a1 to your computer and use it in GitHub Desktop.
Save kckrinke/f1d202bdbda1e417508acb40741b76a1 to your computer and use it in GitHub Desktop.
CLI for listing, viewing profiles and modifying ~/.ssh/config
#!/usr/bin/env perl
use strict;
use warnings;
use diagnostics;
use Getopt::Long;
use Pod::Usage;
use constant {true=>1,false=>0};
our $SSH_CONFIG_PATH = $ENV{'HOME'} . '/.ssh/config';
my $usage = false;
my $man = false;
my $help = false;
our $QUIET = false;
GetOptions
( 'h' => \$usage,
'help' => \$help,
'man' => \$man,
'q|quiet' => \$QUIET,
'C|config=s' => \$SSH_CONFIG_PATH,
)
or pod2usage(0);
pod2usage(-verbose => 0) if $usage;
pod2usage(-verbose => 1) if $help;
pod2usage(-verbose => 2) if $man;
pod2usage(-message => "File Not Found: ".$SSH_CONFIG_PATH)
unless -f $SSH_CONFIG_PATH;
our (%PROFILES) = get_profiles_from_file($SSH_CONFIG_PATH);
if ((@ARGV == 0) && (-t STDIN)) {
print_profiles_list(list_profile_names('.'));
} else {
my $operation = shift @ARGV;
if ($operation eq "list") {
if (@ARGV==1) {
print_profiles_list(list_profile_names($ARGV[0]));
} elsif (@ARGV>=1) {
pod2usage
( -message => "The \"list\" sub-command requires a single regular expression pattern to search with" );
} else {
print_profiles_list(list_profile_names('.'));
}
} elsif ($operation eq "show") {
my (@keys) = (@ARGV) ? @ARGV : (sort keys %PROFILES);
foreach my $key (@keys) {
unless (print_profile($key)) {
my (@names) = list_profile_names($key);
foreach my $name (@names) {
print_profile($name);
}
}
}
} elsif ($operation eq "check") {
if (@ARGV == 1) {
my (@names) = list_profile_names($ARGV[0]);
if (@names) {
print_profiles_list(@names);
} else {
stdmsg("No profiles found matching: \"".$ARGV[0]."\"\n");
exit(4);
}
} else {
pod2usage
( -message => 'The "check" sub-command requires a single profile name to check for' );
}
} elsif ($operation eq "edit") {
my $editor = undef;
# look for user's EDITOR environment
if (exists $ENV{EDITOR}) {
if (-f $ENV{EDITOR}) {
$editor = $ENV{EDITOR};
}
$editor = find_bin_path($ENV{EDITOR});
}
# look for 'editor' program (usually a wrapper)
$editor = find_bin_path('editor')
unless defined $editor;
# stop the esoteric-keybound-editor-wars, make nano the defacto unix
# default command line editor
$editor = find_bin_path('nano')
unless defined $editor;
# if emacs is installed - they probably really really want that
$editor = find_bin_path('emacs')
unless defined $editor;
# at least it's not vi
$editor = find_bin_path('vim')
unless defined $editor;
# vi - the real man's last ditch effort
$editor = find_bin_path('vi')
unless defined $editor;
# if we haven't found it by now
if (defined $editor) {
exec($editor,$SSH_CONFIG_PATH)
} else {
pod2usage(-message=>"Please set your EDITOR environment variable if you want to edit the file: ".$SSH_CONFIG_PATH);
}
} else {
pod2usage(-verbose=>0);
}
}
exit(0);
#
# Sub Functions
#
sub list_profile_names {
my ($pattern) = @_;
return sort
grep { m/$pattern/ }
keys %PROFILES;
}
sub print_profiles_list {
my (@keys) = @_;
if (@keys) {
stdout( join("\n",@keys)."\n" );
return true;
}
return false;
}
sub find_bin_path {
my ($command) = @_;
if (exists $ENV{PATH}) {
foreach my $dir ( split(m/\:/,$ENV{PATH}) ) {
return $dir . '/' . $command
if -x $dir . '/' . $command;
}
}
return undef;
}
sub stdmsg {
foreach my $line (@_) {
stdout( $line )
unless $QUIET;
}
}
sub stdout {
foreach my $line (@_) {
print STDOUT $line;
}
}
sub stderr {
foreach my $line (@_) {
print STDERR $line;
}
}
sub print_profile {
my ($key) = @_;
if (exists $PROFILES{$key}) {
my $profile = $PROFILES{$key};
stdout( "Host ".$key."\n" );
stdout( $profile->{_raw}."\n" );
stdout( "\n" );
return true;
}
return false;
}
sub get_profiles_from_file {
my ($input_file) = @_;
my %profiles = ();
open( FILE, '<', $input_file )
or die 'Could not open file: ' . $!;
my $whole_file = '';
{
local $/;
$whole_file = <FILE>;
}
my (%blobs) = $whole_file =~ m!^Host ([_\w]+?)\s*\r??\n(.+?)\s*\r??\n(?:\s*\r??\n|\Z)!msg;
foreach my $key (keys %blobs) {
my %profile = ();
$profile{_raw} = $blobs{$key};
foreach my $line (split(m/\r??\n/,$blobs{$key})) {
if ($line =~ m!^\s*(\w+)\s+(.+?)\s*$!) {
$profile{$1} = $2;
}
}
$profiles{$key} = \%profile;
}
return %profiles;
}
__END__
=head1 NAME
ssh-profile - CLI for listing, viewing profiles and modifying ~/.ssh/config
=head1 SYNOPSIS
ssh-profile [-h|--help|--man] [-C|--config=/path] [-q|--quiet] <operation>
=head1 OPTIONS
=over 8
=item B<-h>
Print only the synopsis message and exit.
=item B<--help>
Print a brief help message and exit.
=item B<--man>
Prints the manual page and exits.
=item B<--config=/path>
Specify the path to an OpenSSH config file. Defaults: ~/.ssh/config
=item B<-q|--quiet>
Be less verbose about the operations being performed.
=back
=head2 OPERATIONS
If no arguments are given, beyond the base options, then the default behaviour
is to perform the default "list" operation.
=over 8
=item B<list [pattern]>
Display a listing of just the profile names matching the given (regex) pattern.
With no arguments, displays a sorted listing of all profile names.
=item B<show [profile]>
Display one or more profile definitions. If no profile name given, all profiles
are displayed. Profile name can also be a Perl regular expression.
=item B<check pattern>
Exit with code 0 if given pattern matches any profile names, code 4 if not
found. Pattern must be given and can be any valid Perl regular expression.
=item B<edit>
Open the SSH config file in a text editor. Set EDITOR evironment variable to
specify the prefered program.
=back
=head1 DESCRIPTION
B<ssh-profile> is a command line interface for listing, viewing Host profiles and
modifying the ~/.ssh/config file.
=head1 DEPENDENCIES
=over
=item OpenSSH
https://www.openssh.com/
=item EDITOR Environment Variable
For edit functionality, a text editor must be present on the system. The
default heuristics for selecting which editor to use can be overridden via
setting the EDITOR environment variable prior to execution.
=back
=head1 COPYRIGHT
ssh-profile - CLI for listing, viewing profiles and modifying ~/.ssh/config
Copyright (C) 2017 Kevin C. Krinke <kevin@krinke.ca>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; specifically
version 2.1 of the License and no other version.
This program 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
=cut
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment