#!/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,([^/]+)$,;
#################################################################################
my %Opt = (
'header' => 0,
'total' => 1,
+ 'encode' => 1,
'sep' => "\t",
);
get_options(\%Opt);
-#@help() unless @ARGV;
$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 (<STDIN>) {
+ 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;
#################################################################################
-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.
--- /dev/null
+#!/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 (<DATA>) {
+ 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 <http://www.gnu.org/licenses/>.
+
+=head1 AUTHOR
+
+Nicolas Boisselier <nico@nbdom.net>
+
+=cut