From: Nicolas Boisselier Date: Sun, 21 Jun 2026 11:14:36 +0000 (+0200) Subject: bin/csv2human X-Git-Url: https://git.nbdom.net/?a=commitdiff_plain;h=30de7ca18903d2c741fdcf75a5ff662db2217e2f;p=nb.git bin/csv2human --- diff --git a/bin/csv2human b/bin/csv2human index 47d7fc1e..ba2185c3 100755 --- a/bin/csv2human +++ b/bin/csv2human @@ -1,20 +1,15 @@ #!/usr/bin/env perl use strict; use warnings; -################################################################################# -# -# VERSION -# -################################################################################# -my $VERSION = '0.0.1'; -# NB 16.01.18 -# - create script +use utf8; +use open qw(:std :utf8); # STDIN/STDOUT are UTF-8 ################################################################################# # # GLOBALS # ################################################################################# +my $VERSION = '0.0.1'; my ($NAME) = $0 =~ m,([^/]+)$,; ################################################################################# @@ -28,10 +23,10 @@ my $DEBUG = $main::DEBUG = 0; my %Opt = ( 'header' => 0, 'total' => 1, + 'encode' => 1, 'sep' => "\t", ); get_options(\%Opt); -#@help() unless @ARGV; $main::_DATA_ = undef; ################################################################################# @@ -39,115 +34,84 @@ $main::_DATA_ = undef; # BEGIN # ################################################################################# -#use Data::Dumper; print Dumper(\%Opt); -use Encode; - -# -# Calculate lengths -# -my @len = (); -my @lines = (); - -use Encode; -my $ENCODE = 1; -binmode STDOUT, ":utf8" if $ENCODE; -#binmode STDOUT, "raw:"; -#binmode STDOUT; -sub _realLength() { - my $str = shift; - $str = Encode::encode_utf8($str) if $ENCODE; - # NB 11.06.26 $str = Encode::encode('UTF-8', $str) if $ENCODE; - return length($str); +# ------------------- width measurement ------------------- +my $width_func; +eval { + require Text::CharWidth; + Text::CharWidth->import(qw(mbswidth)); + $width_func = sub { mbswidth(shift) }; + 1; +} or do { + # Fallback: count characters (works if emoji are 1 column on your terminal) + $width_func = sub { length(shift) }; + warn "Text::CharWidth not installed – falling back to character count.\n"; + warn "For correct alignment with all Unicode widths, install:\n"; + warn " cpan install Text::CharWidth\n"; +}; + +# ------------------- read input -------------------------- +my @rows; +while () { + chomp; + next unless length; + my @fields = split /$Opt{sep}/; # tab‑separated + push @rows, \@fields; } +exit 0 unless @rows; -sub realLength() { - $_[0] = Encode::decode_utf8($_[0]) if $ENCODE; - # NB 11.06.26 return length(Encode::encode('UTF-8',$_[0])); - return length($_[0]); +# Pad rows to equal number of columns +my $num_cols = 0; +for my $row (@rows) { + $num_cols = scalar @$row if scalar @$row > $num_cols; +} +for my $row (@rows) { + push @$row, '' while scalar @$row < $num_cols; } -while (<>) { - - my @F = $Opt{format} ? split(/\s*$Opt{sep}\s*/) : split(/$Opt{sep}/); - - for (my $i=0;$i<@F;$i++) { - chomp($F[$i]); - my $l; - $len[$i] = $l if ( $l = &realLength($F[$i]) ) >= ($len[$i] || 0); +# ------------------- column widths ----------------------- +my @max_widths; +for my $row (@rows) { + for my $col (0 .. $num_cols - 1) { + my $w = $width_func->($row->[$col]); + $max_widths[$col] = $w + if !defined $max_widths[$col] || $w > $max_widths[$col]; } - - push @lines, [@F]; } -exit unless @lines; - -# -# Build format string -# -my $tot = @lines - ($Opt{header} ? 1 : 0); - -my $t = -1; - -my $format = "│ ".join(" │ ",map {$t+=$_+3; "\%-".$_."s"} @len)." │".chr(10); -my $sep_line_first = "┌".join("┬",map {("─"x($_+2))} @len)."┐".chr(10); -my $sep_line = "├".join("┼",map {("─"x($_+2))} @len)."┤".chr(10); -my $sep_line_last = "└".join("┴",map {("─"x($_+2))} @len)."┘".chr(10); -if ($ENCODE) { - $format = Encode::decode("UTF-8",$format); - $sep_line_first = Encode::decode("UTF-8",$sep_line_first); - $sep_line = Encode::decode("UTF-8",$sep_line); - $sep_line_last = Encode::decode("UTF-8",$sep_line_last); +# ------------------- formatting helpers ------------------ +sub format_row { + my ($row) = @_; + my @padded; + for my $col (0 .. $num_cols - 1) { + my $cell = $row->[$col]; + my $width = $width_func->($cell); + my $pad = ' ' x ($max_widths[$col] - $width); + push @padded, $cell . $pad; # left‑align + } + return '│ ' . join(' │ ', @padded) . ' │'; } -# -# Format / align -# -# a => 1 -# b => 222222 -if ($Opt{format}) { - my $sep = " ".$Opt{sep}." "; - my $i; - $format = join($sep,map - { - $i++; - $t+=$_+&realLength($sep); - $i == @len ? '%s' : "\%-".$_."s"; - } @len - ).chr(10); - $sep_line_first = ""; - $sep_line = ""; - $sep_line_last = ""; - $VERBOSE = 0; - $Opt{total} = 0; - $Opt{header} = 0; +sub border_line { + my ($left, $middle, $right) = @_; + my @segments = map { '─' x ($_ + 2) } @max_widths; # +2 for the spaces + return $left . join($middle, @segments) . $right; } -# -# Print -# -print $sep_line_first; -my $i = 0; -my $count; -while (my $line = shift @lines) -{ - # Add missing empty columns - for (my $j=@$line; $j<@len; $j++ ) - { - $line->[$j] = ''; - } - - printf $format,@$line; - print $sep_line if $Opt{header} and !$i++; +# ------------------- output ------------------------------ +print border_line('┌', '┬', '┐'), "\n"; +my $row_start_index = 0; +if ($Opt{header}) { + print format_row($rows[0]), "\n"; # header + print border_line('├', '┼', '┤'), "\n"; + $row_start_index = 1; +} +for my $i ($row_start_index .. $#rows) { + print format_row($rows[$i]), "\n"; # data rows } +print border_line('└', '┴', '┘'), "\n"; -print $sep_line_last; +my $tot = @rows - ($Opt{header} ? 1 : 0); print "$tot Records\n" if $Opt{total}; - -################################################################################# -# -# END -# -################################################################################# exit 0; ################################################################################# @@ -253,6 +217,7 @@ Description! -option[header|h!] Print or not header separotor after first row -option[sep|s=s] Separtor + -option[encode|e!] Encode (default: $Opt{encode}) -option[total|t!] Print number of total record (default: $Opt{total}) -option[format|f!] Only format according to option --sep. Usefull for coding -option[verbose|v+] Verbose mode: increase the verbosity level. diff --git a/bin/csv2human.TODEL b/bin/csv2human.TODEL new file mode 100755 index 00000000..ee43e96f --- /dev/null +++ b/bin/csv2human.TODEL @@ -0,0 +1,310 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use Unicode::GCString; +use Text::CharWidth qw(mbswidth); +use String::Print qw(printp); +#use utf8; +################################################################################# +# +# VERSION +# +################################################################################# +my $VERSION = '0.0.1'; +# NB 16.01.18 +# - create script + +################################################################################# +# +# GLOBALS +# +################################################################################# +my ($NAME) = $0 =~ m,([^/]+)$,; + +################################################################################# +# +# ARGS +# +################################################################################# +my $VERBOSE = $main::VERBOSE = 1; +my $DEBUG = $main::DEBUG = 0; + +my %Opt = ( + 'header' => 0, + 'total' => 1, + 'encode' => 1, + 'sep' => "\t", +); +get_options(\%Opt); +$main::_DATA_ = undef; + +################################################################################# +# +# BEGIN +# +################################################################################# +#use Data::Dumper; print Dumper(\%Opt); +use Encode; + +# +# Calculate lengths +# +my @len = (); +my @lines = (); + +use Encode; +binmode STDOUT, ":utf8" if $Opt{encode}; +#binmode STDOUT, "raw:"; +#binmode STDOUT; +sub realLength { + my $str = shift; + # NB 21.06.26 $str = Encode::encode_utf8($str) if $Opt{encode}; + # NB 21.06.26 $str = Encode::encode('UTF-8', $str) if $Opt{encode}; + $str = Encode::decode('UTF-8', $str); + # NB 21.06.26 return mbswidth($str); + # NB 21.06.26 my $gcs = Unicode::GCString->new($str); + # NB 21.06.26 return $gcs->columns(); + return length($str); +} + +sub pad_to_width { + my ($string, $width) = @_; + my $gcs = Unicode::GCString->new($string); + my $visual_width = $gcs->columns(); # Gets the actual screen width[reference:9][reference:10] + #$visual_width = length(Encode::encode_utf8($string)); + $visual_width = mbswidth($string); + #return $string if $visual_width>$width; + #$visual_width = 0 if $visual_width>$width; + my $padding = " " x ($width - $visual_width); + return $string . $padding; # For left-aligned text +} + +sub pad_format { + my $fmt = shift; + for my $str (@_) { + $fmt =~ /%-(\d+)s/; + my $width = $1; + my $new = pad_to_width($str,$width); + $fmt =~ s/%-(\d+)s/$new/; + } + return $fmt; +} + +while (<>) { + + my @F = $Opt{format} ? split(/\s*$Opt{sep}\s*/) : split(/$Opt{sep}/); + + for (my $i=0;$i<@F;$i++) { + chomp($F[$i]); + my $l; + $len[$i] = $l if ( $l = realLength($F[$i]) ) >= ($len[$i] || 0); + } + + push @lines, [@F]; +} + +exit unless @lines; + +# +# Build format string +# +my $tot = @lines - ($Opt{header} ? 1 : 0); + +my $t = -1; + +my $format = "│ ".join(" │ ",map {$t += $_ + 3; "\%-".$_."s"} @len)." │".chr(10); +my $sep_line_first = "┌".join("┬",map {("─"x($_+2))} @len)."┐".chr(10); +my $sep_line = "├".join("┼",map {("─"x($_+2))} @len)."┤".chr(10); +my $sep_line_last = "└".join("┴",map {("─"x($_+2))} @len)."┘".chr(10); +if (1 and $Opt{encode}) { + $format = Encode::decode("UTF-8",$format); + $sep_line_first = Encode::decode("UTF-8",$sep_line_first); + $sep_line = Encode::decode("UTF-8",$sep_line); + $sep_line_last = Encode::decode("UTF-8",$sep_line_last); +} +# NB 21.06.26 utf8::decode($format); +# NB 21.06.26 utf8::decode($sep_line_first); +# NB 21.06.26 utf8::decode($sep_line); +# NB 21.06.26 utf8::decode($sep_line_last); +#die $format; + +# +# Format / align +# +# a => 1 +# b => 222222 +if ($Opt{format}) { + my $sep = " ".$Opt{sep}." "; + my $i; + $format = join($sep, + map { + $i++; + $t += $_ + realLength($sep); + $i == @len ? '%s' : "\%-".$_."s"; + } @len + ).chr(10); + $sep_line_first = ""; + $sep_line = ""; + $sep_line_last = ""; + $VERBOSE = 0; + $Opt{total} = 0; + $Opt{header} = 0; +} + +# +# Print +# +print $sep_line_first; +my $i = 0; +my $count; +while (my $line = shift @lines) +{ + # Add missing empty columns + for (my $j=@$line; $j<@len; $j++ ) + { + $line->[$j] = ''; + } + + @$line = map { utf8::decode($_); $_ } @$line; + printf($format,@$line); + # NB 21.06.26 printf pad_format($format,@$line); + print $sep_line if $Opt{header} and !$i++; +} + +print $sep_line_last; +print "$tot Records\n" if $Opt{total}; + +################################################################################# +# +# END +# +################################################################################# +exit 0; + +################################################################################# +# +# Functions +# +################################################################################# +sub help { +#------------------------------------------------------------------------------ +# Print help and exit +#------------------------------------------------------------------------------ + + require 'Pod/Usage.pm' unless $INC{'Pod/Usage.pm'}; + + # Substitutions + sub pod_env { + my $v = ''; + eval '$v = ref(\\'.$_[0].') eq "ARRAY" ? join(" ",'.$_[0].') : '.$_[0].'; return defined $v ? $v : qq|UNDEF|;'; + return $v; + } + + $main::_DATA_ =~ s/([@\$][A-Z_a-z\{\}]+)/pod_env($1)/eg; + + my $in; + open($in,'<',\$main::_DATA_); + + open(STDOUT,"|perl -pe 's/\.$$//g'".(($ENV{PAGER}||'') eq 'less' ? "|less -FRi" : "")); + my $opts = { + -input => $in, + -ouput => \*STDOUT, + -exitval => 'noexit', + -sections => [qw(SYNOPSIS DESCRIPTION OPTIONS)], + -verbose => ($Opt{'help'} ? 99 : 3), + }; + + Pod::Usage::pod2usage($opts); + close $in; + close STDOUT; + + exit 0; +} + +#------------------------------------------------------------------------------ +# Print version and exit +#------------------------------------------------------------------------------ +sub version { print "$NAME: version [$VERSION]\n"; exit 0; } + +#------------------------------------------------------------------------------ +# Get options from pod +#------------------------------------------------------------------------------ +sub get_options { + + use Getopt::Long qw(:config no_ignore_case no_auto_abbrev); + + my @Opt; + + sub pod_opt { + local $_; + my $o = shift; + $o =~ s/(=.|[\+\-\!]$)//; + $o = join(", ",map{"-$_"} split(/[\|,:;]/,$o)); + return "$o"; + } + + while () { + s/option\[([^\]]+)\]/push(@Opt,$1) and pod_opt($1)/eg; + $main::_DATA_ .= $_; + } + + GetOptions($_[0],@Opt) || exit -1; + + help() if $_[0]{'help'} or $_[0]{'man'}; + version() if $_[0]{'version'}; + + $main::VERBOSE = $VERBOSE = $_[0]{'verbose'} if defined $_[0]{'verbose'}; + $main::DEBUG = $DEBUG = $_[0]{'debug'} if defined $_[0]{'debug'}; + +} + +__DATA__ + +=head1 NAME + +$NAME - Alternative to linux command column, format csv tab style rows + +=head1 SYNOPSIS + +Quick usage! + +=over + +=item $NAME --sep ',' --noheader FILE + +=item $NAME --help + +=back + +=head1 DESCRIPTION + +Description! + +=head1 OPTIONS + + -option[header|h!] Print or not header separotor after first row + -option[sep|s=s] Separtor + -option[encode|e!] Encode (default: $Opt{encode}) + -option[total|t!] Print number of total record (default: $Opt{total}) + -option[format|f!] Only format according to option --sep. Usefull for coding + -option[verbose|v+] Verbose mode: increase the verbosity level. + -option[debug+] Debug mode: increase the verbosity level. + -option[version|V] Print version (default: $VERSION) + -option[help|?] Print a brief help message and exits. + -option[man] Print the manual page and exits. + +=cut + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2018 Nicolas Boisselier + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +See . + +=head1 AUTHOR + +Nicolas Boisselier + +=cut diff --git a/lib/perl/NB/CGI.pm b/lib/perl/NB/CGI.pm index 6956c58a..4a200c04 100644 --- a/lib/perl/NB/CGI.pm +++ b/lib/perl/NB/CGI.pm @@ -275,13 +275,13 @@ sub tag { sub _tag_func_ { #warn 'TEXT' if $TEXT_OUTPUT; - return &text__tag_func(@_) if $TEXT_OUTPUT; + return _tag_func_text(@_) if $TEXT_OUTPUT; shift if ref($_[0]) eq __PACKAGE__; # for case: $cgi->_tag_func return &CGI::_tag_func(@_); } sub _tag_func { - return &text__tag_func(@_) if $TEXT_OUTPUT; + return _tag_func_text(@_) if $TEXT_OUTPUT; my $tagname = shift; my ($q,$a,@rest) = self_or_default(@_); @@ -309,7 +309,7 @@ sub _tag_func { } } -sub text__tag_func { +sub _tag_func_text { my $self; ($self,@_) = &self_or_default(@_); my $tagname = shift; my ($q,$a,@rest) = self_or_default(@_); @@ -343,7 +343,7 @@ sub text__tag_func { @result = map {"$_\n";} @result; } # TR - elsif ($tagname =~ /^(start_)?tr$/i) + elsif ($tagname =~ /^(end_)?tr$/i) { # NB 24.10.24 @result = map {"$_|\n";} @result; @result = map {"$_\n";} @result;