#!/usr/bin/perl # $Id: term-cat.pl 527 2016-04-17 13:46:22Z whynot $ # Copyright 2014, 2015 Eric Pozharski # GNU GPLv3 # AS-IS, NO-WARRANTY, HOPE-TO-BE-USEFUL use strict; use warnings; use version 0.77; our $VERSION = version->declare( v0.2.9 ); use IO::Pipe; use IO::Pty; use IO::Select; use Term::ReadKey; use POSIX qw| |; use Getopt::Long; =head1 NAME term-cat.pl - is not about terminating cats, got it? =cut =head1 DESCRIPTION It's about catenating terminals. "Catentating" in B sense. Well, have you ever tried using C noninteractively? You better don't because that weird thing insists to write on terminal without option. So does B. Now, with B you can cheat the fsck out of them. Read L and be happy. =cut =head1 DEPENDENCIES =over =item B Install from CPAN. =item B and B Subject to be distributed with B itself. B builds muscles on bare bones. =item B Install from CPAN. L|/Notes: Slave's Dimensions (Again)> briefly covers reasons. =item B Subject to be distributed with B itself. Because constants -- C and C. =item B and B Subject to be distributed with B itself. Cosmetic. =back =cut =head1 OPTIONS =over =item I<--controls> =item I<-c> A binary option to toggle Control Chars handling. Hairs about this handling are discussed in details in L|/Notes: Control Chars> and L|/Notes: Control Chars (Again)>. =item I<--dump> C =item I<-d> C Enables funcy by-directional dump of bytes going to and fro. Looks like this (that's a tail of slrn(1) exiting): 00027192 < 6F 1B 5B 4B # o.[K........ 00000015 > 79 # ...y........ 00027196 < 0D 57 72 69 74 69 6E 67 # .....Writing 00027204 < 20 2F 68 6F 6D 65 2F 77 68 79 6E 6F # /home/whyno 00027216 < 74 2F 2E 6E 65 77 73 72 63 20 2E 2E # t/.newsrc .. 00027228 < 2E 1B 5B 4B # ..[K........ 00027232 < 1B 5B 37 38 3B 33 34 48 # .....[78;34H 00027240 < 64 6F 6E 65 2E 0D # done........ 00027246 < 1B 5B 34 39 6D 1B # .......[49m. 00027252 < 5B 6D 1B 28 42 1B 5B 33 39 3B 34 39 # [m.(B.[39;49 00027264 < 6D 0D 1B 5B 4B 0D 1B 5B 72 1B 5B 3F # m..[K..[r.[? 00027276 < 31 30 34 39 6C # 1049l....... That's familiar hex-dump. Two things to mention. Index (1st column) refers to first *signifcant* byte in row and not leftmost position in 2nd column. Flows are intermixed: right angle (C<< > >>) marks flow *in* terminal; left angle (C<< < >>) marks flow *out* of terminal; separating hash (between second and third columns) is just for readability (at least for now). If L> is enabled, then flow through I is marked with question-mark (C). Hash (C<#>) marks diagnositc output of B itself (readability of this output is questinable but it could output binary too; so it's safer this way). =item I<--dump-width> C =item I<-D> C Adjusts number of bytes shown in L|/I<--dump> C> output. B<(bug)> Should accept C<0> as request of optimal fill. B<(bug)> Should accept negatives as request of total width. =item I<--help> =item I<-h> Obvious. =item I<--set-pixels> C C =item I<-p> C C Sets terminal size in pixels, if needed. Requires L|/I<--set-winsize> C C> (or else). Defaults (if needed) to C*6 and C*13. C and C must be positive integers (or else). =item I<--set-winsize> C C =item I<-w> C C Sets terminal size in character-places. Mutually exclusive with L|/I<--winsize-from> C>. Defaults (if needed) to sane 80x25. C and C must be positive integers (or else). =item I<--stderr> =item I<--stderr> C =item I<-s> =item I<-s> C If you ever need I prevented from merging in terminal's output here is this option. Without C it just dumps I of forked process on regular I. Also, if you have difficulties redirecting regular I with C specified this option will place output at your disposal there. C will be trancated with each run, beware. Also affects L|/I<--dump> C>. =item I<--version> =item I<-v> Obvious. =item I<--winsize-from> C =item I<-W> C Assigns a file handle C to acquire terminal dimensions from. B<(note)> It's *handle* and not *filename*, parameter must be numeric or else. If C isn't connected to terminal behaviour is up to L (it might work). Mutually exclusive with L|/I<--set-winsize> C C>. =back =cut my %opts = ( ws_set => [ ], ps_set => [ ], control_chars => -t \*STDIN ); Getopt::Long::Configure qw| no_ignore_case |; GetOptions \%opts, qw[ help|h! version|v! stderr|s:s dump|d=s dump_width|dump-width|D=i control_chars|controls|c ws_from|winsize-from|W=i ws_set|set-winsize|w=i{2} ps_set|set-pixels|p=i{2} ] or die qq|[GetOptions] failed, see above\n|; if( $opts{help} ) { print < off slave's output `- (optional) redirect to file --dump -d -- enables by-directional dumping, `- requires a filename --dump-width -D -- adjusts width of dumping, `- requires byte-factor --set-pixels -p -- sets slave's terminal size `- in pixels (W H) (requires -w) --set-winsize -w -- sets slave's terminal size `- in characters (W H) --winsize-from -W -- assigns filehandle to acquire terminal size from `- filehandle (required) must be numeric --controls -c -- force handling of "control chars" +- *on* if is connected to a terminal `- *off* otherwise --help -h -- this cheatsheet `- plz refer to 'perldoc term-cat.pl', really --verison -v -- versions handled "control chars": intr quit start stop eof defaults: ^C ^\\ ^Q ^S ^D END_OF_DUMP elsif( $opts{version} ) { print <&|, \*STDERR or die qq|[dup] (STDERR): $!\n|; $opts{stderr} = $fh } elsif( defined $opts{stderr} ) { open my $fh, qq|>|, $opts{stderr} or die qq|[open] ($opts{stderr}): $!\n|; $opts{stderr} = $fh } if( $opts{dump} ) { open my $fh, q|>|, $opts{dump} or die qq|[open] ($opts{dump}): $!\n|; $opts{dump} = $fh } if( !defined $opts{dump_width} ) { $opts{dump_width} = 8 } elsif( 0 >= $opts{dump_width} ) { die qq|{--dump_width}: should be positive\n| } if( @{$opts{ws_set}} && $opts{ws_from} ) { die qq|{--set-winsize} and {--winsize-from} are both set\n| } elsif( @{$opts{ps_set}} && !@{$opts{ws_set}} ) { die qq|{--set-pixel} is set but {--set-winsize} isn't\n| } elsif( @{$opts{ws_set}} && 0 >= $opts{ws_set}[0] ) { die qq|width of {--set-winsize} isn't positive\n| } elsif( @{$opts{ws_set}} && 0 >= $opts{ws_set}[1] ) { die qq|height of {--set-winsize} isn't positive\n| } elsif( @{$opts{ps_set}} && 0 >= $opts{ps_set}[0] ) { die qq|width of {--set-pixel} isn't positive\n| } elsif( @{$opts{ps_set}} && 0 >= $opts{ps_set}[1] ) { die qq|width of {--set-pixel} isn't positive\n| } elsif( defined $opts{ws_from} && 0 > $opts{ws_from} ) { die qq|{--winsize-from} isn't positive\n| } my $wsf = @{$opts{ws_set}} ? [ @{$opts{ws_set}}, @{$opts{ps_set}} ? @{$opts{ps_set}} : ( $opts{ws_set}[0]*6, $opts{ws_set}[1]*13 ) ] : $opts{ws_from} ? do { open my $fh, q|+<&=|, $opts{ws_from} or die qq|{--winsize-from} ($opts{ws_from}): $!\n|; $fh } : -t STDOUT ? \*STDOUT : -t STDERR ? \*STDERR : -t STDIN ? \*STDIN : [ 80, 25, 480, 325 ]; my( $oc, $ic, $ec, $dc ) = ( 0, 0, 0, 0 ); # XXX:201502081611:whynot: Fsck C. my @markings = ( qw| > < ? |, '#' ); # TODO:201501082024:whynot: Add I<-1> option to dump diagnostics. sub do_dump ( $$ ) { $opts{dump} or return; my $wth = $opts{dump_width}; my( $opt, $dump ) = @_; my @hex = map qq|\U$_ |, unpack q|H2| x length $dump, $dump; my @str = map +( $_ >= 0x20 && $_ < 0x7f ? chr : '.' ), map ord, split '', $dump; my $mark = $opt == 0 ? \$oc : $opt == 1 ? \$ic : $opt == 2 ? \$ec : \$dc; my @lft = ( q| | ) x ( $$mark% $wth ); my @rht = ( '.' ) x ( $$mark% $wth ); for(;@hex;) { my $bu = $$mark; while( my $it = shift @hex ) { push @lft, $it; push @rht, shift @str; $$mark++; @lft% $wth or last } push @lft, ( q| | ) x ($wth-@lft); push @rht, ( '.' ) x ($wth-@rht); $opts{dump}->printflush( sprintf qq|%08i %s %s# %s\n|, $bu, $markings[$opt], join( '', @lft ), join( '', @rht )); @lft = @rht = ( ) }} my $chk = IO::Pipe->new; my $pty = IO::Pty->new; my $fse = $opts{stderr} ? IO::Pipe->new : undef; my( %cc_map ); if( $opts{control_chars} ) { my $fh = -t STDOUT ? \*STDOUT : -t STDERR ? \*STDERR : -t STDIN ? \*STDIN : undef; my %raw = defined $fh ? GetControlChars( $fh ) : # TODO:201502092330:whynot: Provide way to override "control chars". # TODO:201502100242:whynot: Add feature of arbitrary B. # XXX:201502092208:whynot: Fsck colorization. ( INTERRUPT => "\cC", QUIT => "\c\\", #" START => "\cQ", STOP => "\cS", EOF => "\cD" ); $raw{$_} eq "\x00" and delete $raw{$_} foreach keys %raw; $cc_map{$raw{INTERRUPT}} = { sig => q|INT| } if $raw{INTERRUPT}; $cc_map{$raw{QUIT}} = { sig => q|QUIT| } if $raw{QUIT}; $cc_map{$raw{START}} = { sig => q|CONT| } if $raw{START}; $cc_map{$raw{STOP}} = { sig => q|STOP| } if $raw{STOP}; delete $raw{$_} foreach qw| INTERRUPT QUIT START STOP |; $cc_map{$raw{EOF}} = { code => sub { close $pty }, dscr => q|eof| } if $raw{EOF}; delete $raw{$_} foreach qw| EOF |; do_dump -1, join( ' ', q|Omitting handlers for: |, keys %raw ) if keys %raw } defined( my $pid = fork ) or die qq|[fork]: $!\n|; unless( $pid ) { $chk->writer; $pty->make_slave_controlling_terminal; my $slv = $pty->slave; close $pty; # CHECK:201409270331:whynot: https://rt.cpan.org/Public/Bug/Display.html?id=33841 SetTerminalSize +(q|ARRAY| eq ref $wsf ? @$wsf : GetTerminalSize( undef, $wsf )), $slv; ReadMode q|ultra-raw|, $slv; open STDIN, q|<&|, $slv->fileno or die qq|[dup] (STDIN): $!\n|; open STDOUT, q|>&|, $slv->fileno or die qq|[dup] (STDOUT): $!\n|; if( $fse ) { $fse->writer; open STDERR, q|>&|, $fse->fileno or die qq|[dup] (STDERR): $!\n| } else { open STDERR, q|>&|, $slv->fileno or die qq|[dup] (STDERR): $!\n| } close $slv; # XXX:201501082026:whynot: Yup. L. { exec @ARGV } $chk->print( $! + 0, "\n" ); die qq|[exec]: $!\n| } $chk->reader; $fse->reader if $fse; $pty->close_slave; ReadMode q|ultra-raw|, $pty; ReadMode q|ultra-raw|, \*STDIN if -t \*STDIN; END { ReadMode q|normal|, \*STDIN if -t \*STDIN } my $chld = $chk->sysread( my $cher, 1024 ); defined $chld or die qq|sync with slave: $!\n|; undef $chk; if( $chld ) { my $errno = int +( split m{\n}, $chld )[0]; die qq|[exec] slave: $errno\n| } # FIXME:201405202106:whynot: Go B, plz. END { kill 15, $pid if defined $pid } defined STDIN->blocking( 0 ) or die qq|[blocking] (STDIN): $!\n|; $SIG{WINCH} = sub { # CHECK:201409282317:whynot: Here too [33841@rt.cpan.org]. my $nsz = [ q|ARRAY| eq ref $wsf ? @$wsf : GetTerminalSize( undef, $wsf ) ]; do_dump -1, sprintf q|(SIGWHICH) has just come in, going for %3i %3i %2i %2i|, @$nsz; SetTerminalSize @$nsz, $pty->slave; kill q|WINCH|, $pid }; my $slct = IO::Select->new( \*STDIN, $pty, $fse ? $fse : () ); END { do_dump -1, sprintf q|exiting with (%04x)|, $? } my $eof = 10; # FIXME:201505182208:whynot: Probably, should recover from interrupted Bs on I wiht B successfully reading *nothing* (that is, it reads empty string). This situation is believed to be I being closed and B exits cleanly. =item * Also, if you need I of B opened for a reason (controlling forked process sounds good enough) and you can live with I<--contorls> (what might not be feasable) then you can message B about intended end of session with C (whatever it is for your terminal). Then B will exit cleanly too. =item * However, managing both ways (I and I) might be too cumbersome for one-liner or sh-function (besides, you may be interested in I of slave only). Then closing I of B looks like the only way. And it's problematic. Without output there's no way for B to know that its I is closed. However, when it tries to output it will get C, got to conclusion that I is out, and then exit cleanly. =item * But that means that B would hang around for some time (probably after your one-liner or sh-function finished) (and yes, I stays open in this case). What might, literally, never happen -- if forked process has nothing to say then B will have nothing to output too. For situations like this you can safely C it -- B will understand it as a message to give up and will exit cleanly. B<(note)> One point of significant importance here. You must B first, then B. Because B on pipe does an implicit B. What won't finish ever because B doesn't know it should B. =item * Anything else is an error with apropriate B and I<$?> =back =cut =head1 NOTES =cut =head2 Notes: Slave's Dimensions First, dimensions can be overridden with L|/I<--set-winsize> C C> (with accompaning L|/I<--set-pixels> C C>). How it would appear on real terminal (if any) is up to curses. Second, it can be sourced from different terminal; it's not clear why would you need it but here we are: % stty -a | perl -pwle '$.>1 and last' ; tty speed 38400 baud; rows 74; columns 92; line = 0; /dev/pts/15 % stty -a 1 and last' speed 38400 baud; rows 78; columns 99; line = 0; % ./term-cat.pl -W 3 -- stty -a 31 and last' speed 38400 baud; rows 78; columns 99; line = 0; Yes, it's not that easy. Third, if not overriden then dimensions are sourced from I, I, or I. One important *if* -- first connected to a terminal wins. It's not clear anymore why it's in this particular order. Fourth, defaults to 80x25 (and 480x325 in pixels). =cut =head2 Notes: Slave's Dimensions (Again) Now, if it wasn't made clear already -- B attempts to do weird stuff here (L of B|IO::Pty/clone_winsize_from(\*FH)> is not enough). Dimesions are made with L of B|Term::ReadKey/SetTerminalSize WIDTH,HEIGHT,XPIX,YPIX [, Filehandle]> what works just fine. They are read with L of B|Term::ReadKey/GetTerminalSize [Filehandle]> and here is a problem: L<[33841@rt.cpan.org]|https://rt.cpan.org/Public/Bug/Display.html?id=33841>. It's set being resolved; however, it's resolved in-git, so it's not clear what changes were made; however, what C said suggests that changes are documentation only. What means no modifications on B side needed. However, if my reading is wrong there will be a breakage of unknown magnitude. Let's hope for documentation. =cut =head2 Notes: Control Chars One thing must be stated upfront -- there are two types of The Control Chars. First type consists of Chars that clearly map to Bs (or The Signal-Type Chars, or StC hereafter). Second type doesn't -- it's about line editing (or The Line-Editing Chars, or LeC hereafter). StC are easy to implement (find a Char, then send a B, straightforward, eh?) Line-editing Chars aren't difficult to implement -- B just can't do it. I humbly remind that B isn't about emulating terminals, it "is about catenating terminals". In B sense. B does its IO in blocks. Indeed, if I comes from terminal then IO will appear as if it's done in characters (it is, unless you type encoded UTF-8) then forked process will receive your input in characters too. But it's still blocks under hood. It is, B doesn't do line-wise IO, it does block-wise IO. As soon as there's anything to push to forked process it will do it immediately. And now when, say, 'kill' comes in there won't be a word to remove -- it's already gone. However, 'eof' stands aside. It's natural for it (if you think about it) to be StC. And it's not. But you really will need it if you just grep output of forked process. Actually, 'lnext and 'dsusp' (or 'flush', if you prefer) smell StC too. But they are left out in cold. Probably B<(bug)>. =cut =head2 Notes: Control Chars (Again) Control Chars handling is *on* when I is connected to terminal (because, as mentioned earlier, you will totally need 'eof' LeC); if it's excessive then turn it off with I<--no-controls>. However, if I isn't connected to terminal then the handling is off. However, if you insist (with I<--controls>) then B will try to source them from I, I, or (surprise!) I (no, I<--winsize-from> isn't consulted) (yes, peek order being the same is coinsidence) (yes, there's logic in this). Because, supposedly, whatever would be sourced will be familiar for you (it's your terminal after all). However, if sourcing proves unsuccessful then some defaults will be provided. Those are completely arbitrary: intr ^C x03 ETX quit ^\ x1C FS start ^Q x11 DC1 stop ^S x13 DC3 eof ^D x04 EOT As you can see, there's neither I<--controls-from> nor a way to provide comfortable override; That's a B<(bug)>. =cut =head2 Notes: Syslog B doesn't log, Use L|/I<--dump> C> instead. But there will be noise: vmunix: [12321819.780851] perl: sending ioctl 5401 to a partition! Looks like it comes through C. Harmless. Probably natural. =cut =head1 AUTHOR Eric Pozharski, =head1 COPYRIGHT & LICENSE Copyright 2014, 2015 by Eric Pozharski This utility is free in sense: AS-IS, NO-WARRANTY, HOPE-TO-BE-USEFUL. This utility is released under GNU GPLv3.