App-CSVUtils

 view release on metacpan or  search on metacpan

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

    defined($str) && length($str) or die [400, "Please specify code ($label)"];
    $str = "package main; no strict; no warnings; sub { $str }";
    log_trace "[csvutil] Compiling Perl code: $str";
    my $code = eval $str; ## no critic: BuiltinFunctions::ProhibitStringyEval
    die [400, "Can't compile code ($label) '$str': $@"] if $@;
    $code;
}

sub eval_code {
    no warnings 'once';
    my ($code, $r, $value_for_topic, $return_topic) = @_;
    local $_ = $value_for_topic;
    local $main::r = $r;
    local $main::row = $r->{input_row};
    local $main::rownum = $r->{input_rownum};
    local $main::data_rownum = $r->{input_data_rownum};
    local $main::csv = $r->{input_parser};
    local $main::fields_idx = $r->{input_fields_idx};
    if ($return_topic) {
        $code->($_);
        $_;
    } else {
        $code->($_);
    }
}

sub _get_field_idx {
    my ($field, $field_idxs) = @_;
    defined($field) && length($field) or die "Please specify at least a field\n";
    my $idx = $field_idxs->{$field};
    die "Unknown field '$field' (known fields include: ".
        join(", ", map { "'$_'" } sort {$field_idxs->{$a} <=> $field_idxs->{$b}}
             keys %$field_idxs).")\n" unless defined $idx;
    $idx;
}

sub _get_csv_row {
    my ($csv, $row, $i, $outputs_header) = @_;
    #use DD; print "  "; dd $row;
    return "" if $i == 1 && !$outputs_header;
    my $status = $csv->combine(@$row)
        or die "Error in line $i: ".$csv->error_input."\n";
    $csv->string . "\n";
}

sub _instantiate_parser_default {
    require Text::CSV_XS;

    Text::CSV_XS->new({binary=>1});
}

sub _instantiate_parser {
    require Text::CSV_XS;

    my ($args, $prefix) = @_;
    $prefix //= '';

    my %tcsv_opts = (binary=>1);
    if (defined $args->{"${prefix}sep_char"} ||
            defined $args->{"${prefix}quote_char"} ||
            defined $args->{"${prefix}escape_char"}) {
        $tcsv_opts{"sep_char"}    = $args->{"${prefix}sep_char"}    if defined $args->{"${prefix}sep_char"};
        $tcsv_opts{"quote_char"}  = $args->{"${prefix}quote_char"}  if defined $args->{"${prefix}quote_char"};
        $tcsv_opts{"escape_char"} = $args->{"${prefix}escape_char"} if defined $args->{"${prefix}escape_char"};
    } elsif ($args->{"${prefix}tsv"}) {
        $tcsv_opts{"sep_char"}    = "\t";
        $tcsv_opts{"quote_char"}  = undef;
        $tcsv_opts{"escape_char"} = undef;
    }
    $tcsv_opts{always_quote} = 1 if $args->{"${prefix}always_quote"};
    $tcsv_opts{quote_empty} = 1 if $args->{"${prefix}quote_empty"};

    Text::CSV_XS->new(\%tcsv_opts);
}

sub _instantiate_emitter {
    my $args = shift;
    _instantiate_parser($args, 'output_');
}

sub _complete_field_or_field_list {
    # return list of known fields of a CSV

    my $which = shift;

    my %args = @_;
    my $word = $args{word} // '';
    my $cmdline = $args{cmdline};
    my $r = $args{r};

    # we are not called from cmdline, bail
    return undef unless $cmdline; ## no critic: Subroutines::ProhibitExplicitReturnUndef

    # let's parse argv first
    my $args;
    {
        # this is not activated yet
        $r->{read_config} = 1;

        my $res = $cmdline->parse_argv($r);
        #return undef unless $res->[0] == 200;

        $cmdline->_read_config($r) unless $r->{config};
        $args = $res->[2];
    }

    # user hasn't specified -f, bail
    return {message=>"Please specify input filename first"} unless defined $args && $args->{input_filename};

    # user wants to read CSV from stdin, bail
    return {message=>"Can't get field list when input is stdin"} if $args->{input_filename} eq '-';

    # can the file be opened?
    my $csv_parser = _instantiate_parser(\%args, 'input_');
    open my($fh), "<encoding(utf8)", $args->{input_filename} or do {
        #warn "csvutils: Cannot open file '$args->{input_filename}': $!\n";
        return [];
    };

    # can the header row be read?
    my $row = $csv_parser->getline($fh) or return [];

    if (defined $args->{input_header} && !$args->{input_header}) {
        $row = [map {"field$_"} 1 .. @$row];
    }

    if ($which =~ /sort/) {
        $row = [map {($_,"-$_","+$_","~$_")} @$row];

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

    input_skip_num_lines => {
        summary => 'Number of lines to skip before header row',
        schema => 'posint*',
        description => <<'MARKDOWN',

This can be useful if you have a CSV files (usually some generated reports,
sometimes converted from spreadsheet) that have additional header lines or info
before the CSV header row.

See also the alternative option: `--input-skip-until-pattern`.

MARKDOWN
        tags => ['category:input'],
    },
    input_skip_until_pattern => {
        summary => 'Skip rows until the first header row matches a regex pattern',
        schema => 're_from_str*',
        description => <<'MARKDOWN',

This is an alternative to the `--input-skip-num-lines` and can be useful if you
have a CSV files (usually some generated reports, sometimes converted from
spreadsheet) that have additional header lines or info before the CSV header
row.

With `--input-skip-num-lines`, you skip a fixed number of lines. With this
option, rows will be skipped until the first field matches the specified regex
pattern.

MARKDOWN
        tags => ['category:input'],
    },
    input_skip_before_num_data_rows => {
        summary => 'Skip a certain number of data rows for each input file',
        schema => 'uint*',
        description => <<'MARKDOWN',

This option can be used to skip the first certain number of data rows. If set to
1, for example, will only process 1 data row for each input file. This option is
a convenient alternative to composing with <pm:csv-tail>.

MARKDOWN
        tags => ['category:input'],
    },
    input_skip_file_after_num_data_rows => {
        summary => 'Limit processing each input file to this number of data rows',
        schema => 'uint*',
        description => <<'MARKDOWN',

This option can be used to limit processing only to a certain number of data
rows. If set to 1, for example, will only process 1 data row for each input
file. This option is a convenient alternative to composing with <pm:csv-head>.

MARKDOWN
        tags => ['category:input'],
    },
    input_tsv => {
        summary => "Inform that input file is in TSV (tab-separated) format instead of CSV",
        schema => 'true*',
        description => <<'MARKDOWN',

Overriden by `--input-sep-char`, `--input-quote-char`, `--input-escape-char`
options. If one of those options is specified, then `--input-tsv` will be
ignored.

MARKDOWN
        tags => ['category:input'],
    },
    input_sep_char => {
        summary => 'Specify field separator character in input CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

Defaults to `,` (comma). Overrides `--input-tsv` option.

MARKDOWN
        tags => ['category:input'],
    },
    input_quote_char => {
        summary => 'Specify field quote character in input CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

Defaults to `"` (double quote). Overrides `--input-tsv` option.

MARKDOWN
        tags => ['category:input'],
    },
    input_escape_char => {
        summary => 'Specify character to escape value in field in input CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

Defaults to `\\` (backslash). Overrides `--input-tsv` option.

MARKDOWN
        tags => ['category:input'],
    },
);

our %argspecs_csv_output = (
    output_header => {
        summary => 'Whether output CSV should have a header row',
        schema => 'bool*',
        description => <<'MARKDOWN',

By default, a header row will be output *if* input CSV has header row. Under
`--output-header`, a header row will be output even if input CSV does not have
header row (value will be something like "col0,col1,..."). Under
`--no-output-header`, header row will *not* be printed even if input CSV has
header row. So this option can be used to unconditionally add or remove header
row.

MARKDOWN
        tags => ['category:output'],
    },
    output_tsv => {
        summary => "Inform that output file is TSV (tab-separated) format instead of CSV",
        schema => 'bool*',
        description => <<'MARKDOWN',

This is like `--input-tsv` option but for output instead of input.

Overriden by `--output-sep-char`, `--output-quote-char`, `--output-escape-char`
options. If one of those options is specified, then `--output-tsv` will be
ignored.

MARKDOWN
        tags => ['category:output'],
    },
    output_sep_char => {
        summary => 'Specify field separator character in output CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

This is like `--input-sep-char` option but for output instead of input.

Defaults to `,` (comma). Overrides `--output-tsv` option.

MARKDOWN
        tags => ['category:output'],
    },
    output_quote_char => {
        summary => 'Specify field quote character in output CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

This is like `--input-quote-char` option but for output instead of input.

Defaults to `"` (double quote). Overrides `--output-tsv` option.

MARKDOWN
        tags => ['category:output'],
    },
    output_escape_char => {
        summary => 'Specify character to escape value in field in output CSV, will be passed to Text::CSV_XS',
        schema => ['str*', len=>1],
        description => <<'MARKDOWN',

This is like `--input-escape-char` option but for output instead of input.

Defaults to `\\` (backslash). Overrides `--output-tsv` option.

MARKDOWN
        tags => ['category:output'],
    },
    output_always_quote => {
        summary => 'Whether to always quote values',
        schema => 'bool*',
        default => 0,
        description => <<'MARKDOWN',

When set to false (the default), values are quoted only when necessary:

    field1,field2,"field three contains comma (,)",field4

When set to true, then all values will be quoted:

    "field1","field2","field three contains comma (,)","field4"

MARKDOWN
        tags => ['category:output'],
    },
    output_quote_empty => {
        summary => 'Whether to quote empty values',
        schema => 'bool*',
        default => 0,
        description => <<'MARKDOWN',

When set to false (the default), empty values are not quoted:

    field1,field2,,field4

When set to true, then empty values will be quoted:

    field1,field2,"",field4

MARKDOWN
        tags => ['category:output'],
    },
);

our %argspecopt_input_filename = (
    input_filename => {
        summary => 'Input CSV file',
        description => <<'MARKDOWN',

Use `-` to read from stdin.

Encoding of input file is assumed to be UTF-8.

MARKDOWN
        schema => 'filename*',
        default => '-',
        'x.completion' => $xcomp_csvfiles,
        tags => ['category:input'],
    },
);

our %argspecopt_input_filenames = (



( run in 0.579 second using v1.01-cache-2.11-cpan-54e63673c56 )