#! /usr/bin/perl
#
# Copyright (C) 2002-2021 by Peder Stray
#
# This is a standalone perl program and not intended to run within
# irssi, it will complain if you try to...
use strict;
use Getopt::Long;
use Encode;
use Pod::Usage;
use vars qw(%ansi %base %attr %old);
use vars qw(@bols @nums @mirc @irssi @mc @mh @ic @ih @cn);
use vars qw($class $oldclass);
use vars qw{$VERSION %IRSSI};
($VERSION) = '$Revision: 1.11.1 $' =~ / (\d+(\.\d+)+) /;
%IRSSI = (
name => 'log2ansi',
authors => 'Peder Stray',
contact => '[email protected]',
url => 'https://github.com/pstray/irssi-log2ansi',
license => 'GPL',
description => 'Convert various color codes to ANSI colors, useful for log filtering and viewing.',
);
my $opt_clear = 0;
my $opt_html = 0;
my $opt_utf8 = 0;
my $opt_help = 0;
if (__PACKAGE__ =~ /^Irssi/) {
# we are within irssi... die!
Irssi::print("%RWarning:%n log2ansi should not run from within irssi");
}
else {
do_convert();
}
sub defc {
my($attr) = shift || \%attr;
$attr->{fgc} = $attr->{bgc} = -1;
$attr->{fgh} = $attr->{bgh} = 0;
}
sub defm {
my($attr) = shift || \%attr;
$attr->{bold} = $attr->{underline} =
$attr->{blink} = $attr->{reverse} = 0;
}
sub def {
my($attr) = shift || \%attr;
defc($attr);
defm($attr);
}
sub setold {
%old = %attr;
}
sub emit {
my($str) = @_;
my(%elem,@elem);
if ($opt_clear) {
# do nothing
}
else {
if ($opt_html) {
my %class;
for (@bols) {
$class{$_}++ if $attr{$_};
}
for (qw(fg bg)) {
my $h = delete $class{"${_}h"};
my $n = $attr{"${_}c"};
next unless $n >= 0;
$class{"$_$cn[$n + 8 * $h]"}++;
}
$class = join " ", sort keys %class;
print qq{} if $oldclass;
print qq{} if $class;
$oldclass = $class;
}
else {
my(@clear) = ( (grep { $old{$_} > $attr{$_} } @bols),
(grep { $old{$_}>=0 && $attr{$_}<0 } @nums)
);
$elem{0}++ if @clear;
for (@bols) {
$elem{$base{$_}}++
if $attr{$_} && ($old{$_} != $attr{$_} || $elem{0});
}
for (@nums) {
$elem{$base{$_}+$attr{$_}}++
if $attr{$_} >= 0 && ($old{$_} != $attr{$_} || $elem{0});
}
@elem = sort {$a<=>$b} keys %elem;
if (@elem) {
@elem = () if @elem == 1 && !$elem[0];
printf "\e[%sm", join ";", @elem;
}
}
}
if ($opt_html) {
for ($str) {
s/&/&/g;
s/</g;
s/>/>/g;
}
}
print $str;
setold;
}
sub do_convert {
GetOptions(
'c|clear!' => \$opt_clear,
'h|html!' => \$opt_html,
'u|utf8!' => \$opt_utf8,
'help' => sub { $opt_help = 1 },
'full-help' => sub { $opt_help = 2 },
) or pod2usage(2);
# show some help if stdin is a tty and no files
$opt_help = 1 if !$opt_help && -t 0 && !@ARGV;
pod2usage(-verbose => $opt_help,
-exitval => 0,
) if $opt_help;
for (@ARGV) {
if (/\.xz$/) {
$_ = "unxz < '$_' |";
}
elsif (/\.bz2$/) {
$_ = "bunzip2 < '$_' |";
}
elsif (/\.gz$/) {
$_ = "gunzip < '$_' |";
}
elsif (/\.lzma$/) {
$_ = "unlzma < '$_' |";
}
}
my($n) = 0;
%ansi = map { $_ => $n++ } split //, 'krgybmcw';
@bols = qw(bold underline blink reverse fgh bgh);
@nums = qw(fgc bgc);
@base{@bols} = qw(1 4 5 7 1 5);
@base{@nums} = qw(30 40);
@mirc = split //, 'WkbgRrmyYGcCBMKw';
@irssi = split //, 'kbgcrmywKBGCRMYW';
@mc = map {$ansi{lc $_}} @mirc;
@mh = map {$_ eq uc $_} @mirc;
@ic = map {$ansi{lc $_}} @irssi;
@ih = map {$_ eq uc $_} @irssi;
@cn = qw(black dr dg dy db dm dc lgray dgray lr lg ly lb lm lc white);
if ($opt_html) {
print qq{\n};
}
if ($opt_utf8) {
binmode STDIN, ':bytes'; #encoding(cp1252)';
binmode STDOUT, ':encoding((UTF-8)';
}
while (<>) {
if ($opt_utf8) {
my $line;
while (length) {
$line .= decode("utf8", $_, Encode::FB_QUIET);
$line .= substr $_, 0, 1, "";
}
$_ = $line;
}
chomp;
def;
setold;
if ($opt_html) {
printf qq{
};
}
while (length) {
if (s/^\cB//) {
# toggle bold
$attr{bold} = !$attr{bold};
} elsif (s/^\cC//) {
# mirc colors
if (/^[^\d,]/) {
defc;
} else {
if (s/^(\d\d?)//) {
$attr{fgc} = $mc[$1 % 16];
$attr{fgh} = $mh[$1 % 16];
}
if (s/^,//) {
if (s/^(\d\d?)//) {
$attr{bgc} = $mc[$1 % 16];
$attr{bgh} = $mh[$1 % 16];
} else {
$attr{bgc} = -1;
$attr{bgh} = 0;
}
}
}
} elsif (s/^\cD//) {
# irssi format
if (s/^a//) {
$attr{blink} = !$attr{blink};
} elsif (s/^b//) {
$attr{underline} = !$attr{underline};
} elsif (s/^c//) {
$attr{bold} = !$attr{bold};
} elsif (s/^d//) {
$attr{reverse} = !$attr{reverse};
} elsif (s/^e//) {
# indent
} elsif (s/^f([^,]*),//) {
# indent_func
} elsif (s/^g//) {
def;
} elsif (s/^h//) {
# cleol
} elsif (s/^i//) {
# monospace
} else {
s/^(.)(.)//;
my($f,$b) = map { ord($_)-ord('0') } $1, $2;
if ($f<0) {
# $attr{fgc} = -1; $attr{fgh} = 0;
} else {
# c>7 => bold, c -= 8 if c>8
$attr{fgc} = $ic[$f];
$attr{fgh} = $ih[$f];
}
if ($b<0) {
# $attr{bgc} = -1; $attr{bgh} = 0;
} else {
# c>7 => blink, c -= 8
$attr{bgc} = $ic[$b];
$attr{bgh} = $ih[$b];
}
}
} elsif (s/^\cF//) {
# blink
$attr{blink} = !$attr{blink};
} elsif (s/^\cO//) {
def;
} elsif (s/^\cV//) {
$attr{reverse} = !$attr{reverse};
} elsif (s/^\c[\[([^m]*)m//) {
my(@ansi) = split ";", $1;
my(%a);
push @ansi, 0 unless @ansi;
for my $code (@ansi) {
if ($code == 0) {
def(\%a);
} elsif ($code == $base{bold}) {
$a{bold} = 1;
} elsif ($code == $base{underline}) {
$a{underline} = 1;
} elsif ($code == $base{blink}) {
$a{underline} = 1;
} elsif ($code == $base{reverse}) {
$a{reverse} = 1;
} elsif ($code >= 30 && $code <= 37) {
$a{fgc} = $code - 30;
} elsif ($code >= 40 && $code <= 47) {
$a{bgc} = $code - 40;
} else {
$a{$code} = 1;
}
}
if ($a{fgc} >= 0 && $a{bold}) {
$a{fgh} = 1;
$a{bold} = 0;
}
if ($a{bgc} >= 0 && $a{blink}) {
$a{bgh} = 1;
$a{blink} = 0;
}
for my $key (keys %a) {
$attr{$key} = $a{$key};
}
} elsif (s/^\c_//) {
$attr{underline} = !$attr{underline};
} else {
s/^(.[^\cB\cC\cD\cF\cO\cV\c[\c_]*)//;
emit $1;
}
}
def;
emit "";
if ($opt_html) {
print "
";
}
print "\n";
}
if ($opt_html) {
print "
\n";
}
}
__END__
=head1 NAME
log2ansi - Convert foo various color escape codes to ANSI (or strip them)
=head1 SYNOPSIS
B
[B<-c>|B<--clear>]
[B<-h>|B<--html>]
[B<-u>|B<--utf8>]
[B<--help>]
[I]
=head1 OPTIONS
=over
=item B<-c>, B<--clear>
Instructs B to clear all formatting and output plain text logs.
=item B<-h>, B<--html>
Instructs B to output a HTML fragment instead of ANSI text.
The whole log will be wrapped in a div with class C, each line
of the log in a div with class C. Colors are wrapped in spans,
with a class name consisting of C or C, concatenated with the
color name, either C or C, or C, C, C, C,
C, C, or C prefixed with either C for light, or C for
dark.
You have to include appropriate CSS yourself to get any colors at all
when viewing the log.
=item B<-u>, B<--utf8>
This forces output to be UTF-8, and does input decoding of UTF-8 with
fallback to ISO-8859-1. Use this if your input logs have mixed UTF-8
and ISO-8859-1.
=item B<--help>, B<--full-help>
Show help, either just option descriptions or a full man page.
=back
=head1 DESCRIPTION
Use B to convert logfiles from Irssi with internal escape
codes, mIRC color codes or ANSI escapes to plain text with ANSI
formatted color codes for viewing in a terminal.
Use the B<--clear> option to strip all formatting escapes and output
just plain text.
You can supply input on standard input, or as filenames on the command
line. Any file ending in B<.gz>, B<.bz2>, B<.xz> or B<.lzma> will be
uncompressed automatically before processing.
=head1 AUTHORS
Peder Stray
=cut