File Coverage

blib/lib/Zoidberg/Fish/Log.pm
Criterion Covered Total %
statement 9 174 5.1
branch 0 132 0.0
condition 0 47 0.0
subroutine 3 10 30.0
pod 5 7 71.4
total 17 370 4.5


line stmt bran cond sub pod time code
1             package Zoidberg::Fish::Log;
2              
3             our $VERSION = '0.981';
4              
5 1     1   1197 use strict;
  1         2  
  1         44  
6             #use AutoLoader 'AUTOLOAD';
7 1     1   6 use Zoidberg::Utils qw/:default path getopt output_is_captured/;
  1         1  
  1         9  
8 1     1   261 use base 'Zoidberg::Fish';
  1         2  
  1         2972  
9              
10             # TODO purge history with some intervals
11              
12             sub init {
13 0     0 1   my $self = shift;
14 0           @$self{qw/pid init_time/} = ($$, time);
15 0 0         close $$self{logfh} if $$self{logfh};
16 0           my $file = path( $$self{config}{logfile} );
17 0           my $fh; # undefined scalar => new anonymous filehandle on open()
18 0 0         if (open $fh, ">>$file") {
19 0           my $oldfh = select $fh;
20 0           $| = 1;
21 0           select $oldfh;
22 0           $$self{logfh} = $fh;
23 0           $self->add_events('prompt');
24             # TODO also set event for change of hist file => re-init filehandle
25             }
26             else {
27 0           delete $$self{logfh};
28 0           complain "Log file not writeable, logging disabled";
29             }
30             }
31             # TODO in %Env::PS1::map
32             # \! The history number of the next command.
33             # \# The command number of the next command
34             # (like history number, but minus the lines read from the history file)
35              
36             # TODO "history_reset" event for when we are forced to read a new hist file
37              
38             # TODO: HISTTIMEFORMAT should give an insight in the timestamps
39             sub history {
40 0     0 1   my $self = shift;
41 0           my ($opts, $args) = getopt('nonu,-n reverse,-r read type$ +* -* @', @_);
42 0   0       my $tag = $$opts{type} || 'cmd';
43 0 0         unshift @$args, grep /^[+-]\d+$/, @{$$opts{_opts}} if exists $$opts{_opts};
  0            
44 0 0         error 'to many arguments' if @$args > 2;
45              
46             # find the rigth history
47 0           my $re;
48 0 0 0       if ($$opts{read} or ! $$self{read_log}) { $re = $self->read_log_file($tag) }
  0 0          
    0          
49 0           elsif (exists $$self{logs}{$tag}) { $re = $$self{logs}{$tag} }
50 0           elsif ($tag eq 'cmd') { $re = $$self{shell}->builtin('GetHistory') }
51 0           debug 'found '.scalar(@$re).' records for '.$tag;
52              
53             # set history numbers
54 0 0         unless (output_is_captured) {
55 0 0         my $i = ($tag eq 'cmd') ? ($$self{command_number} - @$re + 1) : 1;
56             # TODO make this depend on init time indexing ... $$self{shell}{command_number}
57             # avoid modifying the original reference
58             # ouput format found in posix spec for fc
59 0           $re = [ map {$a = $_; $a =~ s/^/\t/mg; $a} @$re ];
  0            
  0            
  0            
60 0 0         @$re = map {$i++.$_} @$re unless $$opts{nonu};
  0            
61             }
62 0           else { $re = [ @$re ] } # force copy
63              
64             # get range if any
65 0 0 0       if (@$args) {
    0          
66 0           for (@$args) { # string match
67 0 0         next if /^[+-]?\d+$/;
68 0 0         my $regex = ref($_) ? $_ : qr/^\d*\t?\Q$_\E/;
69 0           my ($i, $done) = (0, 0);
70 0           for (reverse @$re) {
71 0 0         $i--; next unless $_ =~ $regex;
  0            
72 0 0         ++$done and last;
73             }
74 0 0         error "no record matching '$_'" unless $done;
75 0 0 0       if (@$args == 0 or $$args[0] == $$args[1]) { # default last for string
76 0           @$args = ($i, $i);
77 0           last;
78             }
79 0           else { $_ = $i }
80             }
81 0 0         $$args[1] = scalar @$re unless defined $$args[1]; # default default last
82 0           my $total = scalar @$re;
83 0           for (@$args) { # convert negative 2 positive
84 0 0 0       error 'index out of range: '.$_
      0        
85             if $_ == 0 or $_ < -$total or $_ > $total;
86 0 0         $_ += $total+1 if $_ < 0;
87             }
88 0 0         if ($$args[0] > $$args[1]) { # check order of args
89 0 0         $$opts{reverse} = $$opts{reverse} ? 0 : 1 ;
90 0           @$args = reverse @$args;
91             }
92 0           debug "history range: $$args[0] .. $$args[1]";
93 0           @$re = @$re[$$args[0]-1 .. $$args[1]-1];
94             }
95             elsif ($tag eq 'cmd' and defined $$self{config}{maxlines}) {
96             # FIXME temp hack till ReadLine gets maxlines
97 0           my @range = ($#$re - $$self{config}{maxlines}, $#$re);
98 0           @$re = @$re[$range[0] .. $range[1]];
99             }
100              
101 0 0         output $$opts{reverse} ? [reverse @$re] : $re;
102             }
103              
104             sub read_log_file {
105 0     0 0   my ($self, $tag) = @_;
106 0 0         my %tags = $tag ? ( $tag => [] ) : ();
107 0 0         if ($$self{config}{keep}) {
108 0           $tags{$_} = [] for keys %{$$self{config}{keep}};
  0            
109             }
110 0 0         return unless %tags;
111 0           my $file = path( $$self{config}{logfile} );
112 0 0 0       unless ($file) {
    0          
    0          
113 0           complain 'No log file defined, can\'t read history';
114 0           return;
115             }
116             elsif (-e $file and ! -r _) {
117 0           complain 'Log file not readable, can\'t read history';
118 0           return;
119             }
120             elsif (-s _) {
121             # TODO ignore lines from other shell instances ... use pid + init timestamp
122 0           debug "Going to read $file";
123 0   0       open IN, $file || error 'Could not open log file !?';
124 0           while () {
125             # pid time type string
126 0 0         m/-\s*\[\s*(\d+),\s*(\d+)\s*,\s*(\w+)\s*,\s*"(.*?)"\s*\]\s*$/ or next;
127 0 0 0       push @{$tags{$3}}, $4
  0   0        
128             if exists $tags{$3} and ($2 < $$self{init_time} or $1 == $$self{pid});
129             # if record newer then init_time and not matching our pid it's not ours
130             }
131 0           close IN;
132             }
133              
134 0           my $re;
135 0           $$self{logs} = {}; # reset
136 0           debug 'found the following tags in log: '.join(' ', keys %tags);
137 0           for (keys %tags) {
138 0 0         my @t = map {s/(\\\\)|(\\n)|\\(.)/$1?'\\':$2?"\n":$3/eg; $_}
  0 0          
  0            
  0            
139 0           @{ delete $tags{$_} };
140 0 0         if ($$self{config}{keep}{$_}) {
141 0 0         @t = reverse( ( reverse @t )[0 .. $$self{config}{keep}{$_}] )
142             if @t > $$self{config}{keep}{$_};
143 0           $$self{logs}{$_} = \@t;
144             }
145 0 0         $re = \@t if $_ eq $tag;
146 0 0         $$self{command_number} = scalar @t if $_ eq 'cmd';
147             }
148              
149 0           $$self{read_log}++;
150 0 0         return wantarray ? @$re : $re;
151             }
152              
153             # sub cmd {
154             sub prompt {
155             # my ($self, undef, $cmd) = @_;
156 0     0 0   my $self = shift;
157 0           my $cmd = $$self{shell}{previous_cmd};
158 0 0 0       return unless $$self{settings}{interactive} and $$self{logfh};
159 0           $cmd =~ s/(["\\])/\\$1/g;
160 0           $cmd =~ s/\n/\\n/g;
161 0 0 0       print {$$self{logfh}} "- [ $$self{pid}, ".time().", cmd, \"$cmd\" ]\n"
  0            
162             unless $$self{config}{no_duplicates} and $cmd eq $$self{prev_cmd};
163 0           $$self{prev_cmd} = $cmd;
164 0           $$self{command_number}++;
165             }
166              
167             sub log {
168 0     0 1   my ($self, $string, $type) = @_;
169 0   0       $type ||= 'log';
170 0 0         return prompt($self, undef, $string) if $type eq 'cmd';
171 0 0         if (exists $$self{config}{keep}{$type}) {
172 0   0       $$self{logs}{$type} ||= [];
173 0 0 0       unless ($$self{config}{no_duplicates} and $string eq $$self{logs}{$type}[-1]) {
174 0           push @{$$self{logs}{$type}}, $string;
  0            
175 0           shift @{$$self{logs}{$type}}
  0            
176 0 0         if @{$$self{logs}{$type}} > $$self{config}{keep}{$type};
177             }
178             }
179 0 0         return unless $$self{logfh};
180 0           $string =~ s/(["\\])/\\$1/g;
181 0           $string =~ s/\n/\\n/g;
182 0           print {$$self{logfh}} "- [ $$self{pid}, ".time().', '.$type.", \"$string\" ]\n";
  0            
183             }
184              
185             sub round_up {
186 0     0 1   my $self = shift;
187              
188 0 0         return unless $$self{logfh};
189 0           close $$self{logfh};
190              
191 0 0         my $max = defined( $$self{config}{maxlines} )
192             ? $$self{config}{maxlines} : $ENV{HISTSIZE} ;
193 0 0         return unless defined $max;
194 0           my $file = path( $$self{config}{logfile} );
195              
196 0 0         open IN, $file or error "Could not open hist file";
197 0           my @lines = (reverse ())[0 .. $max-1];
198 0 0         close IN or error "Could not read hist file";
199              
200 0 0         open OUT, ">$file" or error "Could not open hist file";
201 0           print OUT reverse @lines;
202 0           close OUT;
203             }
204              
205             #1;
206              
207             #__END__
208              
209             =head1 NAME
210              
211             Zoidberg::Fish::Log - History and log plugin for Zoidberg
212              
213             =head1 SYNOPSIS
214              
215             This module is a Zoidberg plugin, see Zoidberg::Fish for details.
216              
217             =head1 DESCRIPTION
218              
219             This plugin listens to the 'prompt' event and records all
220             input in the history log.
221              
222             If multiple instances of zoid are using the same history file
223             their histories will be merged.
224              
225             TODO option for more bash like behaviour
226              
227             In order to use the editor feature of the L command the module
228             L should be installed.
229              
230             =head1 EXPORT
231              
232             None by default.
233              
234             =head1 CONFIG
235              
236             =over 4
237              
238             =item loghist
239              
240             Unless this config is set no commands are recorded.
241              
242             =item logfile
243              
244             File to store the history. Defaults to "~/.%s.log.yaml" where '%s' is
245             replaced with the program name. Hence the default for B is
246             F<~/.zoid.log.yaml>.
247              
248             =item maxlines
249              
250             Maximum number of lines in the history. If not set the environment variable
251             'HISTSIZE' is used. In fact the number of lines can be a bit more then this
252             value on run time because the file is not purged after every write.
253              
254             =item no_duplicates
255              
256             If set a command will not be saved if it is the same as the previous command.
257              
258             =item keep
259              
260             Hash with log types mapped to a number representing the maximal number of lines
261             to keep in memory for this type. In contrast to the commandline history,
262             history arrays for these types are completely managed by this module.
263              
264             =back
265              
266             =head1 COMMANDS
267              
268             =over 4
269              
270             =item fc [-r][-e editor] [I [I]]
271              
272             =item fc -l [-nr] [I [I]]
273              
274             =item fc -s [I=I] [I [I]]
275              
276             "Fix command", this builtin allows you to edit and re-execute commands
277             from the history. I and I are either command numbers or strings
278             matching the beginning of a command; a negative number is used to designate
279             commands by counting back from the current one. Use the '-l' option to list
280             the commands in the history, and the '-n' switch to surpress the command
281             numbers in the listing.The '-r' switch reverses the order of the commands.
282             The '-s' switch re-executes the commands without editing.
283              
284             I and I default to '-16' and '-1' when the '-l' option is given.
285             Otherwise I defaults to '-1' and I defaults to I.
286              
287             Note that the selection of the editor is not POSIX compliant
288             but follows bash, if no editor is given using the '-e' option
289             the environment variables 'FCEDIT' and 'EDITOR' are both checked,
290             if neither is set, B is used.
291             ( According to POSIX we should use 'ed' by default and probably
292             ignore the 'EDITOR' varaiable, but I don't think that is "What You Want" )
293              
294             Following B setting the editor to '-' is identical with using
295             the I<-s> switch.
296              
297             Also note that B removes itself from the history and adds the resulting
298             command instead.
299              
300             Typically B is aliased to 'fc -s' so B will re-execute the last
301             command, optionally followed by a substitution and/or a string to match
302             the begin of the command.
303              
304             TODO: regex/glob substitution for '-s' switch; now only does string substitution.
305              
306             =cut
307              
308             sub fc {
309 0     0 1   my $self = shift;
310 0           my ($opt, $args) = getopt 'reverse,-r editor,-e$ list,-l nonu,-n -s -* +* @', @_;
311 0 0         unshift @$args, grep /^[+-]\d+$/, @{$$opt{_opts}} if exists $$opt{_opts};
  0            
312 0 0         my @replace = split('=', shift(@$args), 2) if $$args[0] =~ /=/;
313 0 0         error 'to many arguments' if @$args > 2;
314 0           my ($first, $last) = @$args;
315              
316             # get selection
317 0 0         if (!$first) { ($first,$last) = $$opt{list} ? (-16, -1) : (-1, -1) }
  0 0          
    0          
318 0 0         elsif (!$last) { $last = $$opt{list} ? '-1' : $first }
319              
320             # list history ?
321 0           my @hist_opts = map "--$_", grep $$opt{$_}, qw/nonu reverse/;
322 0 0         return $$self{shell}->builtin('history', @hist_opts, $first, $last) if $$opt{list};
323              
324             # get/edit commands
325 0           my $cmd = join "\n",
326 0           @{ $$self{shell}->builtin('history', @hist_opts, $first, $last) };
327 0 0         $cmd =~ s{\Q$replace[0]\E}{$replace[1]}g if @replace;
328 0   0       my $editor = $$opt{editor} || $ENV{FCEDIT} || $ENV{EDITOR} || 'vi';
329 0 0 0       unless ($$opt{'-s'} or $editor eq '-') {
330             # edit history - editor behaviour consistent with T:RL:Z
331 0           debug "going to edit: << '...'\n$cmd\n...\nwith: $editor";
332 0 0         eval 'require File::Temp' || error 'need File::Temp from CPAN';
333 0           my ($fh, $file) = File::Temp::tempfile(
334             'Zoid_fc_XXXXX', DIR => File::Spec->tmpdir );
335 0           print {$fh} $cmd;
  0            
336 0           close $fh;
337 0           $$self{shell}->shell($editor.' '.$file);
338 0 0         error if $@;
339 0 0         open TMP, $file or error "Could not read $file";
340 0           my $cmd = join '', ;
341 0           close TMP;
342 0           unlink $file;
343             }
344 0           else { debug "going to execute without editing: << '...'\n$cmd\n..." }
345              
346             # execute commands
347 0 0         $$self{shell}->shell($cmd) if length $cmd;
348 0           $$self{shell}{previous_cmd} = $cmd; # reset string to be logged
349              
350             # TODO inherit environment and redirection from self
351             }
352              
353             =item history [--type I] [--read] [-n|--nonu] [-r|--reverse] [I [I]]
354              
355             Returns (a part of) the history. By default it tries to find the commandline
356             history (depending on GetHistory), but the '--read' option forces reading the
357             history file. To get other log types, like 'pwd', use the '--type' option.
358             The '--nonu' option surpressees line numbering for the terminal output.
359              
360             The arguments I and I can either be a positive or negative integer,
361             representing the command number or reverse offset, or a string matching the begin
362             of the command. If only one integer is given I defaults to '-1'; if only one
363             string is given I defaults to I. As a bonus you can supply a regex
364             reference instead of a string when using the perl interface.
365              
366             Note that unlike B the B command is not specified by posix and
367             the implementation varies widely for different shells. In zoid, B is build on
368             top of B, so options for B are chosen consistently with B.
369              
370             =item log I I
371              
372             Adds I to the history file with the current timestamp
373             and the supplied I tag. The type defaults to "log".
374             If the type is set to "hist" the entry will become part of the
375             command history after the history file is read again.
376              
377             =back
378              
379             =head1 AUTHOR
380              
381             Jaap Karssenberg (Pardus) Epardus@cpan.orgE
382              
383             Copyright (c) 2011 Jaap G Karssenberg and Joel Berger. All rights reserved.
384             This program is free software; you can redistribute it and/or
385             modify it under the same terms as Perl itself.
386              
387             =head1 SEE ALSO
388              
389             L
390              
391             =cut
392              
393             1;
394