Config-AWS

 view release on metacpan or  search on metacpan

lib/Config/AWS.pm  view on Meta::CPAN

package Config::AWS;
# ABSTRACT: Parse AWS config files

use strict;
use warnings;

use Carp ();
use Ref::Util;
use Scalar::Util;
use Exporter::Shiny qw(
    read
    read_all
    list_profiles
    read_file
    read_string
    read_handle
    config_file
    credentials_file
    default_profile
);

our $VERSION = '0.12';
our %EXPORT_TAGS = (
    ini  => [qw( read_file read_string read_handle )],
    aws  => [qw( config_file default_profile credentials_file )],
    read => [qw( read read_all list_profiles )],
    all  => [qw( :ini :aws :read )],
);

# Internal methods for parsing and validation

my $prepare = sub {
    # Input is given argument or credentials file (if exists) or config file.
    my $input = shift // do {
        my $cred_file = credentials_file();
        -r $cred_file ? $cred_file : config_file();
    };

    unless ( Ref::Util::is_ref $input ) {
        require Path::Tiny;
        my @lines = eval { Path::Tiny::path( $input )->lines };
        if ($@) {
            Carp::croak "Cannot read from $input: $@->{err}"
                if ref $@ && $@->isa('Path::Tiny::Error');
            Carp::croak $@;
        }
        return \@lines;
    }

    return [ $input->getlines ] if Scalar::Util::openhandle $input;

    if ( Ref::Util::is_blessed_ref $input ) {
        return [ $input->slurp ] if $input->isa('Path::Class::File');
        return [ $input->lines ] if $input->isa('Path::Tiny');

        Carp::croak 'Cannot read from objects of type ', ref $input;
    }

    return [ split /\R/, $$input ] if Ref::Util::is_scalarref $input;
    return $input                  if Ref::Util::is_arrayref  $input;

    Carp::croak "Could not use $input as source for ", (caller 1)[3];
};

my $read = sub {
    my ($lines, $target_profile) = @_;

    Carp::carp 'Reading config with only one line or less. Faulty input?'
        if @$lines <= 1;

    my $hash    = {};
    my $nested  = {};
    my $profile = '';

    for my $i (0 .. $#$lines) {
        my $line = $lines->[$i];
        $line =~ s/\R$//;

        if ($line =~ /^\[(?:profile )?([\w\/.@%:_-]+)\]/) {
            $profile = $1;
            next;
        }

        next if $target_profile && $profile ne $target_profile;

        next unless my ($indent, $key, $value) = $line =~ /^(\s*)(\w+)\s*=\s*(.*)/;

        if (length $indent) {
            $nested->{$key} = $value;
        }
        else {
            # Add nested hash if the value is empty and next line is indented.
            $hash->{$profile}{$key}
                = !length $value && $i < $#$lines && $lines->[$i + 1] =~ /^\s+/
                ? $nested : $value;

            $nested = {} if keys %$nested;
        }
    }

    return $target_profile ? ( $hash->{$target_profile} // {} ) : $hash;
};

# Config parsing interface

sub read {
    $read->( $prepare->(shift), shift // default_profile() );
}

sub read_all {
    $read->( &$prepare );
}

sub list_profiles {
    map /^\[(?:profile )?([\w\/.@%:_-]+)\]/, @{ &$prepare };
}

# AWS information methods

sub default_profile {
    $ENV{AWS_DEFAULT_PROFILE} // 'default';
}

sub credentials_file {
    $ENV{AWS_SHARED_CREDENTIALS_FILE} // ( glob '~/.aws/credentials' )[0];
}

sub config_file {
    $ENV{AWS_CONFIG_FILE} // ( glob '~/.aws/config' )[0];
}

# Methods for compatibility with Config::INI interface

sub read_file {
    Carp::croak 'Filename is missing' unless @_;
    Carp::croak 'Argument was not a string' if Ref::Util::is_ref $_[0];
    $read->( $prepare->(shift), @_ );
}

sub read_string {
    Carp::croak 'String is missing' unless @_;
    Carp::croak 'Argument was not a string' if Ref::Util::is_ref $_[0];
    $read->( [ split /\R/, shift // '' ], @_ );
}

sub read_handle {
    Carp::croak 'Handle is missing' unless @_;
    Carp::croak 'Argument was not a handle' unless Scalar::Util::openhandle $_[0];
    $read->( [ shift->getlines ], @_ );
}

1;

__END__

=encoding utf8

=head1 NAME

Config::AWS - Parse AWS config files

=head1 SYNOPSIS

    use Config::AWS ':all';

    # Read the data for a specific profile
    $config = read( $source, $profile );

    # Or read the default profile from the default file
    $config = read();

    # Which is the same as
    $config = read(
        -r credentials_file() ? credentials_file() : config_file(),
        default_profile()
    );

    # Read all of the profiles from a file
    $profiles = read_all( $source );

    # Or if you have cycles to burn
    $profiles = {
        map { $_ => read( $source, $_ ) } list_profiles( $source )
    };

=head1 DESCRIPTION

Config::AWS is a small distribution with generic methods to correctly parse
the contents of config files for the AWS CLI client as described in
L<the AWS documentation|https://docs.aws.amazon.com/cli/latest/topic/config-vars.html>.

Although it is common to see these files parsed as standard INI files, this
is not appropriate since AWS config files have an idiosyncratic format for
nested values (as shown in the link above).

Standard INI parsers (like L<Config::INI>) are not made to parse this sort of
structure (nor should they). So Config::AWS exists to provide a suitable
and lightweight ad-hoc parser that can be used in other applications.

=head1 ROUTINES

Config::AWS does not export anything by default. All the functions



( run in 2.048 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )