package MySQL::Config;

# ----------------------------------------------------------------------
# $Id: Config.pm,v 1.2 2003/09/24 12:25:36 dlc Exp $ 
# ----------------------------------------------------------------------
# MySQL::Config - 
# Copyright (C) 2003 darren chamberlain <darren@cpan.org>
# ----------------------------------------------------------------------

use strict;
use base qw(Exporter);
use vars qw($VERSION $REVISION $GLOBAL_CNF @EXPORT @EXPORT_OK);

use Carp qw(carp);
use File::Spec;

$VERSION    = '1.03';
$REVISION   = sprintf "%d.%02d", q$Revision: 1.2 $ =~ /(\d+)\.(\d+)/;
$GLOBAL_CNF = "/etc/%s.cnf" unless defined $GLOBAL_CNF;
@EXPORT     = qw(load_defaults);
@EXPORT_OK  = qw(parse_defaults);

# ======================================================================
#                        --- Public Functions ---
# ======================================================================

sub load_defaults {
    my ($conf_file, $groups, $argc, $argv) = @_;
    my ($user_cnf, $ini, $field);
    $ini = [ ];

    # ------------------------------------------------------------------
    # Sanity checking:
    #   * $conf_file should be a string, defaults to "my"
    #   * $groups should be a ref to an array
    #   * $argc should be a ref to a scalar
    #   * $argv should be a ref to an array
    # ------------------------------------------------------------------
    $conf_file = "my" unless defined $conf_file && ! ref($conf_file);
    $groups = [ $groups ]
        unless ref $groups eq 'ARRAY';

    if (defined $argc) {
        $argc = \$argc
            unless ref $argc eq 'SCALAR';
    }
    else {
        $argc = \(my $i = 0);
    }

    $argv = \@ARGV unless defined $argv;
    $argv = [ $argv ]
        unless ref $argv eq 'ARRAY';

    $user_cnf = File::Spec->catfile($ENV{HOME}, ".$conf_file.cnf");

    # ------------------------------------------------------------------
    # Parse the global config and user's config
    # ------------------------------------------------------------------
    _parse($ini, sprintf $GLOBAL_CNF, $conf_file);
    _parse($ini, $user_cnf);

    # ------------------------------------------------------------------
    # Pull out the appropriate pieces, based on @$groups
    # ------------------------------------------------------------------
    @$groups = map { $_->[0] } @$ini unless @$groups;
    $groups = join '|', map { quotemeta($_) } @$groups;

    push @$argv, map { $$argc++; sprintf "--%s=%s", $_->[1], $_->[2] }
                 grep { $_->[0] =~ /$groups/ } @$ini;

    1;
}

sub parse_defaults {
    my ($conf_file, $groups) = @_;
    my ($count, $argv, %ini);
    $argv = [ ];

    load_defaults($conf_file, $groups, \$count, $argv);

    for (@$argv) {
        next unless /^--(.*?)=(.*)$/;
        my $name = $1;
        my $value = $2; 

        if ($value =~ /=/) {
            $ini{ $name } = { }
                unless exists($ini{ $name }) &&
                    ref($ini{ $name }) eq 'HASH';
            my ($subname, $subvalue) = split /\s*=\s*/, $value, 2;
            $ini{ $name }->{ $subname } = $subvalue;
        }
        else {
            $ini{ $name } = $value;
        }
    }

    return wantarray ? %ini : \%ini;
}

# ======================================================================
#                        --- Private Functions ---
# ======================================================================

# ----------------------------------------------------------------------
# _parse($file)
#
# Parses an ini-style file into an array of [ group, name, value ]
# array refs.
# ----------------------------------------------------------------------
sub _parse {
    my $ini = shift;
    my $file = shift;
    my $current;
    local ($_, *INI);

    return { } unless -f $file && -r _;

    $ini ||= [ ];
    unless (open INI, $file) {
        carp "Couldn't open $file: $!";
        return { };
    }
    while (<INI>) {
        s/[;#].*$//;
        s/^\s*//;
        s/\s*$//;

        next unless length;

        /^\s*\[(.*)\]\s*$/
            and $current = $1, next;

        $_ = join '=', $_, 1
            unless /=/;

        my ($n, $v) = split /\s*=\s*/, $_, 2;
        if ($v =~ /\s/) {
            $v =~ s/"/\\"/g;
            $v = qq("$v") 
        }

        push @$ini, [ $current, $n, $v ];
    }

    unless (close INI) {
        carp "Couldn't close $file: $!";
    }

    return $ini;
}

1;

__END__

=head1 NAME

MySQL::Config - Parse and utilize MySQL's /etc/my.cnf and ~/.my.cnf files

=head1 SYNOPSIS

    use MySQL::Config;

    my @groups = qw(client myclient);
    my $argc = 0;
    my @argv = ();

    load_defaults "my", \@groups, \$argc, \@argv;

=head1 DESCRIPTION

C<MySQL::Config> emulates the C<load_defaults> function from
F<libmysqlclient>.  Just like C<load_defaults>, it will fill an aray
with long options, ready to be parsed by C<getopt_long>, a.k.a.
C<Getopt::Long>.

=head1 THE my.cnf FILE

MySQL's F<my.cnf> file is a mechanism for storing and reusing command
line arguments.  These command line arguments are grouped into
I<groups> using a simple INI-style format:

    ; file: ~/.my.cnf

    [client]
    user = darren
    host = db1
    pager = less -SignMEX

    [mytop]
    color = 1
    header = 0

Each element in C<[>, C<]> pairs is a I<group>, and each call to
C<load_defaults> will specify 0 or more groups from which to grab
options.  For example, grabbing the I<client> group from the above
config file would return the I<user>, I<host>, and I<pager> items.
These items will be formatted as command line options, e.g.,
I<--user=darren>.

=head1 USING MySQL::Config

=head2 load_defaults("name", \@groups, \$count, \@ary)

C<load_defaults> takes 4 arguments: a string denoting the name of the
config file (which should generally be I<my>); a reference to an array
of groups from which options should be returned; a reference to a
scalar that will hold the total number of parsed elements; and a
reference to an array that will hold the final versions of the
extracted name, value pairs.  This final array will be in a format
suitable for processing with C<Getopt::Long>:

    --user=username
    --password=password

and so on.

If the final array reference is missing, C<@ARGV> will be used.  Options
will be pushed onto the end of the array, leaving what is already in
place undisturbed.

The scalar (the third argument to C<load_defaults>) will contain the
number of elements parsed from the config files.

=head2 parse_defaults("name", \@groups)

C<load_defaults> has an un-Perlish interface, mostly because it is
exactly the same signature as the version from the C API.  There is
also a function, not exported by default, called C<parse_defaults>,
which returns a hash of parsed (name, value) pairs (or a hashref
in scalar context):

    use MySQL::Config qw(parse_defaults);

    my %cfg = parse_defaults "my", \@groups;

C<%cfg> looks like:

    %cfg = (
        "user" => "username",
        "password" => "password",
    )

and so on.  This might be a more natural interface for some programs;
however, C<load_defaults> is more true to the original.

Because C<parse_defaults> flattens the arguments into a hash, it makes
special allowances for variables that contain multiple C<=>; these are
turned into nested hashes.  For example, the MySQL's I<set-variable>
option can contain name value pairs, like so:

    set-variable    = key_buffer=16M
    set-variable    = max_allowed_packet=1M
    set-variable    = table_cache=64
    set-variable    = sort_buffer=512K
    set-variable    = net_buffer_length=8K
    set-variable    = myisam_sort_buffer_size=8M

These will be turned into a nested hash like this:

    'set-variable' => {
                        'myisam_sort_buffer_size' => '8M',
                        'sort_buffer' => '512K',
                        'max_allowed_packet' => '16M',
                        'key_buffer' => '16M',
                        'table_cache' => 64,
                        'net_buffer_length' => '8K'
                      },

This is not done for C<load_defaults>, as C<Getopt::Long> will
correctly handle variables with embedded C<=> if the option is
passed a hash reference.

=head1 USING SOMETHING OTHER THAN "my" AS THE FIRST STRING

This string controls the name of the configuration file; the names
work out to, basically F<~/.${cfg_name}.cnf> and
F</etc/${cnf_name}.cnf>.

If you are using this module for mysql clients, then this should
probably remain I<my>.  Otherwise, you are free to mangle this however
you choose:

    $ini = parse_defaults 'superapp', [ 'foo' ];

=head1 SUPPORT

C<MySQL::Config> is supported by the author.

=head1 VERSION

This is C<MySQL::Config>, revision $Revision: 1.2 $.

=head1 AUTHOR

darren chamberlain E<lt>darren@cpan.orgE<gt>

=head1 COPYRIGHT

(C) 2003 darren chamberlain

This library is free software; you may distribute it and/or modify it
under the same terms as Perl itself.

=head1 SEE ALSO

L<Perl>
