App-PerlGlue

 view release on metacpan or  search on metacpan

lib/App/PerlGlue.pm  view on Meta::CPAN

package App::PerlGlue;

use strict;
use warnings;
use feature qw(say);
use JSON::PP qw(encode_json decode_json);
use Text::ParseWords qw(parse_line);

our $VERSION = '0.04';

sub run {
    my ($class, @argv) = @_;
    my $cmd = shift @argv // 'help';

    return _help() if $cmd eq 'help' || $cmd eq '--help' || $cmd eq '-h';

    if ($cmd eq 'version' || $cmd eq '--version' || $cmd eq '-v') {
        say "perlglue $VERSION";
        return 0;
    }

    return _cmd_command_help($cmd)            if @argv && ($argv[0] eq '--help' || $argv[0] eq '-h');

    return _cmd_upper()                      if $cmd eq 'upper';
    return _cmd_lower()                      if $cmd eq 'lower';
    return _cmd_lines(@argv)                 if $cmd eq 'lines';
    return _cmd_lines(@argv)                 if $cmd eq 'where';
    return _cmd_replace(@argv)               if $cmd eq 'replace';
    return _cmd_pick(@argv)                  if $cmd eq 'pick';
    return _cmd_convert(@argv)               if $cmd eq 'convert' || $cmd eq 'csv' || $cmd eq 'from-csv';
    return _cmd_jsonl(@argv)                 if $cmd eq 'jsonl';
    return _cmd_template(@argv)              if $cmd eq 'template';
    return _cmd_rename(@argv)                if $cmd eq 'rename';

    warn "Unknown command: $cmd\n\n";
    _help();
    return 2;
}

sub _cmd_upper { while (my $line = <STDIN>) { print uc $line } return 0 }
sub _cmd_lower { while (my $line = <STDIN>) { print lc $line } return 0 }

sub _open_in {
    my ($file) = @_;
    return *STDIN unless defined $file && length $file;
    open my $fh, '<', $file or die "Cannot open $file: $!";
    return $fh;
}

sub _cmd_lines {
    my (@argv) = @_;
    my ($file, $expr);
    while (@argv) {
        my $arg = shift @argv;
        if ($arg eq '--where') { $expr = shift @argv; }
        elsif (!defined $file) { $file = $arg; }
        elsif (!defined $expr) { $expr = $arg; }
    }
    my $fh = _open_in($file);
    while (my $line = <$fh>) {
        local $_ = $line;
        if (defined $expr) {
            my $ok = eval $expr;
            next unless $ok;
        }
        print $line;
    }
    return 0;
}

sub _cmd_replace {
    my (@argv) = @_;
    my $expr = shift @argv // die "replace requires perl substitution expression\n";
    my $fh = _open_in(shift @argv);
    while (my $line = <$fh>) {
        local $_ = $line;
        eval $expr;
        print $_;
    }
    return 0;
}

sub _parse_csv_rows {
    my ($fh) = @_;
    my @rows;
    while (my $line = <$fh>) {
        chomp $line;
        next if $line eq '';
        push @rows, [ parse_line(',', 1, $line) ];
    }
    return @rows;
}

sub _cmd_pick {
    my (@argv) = @_;
    my $file = shift @argv // die "pick requires file\n";
    my $flag = shift @argv // '';
    die "pick expects --csv name,email\n" unless $flag eq '--csv';
    my @wanted = split /,/, (shift(@argv) // '');

    my $fh = _open_in($file);
    my @rows = _parse_csv_rows($fh);
    my $header = shift @rows // [];
    my %idx; @idx{@$header} = (0 .. $#$header);

    say join ',', @wanted;
    for my $r (@rows) {
        my @out = map { defined $idx{$_} ? $r->[ $idx{$_} ] : '' } @wanted;
        say join ',', @out;
    }
    return 0;
}

sub _cmd_convert {
    my (@argv) = @_;
    my $file = shift @argv // die "convert requires file\n";
    my $to;
    while (@argv) {
        my $a = shift @argv;
        $to = shift @argv if $a eq '--to';
    }
    die "convert only supports --to jsonl\n" unless defined $to && $to eq 'jsonl';

    my $fh = _open_in($file);
    my @rows = _parse_csv_rows($fh);
    my $header = shift @rows // [];
    for my $r (@rows) {
        my %obj;
        @obj{@$header} = @$r;
        say encode_json(\%obj);
    }
    return 0;
}

sub _cmd_jsonl {
    my (@argv) = @_;
    my ($file, $expr, $where);
    while (@argv) {
        my $a = shift @argv;
        if ($a eq '--where') { $where = shift @argv; }
        elsif (!defined $file && $a !~ /^\$_\-/) { $file = $a; }
        elsif (!defined $expr) { $expr = $a; }
    }
    $expr = $where if defined $where;
    my $fh = _open_in($file);
    while (my $line = <$fh>) {
        chomp $line;
        next if $line eq '';
        local $_ = decode_json($line);
        if (defined $expr) {
            my $ok = eval $expr;
            next unless $ok;
        }
        say encode_json($_);
    }
    return 0;
}

sub _cmd_template {
    my (@argv) = @_;
    my $file = shift @argv // die "template requires file\n";
    my $tpl = shift @argv // die "template requires template string\n";

    my $fh = _open_in($file);
    my @rows = _parse_csv_rows($fh);
    my $header = shift @rows // [];
    for my $r (@rows) {
        my %obj;
        @obj{@$header} = @$r;
        (my $out = $tpl) =~ s/\{\{\s*(\w+)\s*\}\}/defined $obj{$1} ? $obj{$1} : ''/ge;
        say $out;
    }
    return 0;
}

sub _cmd_rename {
    my (@argv) = @_;
    my $expr = shift @argv // die "rename requires substitution expression\n";
    for my $old (@argv) {
        (my $new = $old);
        local $_ = $new;
        eval $expr;
        $new = $_;
        next if $new eq $old;
        die "Target exists: $new\n" if -e $new;
        rename $old, $new or die "rename $old -> $new failed: $!";
        say "$old -> $new";
    }
    return 0;
}

sub _cmd_command_help {
    my ($cmd) = @_;
    my %usage = (
        upper    => 'perlglue upper < input.txt',
        lower    => 'perlglue lower < input.txt',
        lines    => 'perlglue lines [file] [--where EXPR]',
        where    => 'perlglue where [file] [--where EXPR]',
        replace  => q{perlglue replace 's/foo/bar/g' [file]},
        pick     => 'perlglue pick users.csv --csv name,email',
        convert  => 'perlglue convert users.csv --to jsonl',
        csv      => 'perlglue csv users.csv --to jsonl',
        'from-csv' => 'perlglue from-csv users.csv --to jsonl',
        jsonl    => q{perlglue jsonl logs.jsonl '\$_->{status} >= 500'},
        template => q{perlglue template users.csv 'Hello, {{name}}'},
        rename   => q{perlglue rename 's/\s+/_/g' files...},
        version  => 'perlglue version',
        help     => 'perlglue help',
    );

    if (exists $usage{$cmd}) {
        say $usage{$cmd};
        return 0;
    }

    warn "Unknown command: $cmd\n";
    return 2;
}

sub _help {
    print <<'HELP';
perlglue - glue messy text into useful shapes

Usage:
  perlglue help
  perlglue --help
  perlglue version
  perlglue <command> --help
  perlglue upper < input.txt
  perlglue lower < input.txt
  perlglue lines [file] [--where EXPR]
  perlglue replace 's/foo/bar/g' [file]
  perlglue pick users.csv --csv name,email
  perlglue convert users.csv --to jsonl
  perlglue jsonl logs.jsonl '$_->{status} >= 500'
  perlglue template users.csv 'Hello, {{name}}'
  perlglue rename 's/\s+/_/g' files...
HELP
    return 0;
}

1;

__END__

=head1 NAME

App::PerlGlue - glue messy text into useful shapes

=head1 SYNOPSIS

  # text
  echo 'Perl is glue' | perlglue upper
  echo 'LOUD' | perlglue lower
  perlglue lines app.log --where '$_ =~ /ERROR/'
  perlglue replace 's/(?<=user=)\w+/REDACTED/g' app.log

  # csv/jsonl
  perlglue pick users.csv --csv name,email
  perlglue convert users.csv --to jsonl
  perlglue jsonl logs.jsonl '$_->{status} >= 500'
  perlglue template users.csv 'Hello, {{name}}'

  # filesystem
  perlglue rename 's/\s+/_/g' *.txt

=head1 DESCRIPTION

C<App::PerlGlue> provides the C<perlglue> command-line program for practical
text munging when simple one-liners become hard to maintain.

It focuses on connecting line-based text, CSV-ish input, and JSON Lines with a



( run in 0.592 second using v1.01-cache-2.11-cpan-800906f7e73 )