]> git.treefish.org Git - fex.git/blob - bin/zz
Original release 20160919
[fex.git] / bin / zz
1 #!/usr/bin/perl -w
2 #
3 # vv : visual versioning
4 # zz : generic shell clip board
5 # ezz : clip board editor
6 #
7 # http://fex.rus.uni-stuttgart.de/fstools/vv.html
8 # http://fex.rus.uni-stuttgart.de/fstools/zz.html
9 #
10 # by Ulli Horlacher <framstag@rus.uni-stuttgart.de>
11 #
12 # Perl Artistic Licence
13 #
14 # vv is a script to handle file versions:
15 # list, view, recover, diff, purge, migrate, save, delete
16 #
17 # vv is an extension to emacs idea of backup~ files
18 #
19 # File versions are stored in local subdirectory .versions/
20 #
21 # To use vv with jed, install to your jed library path:
22 #
23 #   http://fex.rus.uni-stuttgart.de/sw/share/jedlib/vv.sl
24 #
25 # To use vv with vim, add to your .vimrc:
26 #
27 #   autocmd BufWritePre  * execute '! vv -s ' . shellescape(@%)
28 #   autocmd BufWritePost * execute '! vv -b ' . shellescape(@%)
29 #
30 # To use vv with emacs, add to your .emacs:
31 #
32 #   (add-hook 'before-save-hook (lambda () (shell-command (
33 #    concat "vv -s " (shell-quote-argument (buffer-file-name))))))
34 #   (add-hook 'after-save-hook  (lambda () (shell-command (
35 #    concat "vv -b " (shell-quote-argument (buffer-file-name))))))
36 #   (setq make-backup-files nil)
37 #
38 # To use vv with ANY editor, first set:
39 #
40 #   export EDITOR=your_favourite_editor
41 #   alias ve='vv -e'
42 #
43 # and then edit your file with:
44 #
45 #   ve file
46 #
47 # $HOME/.vvrc is the config file for vv
48
49 # 2013-04-15 initial version
50 # 2013-04-16 added options -m and -v
51 # 2013-04-18 added option -s
52 # 2013-04-22 realfilename() fixes symlink problematics
53 # 2013-04-22 use rsync instead of cp
54 # 2013-04-23 added option -I
55 # 2013-04-23 renamed from jedv to vv
56 # 2013-04-24 added options -e -0
57 # 2013-05-09 added option -R
58 # 2013-05-22 modified option -d to double argument
59 # 2013-05-22 added vvrc with $exclude and @diff
60 # 2013-07-05 fixed bug potential endless loop in rotate()
61 # 2014-04-16 added change-file-test for opt_s (needs .versions/$file)
62 # 2014-04-18 added option -b : save backup
63 # 2014-05-02 fixed bug wrong file ownership when using as root
64 # 2014-06-18 options -r -d -v : parameter is optional, default is 1
65 # 2014-06-18 fixed (stupid!) bug option -s does only sometimes saving
66 # 2014-06-20 options -d -v : argument is optional, default is last file
67 # 2014-07-22 fixed bug no (new) backup version 0 on option -r
68 # 2014-11-14 added option -D : delete last saved version
69 # 2014-11-14 make .versions/ mode 777 if parent directory is world writable
70 # 2015-03-19 allow write access by root even if file and .versions/ have different owners
71 # 2015-03-20 better error formating for jed
72 # 2015-06-02 added option -r . to restore last saved backup
73 # 2016-03-07 added options -M -L
74 # 2016-03-08 renamed option -I to -H
75 # 2016-05-02 added -A option to preserve ACLs with rsync
76 # 2016-06-07 option -v : use PAGER=cat if STDOUT is not a tty
77 # 2016-06-08 added zz, ezz and installer vvzz
78 # 2016-07-06 avoid empty $ZZ versioning
79 # 2016-09-12 added option -q quiet mode
80
81 use Getopt::Std;
82 use File::Basename;
83 use Digest::MD5 'md5_hex';
84 use Cwd 'abs_path';
85
86 $prg = abs_path($0);
87 $0 =~ s:.*/::;
88
89 $ZZ = $ENV{ZZ} || "$ENV{HOME}/.zz";
90
91 &install if $0 eq 'vvzz';
92 &zz      if $0 eq 'zz';
93 &ezz     if $0 eq 'ezz';
94
95 # vv
96 $usage = <<EOD;
97 usage: $0 [-l] [file]
98        $0 -r . file
99        $0 -r version-number file [new-file]
100        $0 -d version-number[:version-number] file
101        $0 -v version-number file
102        $0 -s file
103        $0 -D file
104        $0 -e file
105        $0 -M file|.
106        $0 -L file|.
107        $0 -m [-R]
108        $0 -p
109        $0 -q
110        $0 -H
111 options: -l   list available versions
112          -v   view version
113          -r   recover file (. is last saved backup)
114          -d   show diff
115          -s   save file to new version
116          -D   delete last saved version
117          -e   edit file with \$EDITOR (with versioning)
118          -p   purge orphaned versions (without current file)
119          -q   quiet mode
120          -m   migrate backup files to version files (-R all recursive)
121          -M   migrate to more versions (upto 100)
122          -L   migrate to less versions (upto 10)
123          -H   show more information
124 examples: $0 project.pl
125           $0 -d 2 project.pl
126           $0 -r 2 project.pl project_2.pl
127 EOD
128
129 $vvrc = $ENV{HOME} . '/.vvrc';
130
131 $opt_l = 1;
132 $opt_h = $opt_p = $opt_m = $opt_s = $opt_0 = $opt_e = $opt_H = $opt_b = 0;
133 $opt_q = $opt_D = $opt_R = 0;
134 $opt_r = $opt_d = $opt_v = $opt_M = $opt_L = '';
135 ${'opt_+'} = 0;
136 getopts('hHls0bepqmRDrdv+M:L:') or die $usage;
137
138 if ($opt_h) {
139   print $usage;
140   exit;
141 }
142
143 if ($opt_H) {
144   open $prg,$prg or die "$0: $prg - $!\n";
145   $_ = <$prg>;
146   $_ = <$prg>;
147   while (<$prg>) {
148     last if /^\s*$/ or /^#\s*\d\d\d\d-\d\d-\d\d/;
149     print;
150   }
151   exit;
152 }
153
154 if ($opt_r) {
155   die "usage: $0 -r version-number file\n" unless @ARGV;
156   if ($ARGV[0] =~ /^(\d\d?|\.)$/) { $opt_r = shift }
157   else                            { $opt_r = 1 }
158   die "usage: $0 -r version-number file\n" if scalar @ARGV != 1;
159 }
160
161 if ($opt_d) {
162   if (@ARGV and $ARGV[0] =~ /^\d\d?(:\d\d?)?$/) { $opt_d = shift }
163   else                                          { $opt_d = 1 }
164   &check_ARGV;
165   die "usage: $0 -d version-number file\n" unless @ARGV;
166 }
167
168 if ($opt_v) {
169   if (@ARGV and $ARGV[0] =~ /^\d\d?$/) { $opt_v = shift }
170   else                                 { $opt_v = 1 }
171   &check_ARGV;
172   die "usage: $0 -v version-number file\n" unless @ARGV;
173 }
174
175 if ($0 eq 've' or $opt_e) {
176   $a = pop @ARGV or die $usage;
177   $opt_e = 1;
178 } else {
179   $a = shift @ARGV;
180   die $usage if not $opt_r and @ARGV;
181 }
182
183 unless (-e $vvrc) {
184   open $vvrc,'>',$vvrc or die "$0: cannot write $vvrc - $!\n";
185   print {$vvrc} q{
186 $exclude = q(
187   \.tmp$
188   ^mutt-.+-\d+
189   ^#.*#$
190 );
191
192 @diff = qw'diff -u';
193
194 };
195   close $vvrc;
196 }
197
198 require $vvrc;
199
200 if ($a) {
201
202   $file = realfilename($a);
203   $ofile = "$file~";
204   $bfile = basename($file);
205   $dir = dirname($file);
206   $vdir = "$dir/.versions";
207   $vfile = "$vdir/$bfile";
208   $vfile0 = "$vfile~0~";
209   $vfile1 = "$vfile~1~";
210   $vfile01 = "$vfile~01~";
211
212   # change eugid if root and version directory belongs user
213   my @s = stat($vdir);
214   if ($> == 0 and (not @s or $s[4])) {
215     if (my @s = stat($a)) {
216       $) = $s[5];
217       $> = $s[4];
218     }
219   }
220
221   if ($opt_r ne '.' and not ($opt_M or $opt_L)) {
222     if (not -e $file and -s $vfile) {
223       warn "$0: $a does not exist any more\n";
224       print "found $vfile - recover it? ";
225       $_ = <STDIN>;
226       copy($vfile,$file,'.') if /^y/i;
227       exit 0;
228     }
229     die "$0: $a does not exist\n" unless -e $file;
230     die "$0: $a is not a regular file\n" if -l $file or not -f $file;
231   }
232 } else {
233   $file = '*';
234   $vdir = ".versions";
235 }
236
237 if ($opt_M) {
238   if (-d $opt_M and not -l $opt_M) {
239     my $vvv = "$opt_M/.versions";
240     mkdir $vvv;
241     die "$0: cannot mkdir $vvv - $!\n" unless -d $vvv;
242     opendir $vvv,$vvv or die "$0: cannot opendir $vvv - $!\n";
243     while (my $v = readdir($vvv)) {
244       mv100("$opt_M/$1") if -f "$vvv/$v" and $v =~ /(.+)~1~$/;
245     }
246     close $vvv;
247     $vvv .= "/.versions";
248     unless (-d $vvv) {
249       mkdir $vvv or die "$0: cannot mkdir $vvv - $!\n";
250     }
251     $vvv .= "/n";
252     unlink $vvv;
253     symlink 100,$vvv or die "$0: cannot create $vvv - $!\n";
254   } else {
255     die "usage: $0 -M file\n" if @ARGV or $opt_r;
256     mv100($opt_M);
257   }
258   exit;
259 }
260
261 if ($opt_L) {
262   if (-d $opt_L and not -l $opt_L) {
263     my $vvv = "$opt_L/.versions";
264     mkdir $vvv;
265     die "$0: cannot mkdir $vvv - $!\n" unless -d $vvv;
266     opendir $vvv,$vvv or die "$0: cannot opendir $vvv - $!\n";
267     while (my $v = readdir($vvv)) {
268       mv10("$opt_L/$1") if -f "$vvv/$v" and $v =~ /(.+)~01~$/;
269     }
270     closedir $vvv;
271     $vvv .= "/.versions";
272     unless (-d $vvv) {
273       mkdir $vvv or die "$0: cannot mkdir $vvv - $!\n";
274     }
275     $vvv .= "/n";
276     unlink $vvv;
277     symlink 10,$vvv or die "$0: cannot create $vvv - $!\n";
278   } else {
279     die "usage: $0 -L file\n" if @ARGV or $opt_r;
280     mv10($opt_L);
281   }
282   exit;
283 }
284
285 if ($opt_e) {
286   die $usage unless $a;
287   $editor = $ENV{EDITOR} or die "$0: environment variable EDITOR not set\n";
288   system(qw'vv -s',$file) if -f $file; # save current version
289   system($editor,@ARGV,$file); exit $? if $?;
290   unlink $ofile;                       # delete new file~ created by editor
291   system(qw'vv -0',$file);             # post rotating
292   system(qw'vv -b',$file);             # save backup
293   exit;
294 }
295
296 if ($opt_v) {
297   die "$0: no such file $bfile\n" unless $bfile;
298   if (-f "$vfile~0$opt_v~") { $vfile .= "~0$opt_v~" }
299   else                      { $vfile .= "~$opt_v~" }
300   if (-f $vfile) {
301     if (-t STDOUT) {
302       if (($ENV{EDITOR}||$0) =~ /jed/) {
303         $ENV{JEDINIT} = "SAVE_STATE=0";
304         exec 'jed',$vfile,qw'-tmp -f set_readonly(1)';
305       } elsif ($ENV{PAGER}) {
306         exec $ENV{PAGER},$vfile;
307       } else {
308         exec 'view',$vfile;
309       }
310     } else {
311       exec 'cat',$vfile;
312     }
313   } else {
314     die "$0: no $vfile\n";
315   }
316   exit;
317 }
318
319 if ($opt_p) {
320   opendir $vdir,$vdir or die "$0: no $vdir\n";
321   while ($vfile = readdir($vdir)) {
322     next unless -f "$vdir/$vfile";
323     $bfile = $vfile;
324     $bfile =~ s/~\d\d?~$//;
325     if (not -f $bfile or -l $bfile) {
326       unlink "$vdir/$vfile";
327       $purge{$bfile}++;
328     }
329   }
330   if (@purge = keys %purge) {
331     foreach $p (@purge) {
332       printf "%2d %s~ purged\n",$purge{$p},$p;
333     }
334   }
335   exit;
336 }
337
338 if ($opt_m) {
339   migrate('.');
340   exit;
341 }
342
343 if (length($opt_r)) {
344   die "$0: no such file $bfile\n" unless $bfile;
345   if ($opt_r eq '.') {
346     die "$0: no $vfile\n" unless -f $vfile;
347     copy($vfile,$file,$opt_r);
348   } else {
349     if ($opt_r =~ /^\d$/ and -f "$vfile~0$opt_r~") {
350       $vfile .= "~0$opt_r~"
351     } else {
352       $vfile .= "~$opt_r~"
353     }
354     die "$0: no version $opt_r for $file\n" unless -f $vfile;
355     if ($nfile = shift @ARGV) {
356       copy($vfile,$nfile);
357     } else {
358       copy($file,$vfile0) if mtime($file) > mtime($vfile0);
359       copy($vfile,$file);
360     }
361   }
362   exit;
363 }
364
365 if (length($opt_d)) {
366   die "$0: no such file $bfile\n" unless $bfile;
367   @diff = qw'diff -u' unless @diff;
368   if ($opt_d =~ /^(\d\d?):(\d\d?)$/) {
369     if (-f "$vdir/$bfile~0$1~" and -f "$vdir/$bfile~0$2~") {
370       exec @diff,"$vdir/$bfile~0$2~","$vdir/$bfile~0$1~"
371     } else {
372       exec @diff,"$vdir/$bfile~$2~","$vdir/$bfile~$1~"
373     }
374   } else {
375     if (-f "$vdir/$bfile~0$opt_d~") {
376       exec @diff,"$vdir/$bfile~0$opt_d~",$file;
377     } else {
378       exec @diff,"$vdir/$bfile~$opt_d~",$file;
379     }
380   }
381   exit $!;
382 }
383
384 if ($opt_s) {
385   die $usage unless $file;
386   if ($exclude) {
387     $exclude =~ s/^\s+//;
388     $exclude =~ s/\s+$//;
389     $exclude =~ s/\s+/|/g;
390     if ($bfile =~ /$exclude/) {
391       warn "\r\n$0: ignoring $bfile\n";
392       exit;
393     }
394   }
395   unless (-d $vdir) {
396     mkdir $vdir or die "$0: cannot mkdir $vdir - $!\n";
397   }
398   chmod 0777,$vdir if (stat $dir)[2] & 00002;
399
400   # migrate old file~ to versions
401   if (-f $ofile and not -l $ofile and -r $ofile) {
402     $vfn = rotate($vfile);
403     rename($ofile,$vfn);
404   }
405
406   # rotate and save if file has changed
407   if (-f $vfile1) {
408     if (md5f($vfile1) ne md5f($file)) {
409       $vfn = rotate($vfile);
410       copy($file,$vfn);
411     }
412     exit;
413   }
414   # rotate and save if file has changed
415   if (-f $vfile01) {
416     if (md5f($vfile01) ne md5f($file)) {
417       $vfn = rotate($vfile);
418       copy($file,$vfn);
419     }
420     exit;
421   }
422   # save new file
423   if ((readlink("$vdir/.versions/n")||10) == 100) {
424     copy($file,$vfile01);
425   } else {
426     copy($file,$vfile1);
427   }
428   exit;
429 }
430
431 # backup version
432 if ($opt_b) {
433   die $usage unless $file;
434   unless (-d $vdir) {
435     mkdir $vdir or die "\r\n$0: cannot mkdir $vdir - $!\n";
436   }
437   copy($file,$vfile);
438   if ($ENV{VIMRUNTIME}) {
439     print "\n";
440   } else {
441     warn "$file --> $vfile\n" unless $opt_q;
442   }
443   exit;
444 }
445
446 # special post rotating from -e
447 if ($opt_0) {
448   my @sb = stat $file or die "$0: $file - $!\n";
449   if (-f $vfile1) {
450     while (my @sv = stat $vfile1) {
451       # no version change?
452       if ($sb[7] == $sv[7] and $sb[9] == $sv[9]) {
453         # rotate back
454         rb10($vfile);
455       } else {
456         last;
457       }
458     }
459   }
460   if (-f $vfile01) {
461     while (my @sv = stat $vfile01) {
462       # no version change?
463       if ($sb[7] == $sv[7] and $sb[9] == $sv[9]) {
464         # rotate back
465         rb10($vfile);
466       } else {
467         last;
468       }
469     }
470   }
471   exit;
472 }
473
474 # delete last version, roll back
475 if ($opt_D) {
476   die "usage: $0 -D file\n" unless $vfile1 or $vfile01;
477   stat $file or die "$0: $file - $!\n";
478   # 0 version?
479   if (-f $vfile0) {
480     unlink $vfile0;
481   } else {
482     # rotate back
483     rb10($vfile) if -f $vfile1;
484     rb100($vfile) if -f $vfile01;
485   }
486   exec $0,'-l',$file;
487   exit;
488 }
489
490 # default!
491 if ($opt_l) {
492   `stty -a` =~ /columns (\d+)/;
493   $tw = ($1 || 80)-36;
494   if (opendir $vdir,$vdir) {
495     while ($vfile = readdir($vdir)) {
496       if (-f "$vdir/$vfile") {
497         if ($bfile) {
498           if ($vfile =~ /^\Q$bfile\E~(\d\d?)~$/) {
499             push @{$v{$file}},$1;
500           }
501         } else {
502           if ($vfile =~ /^(.+)~(\d\d?)~$/) {
503             push @{$v{$1}},$2;
504           } else {
505             push @{$v{$vfile}},0;
506           }
507         }
508       }
509     }
510     closedir $vdir;
511     $ct = '';
512     foreach $file (sort keys %v) {
513       if (not -f $file or -l $file) {
514         warn "$0: orphaned $file~\n";
515         next;
516       }
517       @v = sort @{$v{$file}};
518       if ($bfile) {
519         @stat = stat $file or die "$0: $file - $!\n";
520         print "version bytes        date time";
521         if (${'opt_+'}) {
522           print "     content";
523           $ct = content($file);
524           $ct =~ s/(.{$tw}).+/$1*/;
525         }
526         print "\n";
527         if (length($v[0]) == 1) { $lf = "%s  %10s  %s %s\n" }
528         else                    { $lf = "%2s %10s  %s %s\n" }
529         printf $lf,'.',size($stat[7]),isodate($stat[9]),$ct;
530         foreach $v (@v) {
531           $vfile = "$vdir/$bfile~$v~";
532           @stat = stat $vfile or next;
533           if (${'opt_+'}) {
534             $ct = content($vfile);
535             $ct =~ s/(.{$tw}).+/$1*/;
536           }
537           printf $lf,int($v),size($stat[7]),isodate($stat[9]),$ct;
538         }
539       } else {
540         my $n = scalar(@v);
541         $n-- if $v[0] == 0; # do not count zero version
542         printf "%d %s\n",$n,$file;
543       }
544     }
545   }
546   exit;
547 }
548
549
550 sub size {
551   my $s = shift;
552   if    ($s > 9999999999) { $s = int($s/2**30).'G' }
553   elsif ($s > 9999999)    { $s = int($s/2**20).'M' }
554   elsif ($s > 9999)       { $s = int($s/2**10).'k' }
555   return $s;
556 }
557
558
559 sub content {
560   my $file = shift;
561   my $ct;
562   local $_;
563
564   chomp ($ct = `file $file`);
565   $ct =~ s/.*?: //;
566   $ct =~ s/,.*//;
567
568   if ($ct =~ /text/ and open $file,$file) {
569     read $file,$_,1024;
570     close $file;
571     s/[\x00-\x20]+/ /g;
572     s/^ //;
573     s/ $//;
574     $ct = '"'.$_.'"';
575   }
576
577   return $ct;
578 }
579
580
581 sub isodate {
582   my @d = localtime shift;
583   return sprintf('%d-%02d-%02d %02d:%02d:%02d',
584                  $d[5]+1900,$d[4]+1,$d[3],$d[2],$d[1],$d[0]);
585 }
586
587 sub rotate {
588   my $vf = shift; # version base file
589   my $vf1 = "$vf~1~";
590   my $vf01 = "$vf~01~";
591   my ($vfi,$vfn);
592
593   if (-f $vf1) {
594     for (my $i = 8; $i >= 0; $i--) {
595       $vfi = sprintf("%s~%d~",$vf,$i);
596       $vfn = sprintf("%s~%d~",$vf,$i+1);
597       if (-e $vfi) {
598         rename $vfi,$vfn or die "$0: $vfi --> $vfn : $!\n";
599       }
600     }
601     # was there a version 0?
602     if (-e $vf1) {
603       my $bf = $vf;
604       $bf =~ s:/\.versions/:/:;
605       my @sb = stat $bf;
606       my @sv = stat $vf1;
607       # version change? (other size or mtime)
608       if (@sb and @sv and $sb[7] == $sv[7] and $sb[9] == $sv[9]) {
609         # same version
610         unlink $vf1;
611       } else {
612         # other version
613         rotate($vf);
614       }
615     }
616     return "$vf~1~";
617   } elsif (-f $vf01) {
618     for (my $i = 98; $i >= 0; $i--) {
619       $vfi = sprintf("%s~%02d~",$vf,$i);
620       $vfn = sprintf("%s~%02d~",$vf,$i+1);
621       if (-e $vfi) {
622         rename $vfi,$vfn or die "$0: $vfi --> $vfn : $!\n";
623       }
624     }
625     # was there a version 0?
626     if (-e $vf01) {
627       my $bf = $vf;
628       $bf =~ s:/\.versions/:/:;
629       my @sb = stat $bf;
630       my @sv = stat $vf01;
631       # version change? (other size or mtime)
632       if (@sb and @sv and $sb[7] == $sv[7] and $sb[9] == $sv[9]) {
633         # same version
634         unlink $vf01;
635       } else {
636         # other version
637         rotate($vf);
638       }
639     }
640     return "$vf~01~";
641   }
642
643   return "$vf~1~";
644 }
645
646 sub copy {
647   my ($from,$to,$restore) = @_;
648
649   unless ($restore) {
650     if (-l $file or not -f $file) {
651       die "$0: $file is not a regular file\n";
652     }
653   }
654
655   if (open $to,'>>',$to) {
656     close $to;
657     if (system(qw'rsync -aA',$from,$to) == 0) {
658       if ($ENV{VIMRUNTIME}) {
659         print "\n";
660       } else {
661         warn "$from --> $to\n" unless $opt_q;
662       }
663     } else {
664       exit $?;
665     }
666   } else {
667     die "\r\n$0: cannot write $to - $!\n";
668   }
669 }
670
671 sub realfilename {
672   my $file = shift;
673
674   return $file unless -e $file;
675
676   if (-l $file) {
677     my $link = readlink($file);
678     if ($link !~ /^\// and $file =~ m:(.*/).:) {
679       $link = $1 . $link;
680     }
681     return realfilename($link);
682   } else {
683     return $file;
684   }
685 }
686
687 sub migrate {
688   my $dir = shift;
689   my $vdir = "$dir/.versions";
690   my $dfile;
691
692   opendir $dir,$dir or die "$0: cannot read directory $dir - $!\n";
693   while ($file = readdir($dir)) {
694     $dfile = "$dir/$file";
695     next if -l $dfile or $file eq '.' or $file eq '..';
696     if (-d $dfile and $opt_R and $file ne '.versions') {
697       migrate($dfile);
698     } elsif (-f $dfile and $file =~ /~$/) {
699       if (-d $vdir) {
700         for ($i = 8; $i > 0; $i--) {
701           $n = $i+1;
702           rename "$vdir/$file$i~","$vdir/$file$n~";
703         }
704       } else {
705         mkdir $vdir or die "$0: cannot mkdir $vdir - $!\n";
706       }
707       $nfile = sprintf("%s/%s1~",$vdir,$file);
708       rename $dfile,$nfile or die "$0: cannot move $dfile to $nfile - $!\n";
709       warn "$dfile --> $nfile\n" unless $opt_q;
710     }
711   }
712   closedir $dir;
713 }
714
715 sub mtime {
716   my @s = stat shift;
717   return @s ? $s[9] : 0;
718 }
719
720 sub md5f {
721   my $file = shift;
722   my $md5 = 0;
723   local $/;
724
725   if (open $file,$file) {
726     $md5 = md5_hex(<$file>);
727     close $file;
728   }
729   return $md5;
730 }
731
732
733 # if ARGV is empty use last saved file as default file argument
734 sub check_ARGV {
735   local $_;
736   local *V;
737
738   if (not @ARGV) {
739     if (-d '.versions' and open V,'ls -at .versions|') {
740       while (<V>) {
741         chomp;
742         if (-f) {
743           close V;
744           s/~\d+~$//;
745           @ARGV = ($_);
746           return;
747         }
748       }
749     }
750   }
751
752 }
753
754
755 sub mv10 {
756   my $file = shift;
757   my $vfile = dirname($file).'/.versions/'.basename($file);
758
759   die "$0: $file has no extended versions\n" unless -f "$vfile~01~";
760   for (my $i=1; $i<10; $i++) {
761     my $vfile1 = "$vfile~$i~";
762     my $vfile2 = "$vfile~0$i~";
763     if (-f $vfile2) {
764       warn "$vfile2 --> $vfile1\n" unless $opt_q;
765       rename $vfile2,$vfile1 or die "$0: $!\n";
766     }
767   }
768   for (my $i=10; $i<100; $i++) {
769     unlink "$vfile~$i~";
770   }
771 }
772
773 sub mv100 {
774   my $file = shift;
775   my $vfile = dirname($file).'/.versions/'.basename($file);
776
777   die "$0: $file has already extended versions\n" if -f "$vfile~01~";
778   die "$0: $file has no versions\n" unless -f "$vfile~1~";
779   for (my $i=1; $i<10; $i++) {
780     my $vfile1 = "$vfile~$i~";
781     my $vfile2 = "$vfile~0$i~";
782     if (-f $vfile1) {
783       warn "$vfile1 --> $vfile2\n" unless $opt_q;
784       rename $vfile1,$vfile2 or die "$0: $!\n";
785     }
786   }
787 }
788
789
790 # rotate back
791 sub rb10 {
792   my $vfile = shift;
793
794   for (my $i = 1; $i <= 8; $i++) {
795     my $vfi = sprintf("%s~%d~",$vfile,$i);
796     my $vfn = sprintf("%s~%d~",$vfile,$i+1);
797     if (-f $vfn) {
798       rename $vfn,$vfi;
799     } else {
800       unlink $vfi if $i == 1;
801       last;
802     }
803   }
804 }
805
806
807 # rotate back
808 sub rb100 {
809   my $vfile = shift;
810
811   for (my $i = 1; $i <= 98; $i++) {
812     my $vfi = sprintf("%s~%02d~",$vfile,$i);
813     my $vfn = sprintf("%s~%02d~",$vfile,$i+1);
814     if (-f $vfn) {
815       rename $vfn,$vfi;
816     } else {
817       unlink $vfi if $i == 1;
818       last;
819     }
820   }
821 }
822
823
824
825 sub pathsearch {
826   my $prg = shift;
827
828   foreach my $dir (split(':',$ENV{PATH})) {
829     return "$dir/$prg" if -x "$dir/$prg";
830   }
831 }
832
833
834 # zz is the generic clip board program
835 #
836 # to use zz with vim, write to your .vimrc:
837 #
838 # noremap <silent> zz> :w !zz<CR><CR>
839 # noremap <silent> zz< :r !zz --<CR>
840 sub zz {
841   my $bs = 2**16;
842   my $wm = '>';
843   my ($file,$tee,$x);
844
845   if ("@ARGV" =~ /^(-h|--help)$/) {
846     print <<'EOD';
847 zz is the generic clip board program. It can hold any data, ASCII or binary.
848 The clip board itself is $ZZ (default: $HOME/.zz).
849 See also the clip board editor "ezz".
850 Limitation: zz does not work across accounts or hosts! Use xx instead.
851
852 Options and modes are:
853
854   "zz"              show content of $ZZ
855   "zz file(s)"      copy file(s) content into $ZZ
856   "zz -"            write STDIN (keyboard, mouse buffer) to $ZZ
857   "zz +"            add STDIN (keyboard, mouse buffer) to $ZZ
858   "... | zz"        write STDIN from pipe to $ZZ
859   "... | zz +"      add STDIN from pipe to $ZZ
860   "... | zz -"      write STDIN from pipe to $ZZ and STDOUT
861   "zz | ..."        write $ZZ to pipe
862   "... | zz | ..."  save pipe data to $ZZ (like tee)
863   "zz --"           write $ZZ to STDOUT
864   "zz -v"           show clip board versions (history)
865   "zz -1"           write $ZZ version 1 to STDOUT
866   "zz -9"           write $ZZ version 9 to STDOUT
867
868 Examples:
869
870   zz *.txt
871   ls -l | zz
872   zz | wc -l
873   (within vi)   :w !zz
874   (within vi)   :r !zz
875   (within mutt) |zz
876 EOD
877     exit;
878   }
879
880   if ("@ARGV" eq '-v') {
881     exec qw'vv -+l',$ZZ;
882   }
883
884   if ("@ARGV" =~ /^-(\d)$/) {
885     exec "vv -v $1 '$ZZ' | cat";
886   }
887
888   # read mode
889   if (-t STDIN and not @ARGV or "@ARGV" eq '--') {
890     exec 'cat',$ZZ;
891   }
892
893   # write mode
894   system "vv -s '$ZZ' >/dev/null 2>&1" if -s $ZZ;
895
896   if (@ARGV and $ARGV[0] eq '+') {
897     shift @ARGV;
898     $wm = '>>';
899   }
900
901   if ("@ARGV" eq '-') {
902     @ARGV = ();
903     $tee = 1 unless -t STDIN;
904   }
905
906  $tee = 1 unless @ARGV or -t STDIN or -t STDOUT;
907  $bs = 2**12 if $tee;
908
909   open $ZZ,$wm,$ZZ or die "$0: cannot write $ZZ - $!\n";
910
911   if (@ARGV) {
912     while ($file = shift @ARGV) {
913       if (-f $file) {
914         if (open $file,$file) {
915           while (read($file,$x,$bs)) {
916             my $s = syswrite $ZZ,$x;
917             defined($s) or die "$0: cannot write to $ZZ - $!\n";
918           }
919           close $file;
920         } else {
921           warn "$0: cannot read $file - $!\n";
922         }
923       } elsif (-e $file) {
924         warn "$0: $file is not a regular file\n";
925       } else {
926         warn "$0: $file does not exist\n";
927       }
928     }
929     close $ZZ;
930     $ZZ1 = $ZZ.'~1~';
931     $ZZ1 =~ s:(.*)/(.*):$1/.versions/$2:;
932     if (-e $ZZ and not -s $ZZ and -s $ZZ1 ) {
933       system qw'rsync -aA',$ZZ1,$ZZ;
934     }
935   } else {
936     while (read(STDIN,$x,$bs)) {
937       syswrite $ZZ,$x;
938       syswrite STDOUT,$x if $tee;
939     }
940   }
941
942   exit;
943 }
944
945
946 sub ezz {
947   my $bs = 2**16;
948   my $wm = '>';
949   my $editor = $ENV{EDITOR} || 'vi';
950   my ($out,$file,$x);
951
952   $ENV{JEDINIT} = "SAVE_STATE=0";
953
954   if ("@ARGV" =~ /^(-h|--help)$/) {
955     print <<'EOD';
956 ezz is the edit helper for the zz clip board program.
957 The clip board itself is $ZZ (default: $HOME/.zz).
958
959 Options and modes are:
960
961   "ezz"                    edit $ZZ with $EDITOR
962   "... | ezz"              write STDIN from pipe to $ZZ and call $EDITOR
963   "... | ezz +"            add STDIN from pipe to $ZZ and call $EDITOR
964   "ezz 'perl commands'"    execute perl commands on $ZZ
965   "ezz - 'perl commands'"  execute perl commands on $ZZ and show result
966   "ezz filter [args]"      run filter [with args] on $ZZ
967   "ezz - filter [args]"    run filter [with args] on $ZZ and show result
968
969 Examples:
970
971   ls -l | ezz
972   ezz 's/ /_/g'
973   ezz head -3
974   ezz - head -3
975 EOD
976     exit;
977   }
978
979   system "vv -s '$ZZ' >/dev/null 2>&1" if -s $ZZ;
980
981   unless (-t STDIN) {
982     if ("@ARGV" eq '+') {
983       @ARGV = ();
984       $wm = '>>';
985     }
986     open $ZZ,$wm,$ZZ or die "$0: cannot write $ZZ - $!\n";
987     syswrite $ZZ,$x while read(STDIN,$x,$bs);
988     close $ZZ;
989   }
990
991   if (@ARGV) {
992     $out = shift @ARGV if $ARGV[0] eq '-';
993     $cmd = shift @ARGV or exec 'cat',$ZZ;
994     rename $ZZ,"$ZZ~" or die "$0: cannot move $ZZ to $ZZ~ - $!\n";
995     $cmd = quotemeta $cmd;
996     @ARGV = map { quotemeta } @ARGV;
997     if (pathsearch($cmd)) {
998       system "$cmd @ARGV <'$ZZ~'>'$ZZ'";
999     } else {
1000       system "perl -pe $cmd @ARGV <'$ZZ~'>'$ZZ'";
1001     }
1002     if ($? == 0) { unlink "$ZZ~" }
1003     else         { rename "$ZZ~",$ZZ }
1004     exec 'cat',$ZZ if $out;
1005   } else {
1006     exec $editor,$ZZ;
1007   }
1008   exit;
1009 }
1010
1011
1012 sub install {
1013   my ($dir);
1014   local $| = 1;
1015
1016   print "Installation directory: ";
1017   $dir = <STDIN>||'';
1018   chomp $dir;
1019   $dir =~ s:/+$::;
1020   $dir ||= '.';
1021   if ($dir eq '.') {
1022     unlink qw'zz ezz vv';
1023     link $prg,'zz'   or die "$0: cannot create zz - $!\n";
1024     link $prg,'ezz'  or die "$0: cannot create ezz - $!\n";
1025     rename $prg,'vv' or die "$0: cannot create vv - $!\n";
1026   } else {
1027     die "$0: $dir does not exist\n"     unless -e $dir;
1028     die "$0: $dir is not a directory\n" unless -d $dir;
1029     die "$0: $dir is not writable\n"    unless -w $dir;
1030     chdir $dir or die "$0: cannot cd $dir - $!\n";
1031     unlink qw'zz ezz vv';
1032     system qw'rsync -a',$prg,'vv';
1033     exit $? if $?;
1034     link 'vv','zz'  or die "$0: cannot create $dir/zz - $!\n";
1035     link 'vv','ezz' or die "$0: cannot create $dir/ezz - $!\n";
1036   }
1037   print "Installation completed. See:\n";
1038   print "\t$dir/vv -h\n";
1039   print "\t$dir/zz -h\n";
1040   print "\t$dir/ezz -h\n";
1041   exit;
1042 }