The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl -T

use strict;
use warnings;
use feature 'say';
use URI;

binmode(STDOUT => ':encoding(utf8)');


=head1 NAME

simpledb - Amazon SimpleDB command line interface

=head1 VERSION

version 1.0600

=head1 SYNOPSIS

 simpledb [opts] create-domain DOMAIN
 simpledb [opts] delete-domain DOMAIN
 simpledb [opts] list-domains

 simpledb [opts] put         DOMAIN ITEM [NAME=VALUE]...
 simpledb [opts] put-replace DOMAIN ITEM [NAME=VALUE]...
 simpledb [opts] get         DOMAIN ITEM
 simpledb [opts] delete      DOMAIN ITEM [NAME[=VALUE]]...

 simpledb [opts] select SELECTEXPRESSION

=head1 OPTIONS

 --help         Print help and exit.

 --http-proxy-host

 --http-proxy-port

 --sdb-service-url URL
      Defaults to https://sdb.amazonaws.com/
      Alternatives: http://docs.aws.amazon.com/general/latest/gr/rande.html#sdb_region

 --aws-access-key-id KEY
                AWS access key id
                [Defaults to $AWS_ACCESS_KEY_ID environment variable]

 --aws-secret-access-key SECRETKEY
                AWS secret access key
                [Defaults to $AWS_SECRET_ACCESS_KEY environment variable]

 --max COUNT
                Maximum number of domains/items to retrieve and list.
                [Defaults to all]

 --separator STRING
                Separator between attribute name and value.
                [Defaults to equals (=)]

=head1 ARGUMENTS

 DOMAIN            Domain name
 ITEM              Item name
 NAME              Attribute name
 VALUE             Attribute value
 SELECTEXPRESSION  SimpleDB select expression

=head1 DESCRIPTION

This utility provides a simple command line interface to most Amazon
SimpleDB (SDB) actions.

=head1 EXAMPLES

# The following examples assume you have set these environment variables:

  export AWS_ACCESS_KEY_ID=...
  export AWS_SECRET_ACCESS_KEY=...
  export SDB_SERVICE_URL=https://sdb.eu-west-1.amazonaws.com

# Create a new SimpleDB domain:

  simpledb create-domain mydomain

# List the domains for this account:

  simpledb list-domains

# Create some items with attribute name=value pairs:

  simpledb put mydomain item1 key1=valueA key2=value2 x=why

  simpledb put mydomain item2 key1=valueB key2=value2 y=zee

# Add another value for an attribute on an item:

  simpledb put mydomain item2 y=zed when=now who=you

# Replace all values for specific attributes on an item:

  simpledb put-replace mydomain item1 key1=value1 newkey=newvalue

# Delete all values for specific attributes on an item:

  simpledb delete mydomain item1 x

# Delete specific values for specific attributes on an item:

  simpledb delete mydomain item2 who=you

# List all item names in a domain  - note backquotes around domain

  simpledb select 'select itemName() from `my-domain`'

# List all items and their attributes matching a given select query:

  simpledb select 'select * from mydomain where key2="value2"'

# List all attributes on an item:

  simpledb get mydomain item1

  simpledb get mydomain item2

# Delete the entire SimpleDB domain including all items and attributes:

  simpledb delete-domain mydomain

=head1 ENVIRONMENT

 AWS_ACCESS_KEY_ID
      Default AWS access key id

 AWS_SECRET_ACCESS_KEY
      Default AWS secret access key

 SDB_SERVICE_URL
      Default https://sdb.amazonaws.com/
      Alternatives:
  http://docs.aws.amazon.com/general/latest/gr/rande.html#sdb_region

=head1 FILES

 $HOME/.awssecret
    If the above fail, then the keys are sought here in the
    format expected by the "aws" toolkit (one per line):
      access_key_id
      secret_access_key

 /etc/passwd-s3fs
    If all of the above fail, then the keys are sought
    here in the format expected by s3fs (colon separated):
      access_key_id:secret_access_key


=head1 CAVEATS

As currently written this tool does not support keys containing equal
signs (=).

Output will be difficult to parse if the values contain newlines.

=head1 HISTORY

 2013-09-11 Andrew Solomon <andrew at illywhacker dot net>
 - Rebased on SimpleDB::Client
 - Removed proxying
 - Removed attribute parameters from "get"

 2010-04-20 Eric Hammond <ehammond@thinksome.com>
 - Removed support for "query".  Please migrate to "select"

 2009-09-01 Peter Kaminski <kaminski@istori.com>
 - Added utf8 binmode for STDOUT
 - Added select method

 2009-03-16 Eric Hammond <ehammond@thinksome.com>
 - Fix --max options and large result sets without --max
   http://code.google.com/p/amazon-simpledb-cli/issues/detail?id=2

 2008-06-09 Eric Hammond <ehammond@thinksome.com>
 - Fallback to finding keys in $HOME/.awssecret or /etc/passwd-s3fs

 2008-06-03 Eric Hammond <ehammond@thinksome.com>
 - Completed --max option
 - bugfix: Corrected --aws-secret-access-key option spelling

 2008-05-26 Eric Hammond <ehammond@thinksome.com>
 - Original release

=cut

BEGIN { # Set envariables for -T tainting.
  $ENV{'PATH'}      = '/bin:/usr/bin';
  delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
}
BEGIN { # Extract path and program name.
  use vars qw($path $prog);
  $0 =~ m%(.*)[/\\]([^/\\]*)%;
  ($path, $prog) = ($1 || '.', $2 || $0);
}

use Getopt::Long;
use Pod::Usage;
use SimpleDB::Client;
use List::Util qw(min);

my %METHODS = (
  'create-domain' => \&create_domain,
  'delete-domain' => \&delete_domain,
  'list-domains'  => \&list_domains,
  'put'           => \&put_attributes,
  'put-replace'   => \&put_replace_attributes,
  'get'           => \&get_attributes,
  'delete'        => \&delete_attributes,
  'select'        => \&select,
);

my $help        = 0;
my $aws_access_key_id     = $ENV{AWS_ACCESS_KEY_ID};
my $aws_secret_access_key = $ENV{AWS_SECRET_ACCESS_KEY};
my $sdb_service_url       = $ENV{SDB_SERVICE_URL} || 'https://sdb.amazonaws.com/';
my $replace               = 0;
my $max                   = undef;
my $separator             = '=';

Getopt::Long::config('no_ignore_case');
GetOptions(
           'help|?'                  => \$help,
           'aws-access-key-id=s'     => \$aws_access_key_id,
           'aws-secret-access-key=s' => \$aws_secret_access_key,
           'sdb-service-url=s'       => \$sdb_service_url,
           'replace'                 => \$replace,
           'max=s'                   => \$max,
           'separator=s'             => \$separator,
          )
  or pod2usage(2);

pod2usage(1) if $help;

if ( not $aws_access_key_id and not $aws_secret_access_key ) {
  # Try reading $HOME/.awssecret in case the keys are there.
  if ( open(AWSSECRET, "< $ENV{HOME}/.awssecret") ) {
    chomp($aws_access_key_id     = <AWSSECRET>);
    chomp($aws_secret_access_key = <AWSSECRET>);
    close(AWSSECRET);
  # Try reading /etc/passwd-s3fs in case the keys are there.
  } elsif ( open(S3FS, "< /etc/passwd-s3fs") ) {
    chomp(($aws_access_key_id, $aws_secret_access_key) = split(':', <S3FS>));
    close(S3FS);
  }
}

die "$prog: ERROR: Specify --aws-access-key-id and --aws-secret-access-key\n"
  unless $aws_access_key_id and $aws_secret_access_key;

my $connection_params = {};

my $sdb = SimpleDB::Client->new(
  access_key   => $aws_access_key_id,
  secret_key   => $aws_secret_access_key,
  simpledb_uri => URI->new($sdb_service_url),
);

my $command = shift(@ARGV) || pod2usage(1);
my $method = $METHODS{$command}
  || die "$prog: Unrecognized command: $command\n";

eval { &$method($sdb, @ARGV); };
say STDERR ("$prog: ERROR: Running '$command @ARGV':".$@) if $@;

exit 0;

sub create_domain {
  my ($sdb, $domain_name) = @_;

  $sdb->send_request('CreateDomain', {DomainName => $domain_name});
}

sub list_domains {
  my ($sdb) = @_;

  return if defined $max and $max <= 0;
  my $next_token;
  my $remaining = $max;
  do {
    my $rh_params = {};
    $rh_params->{NextToken} = $next_token if ($next_token);
    $rh_params->{MaxNumberOfDomains} = min($remaining, 250) if ($remaining);

    my $response = $sdb->send_request('ListDomains',  $rh_params);
    my $domain_name_list = $response->{ListDomainsResult}->{DomainName};
    print defined $_ ? "$_\n" : '' for @$domain_name_list;
    $next_token = $response->{ListDomainsResult}->{NextToken};
    $remaining -= scalar @$domain_name_list if defined $remaining;
  } while ( $next_token and (not defined $remaining or $remaining > 0));
}

sub select {
  my ($sdb, $select_expression) = @_;

  return if defined $max and $max <= 0;
  my $next_token;
  my $remaining = $max;
  do {
    my $response = $sdb->send_request('Select',{
      SelectExpression                 => $select_expression,
      ($next_token        ? (NextToken        => $next_token)          : ()),
      (defined $remaining ? (MaxNumberOfItems => min($remaining, 250)) : ()),
    });

    my $item_list = $response->{SelectResult}->{Item};
    for my $item ( @$item_list ) {
      print $item->{Name} ? ($item->{Name}, "\n") : '';
      my $attribute_list = $item->{Attribute};
      print $_->{Name} ? (' ', $_->{Name}, $separator, $_->{Value}, "\n") : ''
        for @$attribute_list;
    }

    $next_token = $response->{SelectResult}->{NextToken};
    $remaining -= scalar @$item_list if defined $remaining;
  } while ( $next_token and (not defined $remaining or $remaining > 0));
}

sub delete_domain {
  my ($sdb, $domain_name) = @_;

  my $response = $sdb->send_request('DeleteDomain',{
    DomainName => $domain_name,
  });
}


sub put_replace_attributes {
  $replace = 1;
  goto &put_attributes;
}


sub put_attributes {
  my ($sdb, $domain_name, $item_name, @pairs) = @_;

  my $response = $sdb->send_request('PutAttributes',{
    DomainName => $domain_name,
    ItemName   => $item_name,
    %{pairs_to_attributes(\@pairs, 1)},
  });
}

sub delete_attributes {
  my ($sdb, $domain_name, $item_name, @pairs) = @_;

  my $response = eval {
    $sdb->send_request('DeleteAttributes', {
      DomainName => $domain_name,
      ItemName   => $item_name,
      %{pairs_to_attributes(\@pairs, 0)},
    });
  };
}


sub get_attributes {
  my ($sdb, $domain_name, $item_name, @attribute_names) = @_;

  my %attr_param;
  for (my $i=1; $i <= scalar(@attribute_names); $i++) {
    $attr_param{"AttributeName.$i"} = $attribute_names[$i-1];
  }
  my $response = $sdb->send_request('GetAttributes', {
    DomainName => $domain_name,
    ItemName => $item_name,
    %attr_param
  });
  return unless $response->{GetAttributesResult} && ref($response->{GetAttributesResult}) ;

  my $attribute_list = ref($response->{GetAttributesResult}->{Attribute}) eq 'ARRAY' ?
    $response->{GetAttributesResult}->{Attribute} : [$response->{GetAttributesResult}->{Attribute}];

  print $_->{Name}? ($_->{Name}, $separator, $_->{Value}, "\n") : ''
    for @$attribute_list;
}


sub pairs_to_attributes {
  my ($pairs, $with_replace) = @_;

  my %attributes = ();
  for (my $i = 1; $i <= scalar(@$pairs); $i++) {
    my $pair = $pairs->[$i-1];
    my ($name, $value) = split(/$separator/, $pair, 2);
    my $prefix = "Attribute.$i.";
    $attributes{"${prefix}Name"}  = $name;
    $attributes{"${prefix}Value"} = $value;
    if ($with_replace && $replace) {
      $attributes{"${prefix}Replace"} = 'true';
    }
  }
  return \%attributes;
}