-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathemultiple
More file actions
executable file
·692 lines (543 loc) · 20.3 KB
/
emultiple
File metadata and controls
executable file
·692 lines (543 loc) · 20.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
#!/usr/bin/env perl -w
#
# emultiple: Edit a set of related files in tiled windows.
# 2007-06-06: Written by Steven J. DeRose.
#
use strict;
use Getopt::Long;
our %metadata = (
'title' => "emultiple",
'description' => "Edit a set of related files in tiled windows.",
'rightsHolder' => "Steven J. DeRose",
'creator' => "http://viaf.org/viaf/50334488",
'type' => "http://purl.org/dc/dcmitype/Software",
'language' => "Perl 5.18",
'created' => "2007-06-06",
'modified' => "2020-12-10",
'publisher' => "http://github.com/sderose",
'license' => "https://creativecommons.org/licenses/by-sa/3.0/"
);
our $VERSION_DATE = $metadata{'modified'};
=pod
=head1 Usage
emultiple [options] [file...] [cvs-version-numbers?]
Try to edit several 'versions' of a given file, typically for comparison.
These can be actual version-control-system version (such as from svn or git),
or intermediate forms of a file as it passes through some dataflow (such as
txt to HTML to tidy'd HTML to XML and on through XSLT; or whatever).
The files are displayed in tiled windows if your \$EDITOR supports the
X windows I<--geometry> options.
You can also scroll all the files to a line number or a found string (the
latter is probably more useful, since line numbers are relatively transient).
=head2 Note
The script displays the process id for each editor it starts up.
After opening them, it waits rather than exiting.
You can type 'k' (and RETURN) to kill them all, or just RETURN to exit the
script without killing the editor processes.
This makes it easier to kill many editor windows, but be sure to save first
if you have changes to save.
You can make the script exit without the prompt and wait, with I<--quiet>.
=head1 Options
(ones without arguments can also be prefixed C<no-> to turn off):
=over
=item * B<--base>
Base URI to prefix to I<--uri> values
=item * B<--colors> I<'...'>
Replace the default list of X colors to use.
Else if environment variable \$EMULTIPLE_COLORS is set,
those colors are used.
=item * B<--cvs>
Give one file and any number of cvs version numbers for it (untested).
Those versions are retrieved from CVS and displayed.
=item * B<--define>
Edit, but print only the C<kill> command you would use to kill all the windows
(showing all their process IDs).
Thus, an alias can do C<emultiple> and also define such a command:
e() {
x=`emultiple -define ... \$*`
alias kmultiple=\"\$x\"
}
On error, an C<echo> command is returned with a message.
=item * B<--diff>
Run C<diff> I<--bq> on successive file-pairs and report.
=item * B<--dirlist> I<file directory...>
Open the named file from each listed directory. If you have a workflow
where version of "the same" file get put into a directory for each stage,
you may want to define an alias or environment variable to fill in all those
directories for you, then just give the specific filename each itme.
=item * B<--editor> I<cmdName>
Use the specified editor. Otherwise, uses your \$EDITOR,
or defaults to C<emacs> if that's not set.
=item * B<--even>
If there is an odd number of files, don't center the middle one
(useful if you're using two side-by-side monitors).
=item * B<--find> I<str>
Scroll to the first occurrence of I<str> in each file (cf. I<--line>).
=item * B<--line> I<n>
Open all files to line I<n> (conflicts with I<--find>).
=item * B<--list>
Open the files listed on the command line.
=item * B<--noext> I<s>
Exclude files with extension I<s> (don't give the '.'). Repeatable.
=item * B<--norm>
Run a normalizer on the files first (currently C<normalizeXML -i>).
=item * B<--normopts> I<opts>
Pass options to the specified normalizer.
=item * B<--quiet> OR B<-q>
Suppress most messages, including the 'kill' prompt at the end.
=item * B<--test> OR B<--dry-run>
Find and report files, but don't actually edit (implies I<-v>).
=item * B<--topHalf>
Only use the top 1/2 of the screen height.
=item * B<--uri> I<uri>
Attempt to retrieve from Web and show that, too. Repeatable.
=item * B<--verbose>
Add more detailed messages.
=item * B<--version>
Display version info and exit.
=item * B<-2> OR B<--two>
Force 2 rows of wider windows. This will happen automatically
if less than 60 columns per window would fit in one row.
=back
=head1 Geometry adjustments
Doesn't know if you changed your editor fonts;
you can tell it via these additional options
(if not specified, will try to get information via C<xdpyinfo>):
=over
=item * B<--colpix 7>
How many pixels wide is a character?
=item * B<--rowpix 14>
How many pixels high is a line?
=item * B<--sideBorders 48>
Pixels taken up by left+right borders?
=item * B<--topBorders 80>
Pixels taken up by window header
=item * B<--topspace 50>
How far down to place top of windows.
=item * B<--dwidth n>
Screen pixels X
=item * B<--dheight n>
Screen pixels Y
=back
=head1 Known bugs and limitations
Geometry doesn't work with some editors.
I<--html> is having some problems at the moment.
Can't do more than 2 rows of windows.
Does not erase F</tmp> files used with I<--norm>.
=head1 Related Commands
C<xdpyinfo> provides information about your display dimensions.
Editors supported include emacs, bbedit, vim, and nedit.
=head1 History
2007-06-06: Written by Steven J. DeRose.
2007-06-15 sjd: Add --dynamic, --html, --dlinks, multi-row.
2007-06-21 sjd: Add --rend.
2007-09-17 sjd: Add --index --list --article.
2007-09-19 sjd: Add --colors $EMULTIPLE_COLORS --dirlist --find --line --indent.
2007-10-22 sjd: Add --cvs, strict, shift.
2007-10-31 sjd: Getopt.
2007-11-13 sjd: Add --define, add sub fail(), fix --find. Support vi/vim.
2007-12-06 sjd: Test for -x normalizer. Avoid bxml duplicates.
2007-12-19 sjd: Change URI for --html.
2007-12-20 sjd: Call make-aid-map if needed. Add --topHalf.
2007-12-31 sjd: Fix bug finding indented files. Add --ingest.
2008-01-10 sjd: Add --noext option. Start --diff option.
2008-08-12 sjd: Clean up, remove site-dependencies.
2009-05-26 sjd: More fixing.
2012-09-12, 2013-12-20, 2015-02-10: Clean up. --nedit -> --editor.
2015-09-28: add bbedit support, try to get working again.
2020-12-10: Refactor. Update. Make bbedit work.
=head1 To do
Make it use frames for emacs.
Make --define also put in commands to clean up /tmp files.
Option to get display dimensions from env variables.
Add more editors: TextWrangler, TextEdit, gedit, Coda, TextMate?
Can we kill the editors more easily by killing the process group?
See http://perldoc.perl.org/perlipc.html
=head1 Rights
This work by Steven J. DeRose was first written for PubMedCentral at the
US National Institutes of Health, and is therefore in the public domain.
I (Steven J. DeRose) have made extensive changes since, which I
hereby dedicate to the public domain.
For the most recent version, see L<http://www.derose.net/steve/utilities> or
L<https://github.com/sderose>.
=cut
my @pids = (); # list of forked process ids
# Stuff for color
#
my $dftColorList = "/usr/X11/lib/X11/rgb.txt"; # Currently unused
my $dftcolorstring = "bisque2 pink1 goldenrod1 LemonChiffon DarkSeaGreen1 "
. "PowderBlue LightSlateBlue grey50 Ivory";
my $colorstring = (defined $ENV{EMULTIPLE_COLORS}) ? $ENV{EMULTIPLE_COLORS}:"";
if ($colorstring eq "") {
$colorstring = $dftcolorstring;
}
my $colors = split(/\s+/, $colorstring);
my $dft_dheight = 800;
my $dft_dwidth = 1200;
my $normalizer = "normalizeXML"; # or tidy -indent...
my $dft_normOpts = " -i --btags --noparam";
###############################################################################
# Options
#
my $base = ""; # base URI for use with curl
my $cvs = 0; # Retrieve CVS revisions to show?
my @cvsRevs = (); # List of which CVS revisions
my $define = ""; # Return needed 'kill' command so aliases can use
my $diff = 0; # Run 'diff' on each file w/ next file?
my $dheight = 0;
my $dwidth = 0;
my $editor = $ENV{EDITOR} || "emacs";
my $even = 0; # Force geometry as if even number of windows?
my $find = ""; # String to search for in all files.
my $help = 0; # Show help?
my $line = 0; # Open to this line number.
my $listOfDirs = 0; # Use filename + list of dirs from command line?
my $listOfFiles = 1; # Use list of files from command line?
my @noext = (); # List of extensions to exclude
my $norm = 0; # Normalize the files before editing?
my $normOpts = $dft_normOpts;
my $quiet = 0;
my $test = 0; # Actually launch editor?
my $topHalf = 0;
my $twoRows = 0; # Force 2 rows?
my @uri = ();
my $verbose = 0; # Extra messages?
# Geometry options
#
my $colpix = 7; # How many pixels wide is a character?
my $rowpix = 14; # How many pixels high is a line?
my $sideBorders = 48; # Pixels taken up by left+right borders?
my $topBorders = 80; # Pixels taken up by window header
my $topSpace = 50; # How far down to place top of windows.
sub warn0 { ($verbose >= 0) && warn($_[0] . "\n"); }
sub warn1 { ($verbose >= 1) && warn($_[0] . "\n"); }
sub fail {
if ($define) {
print "echo 'emultiple failed: $_[0], so nothing to kill.'";
exit;
}
else {
die $_[0];
}
}
my %getoptHash = (
"2|two!" => \$twoRows,
"base=s" => \$base,
"colors=s" => \$colorstring,
"colpix=i" => \$colpix,
"cvs!" => \$cvs,
"define!" => \$define,
"diff!" => \$diff,
"dheight=i" => \$dheight,
"dwidth=i" => \$dwidth,
"even!" => \$even,
"find=s" => \$find,
"h|help|?" => sub { system "perldoc $0"; exit; },
"line=i" => \$line,
"listOfDirs|dirlist!" => \$listOfDirs,
"listOfFiles|fileList!"=> \$listOfFiles,
"editor=s" => \$editor,
"noext=s" => \@noext,
"norm!" => \$norm,
"normOpts=s" => \$normOpts,
"q|quiet!" => \$quiet,
"rowpix=i" => \$rowpix,
"sideborders=i" => \$sideBorders,
"test|dry-run|dryrun!" => \$test,
"top|topHalf!" => \$topHalf,
"topborders=i" => \$topBorders,
"topspace=i" => \$topSpace,
"uri=s" => \@uri,
"v|verbose+" => \$verbose,
"version" => sub {
fail("Version of $VERSION_DATE, by Steven J. DeRose.\n");
},
);
Getopt::Long::Configure ("ignore_case");
my $result = GetOptions(%getoptHash);
($result) || fail("Bad options.\n");
if ($test) { $verbose = 1; }
($find ne "" && $line ne 0) &&
warn0("-find ($find) conflicts with -line ($line).");
if ($norm) {
(-x $normalizer) ||
die "Cannot find utility to implement -norm at '$normalizer'.\n";
}
my @colors = split(/\s+/,$colorstring);
my $mainArg = $ARGV[0];
###############################################################################
# Fetch an HTML version, via curl.
#
sub tryForHtml {
my $file = $_[0];
my $filelistref = $_[1];
warn1("tryForHtml: incoming filelist has " . scalar(@$filelistref)
. " items.");
# Assemble the URI
my $addr = "";
my $uri = "$base$addr";
# Curl the file down from the server
my $thtml = "/tmp/aid" . "$addr" . "_$file.html";
warn1("Trying to curl HTML from '$uri'..");
system "curl --silent --output $thtml $uri" ||
warn0("Could not curl HTML from $uri.");
if (-f $thtml) {
my $csize = (-s $thtml);
warn1("curl result for HTML: '$thtml' (size $csize).");
push @$filelistref, $thtml;
}
else {
warn0("No file found at '$thtml' after curl for URI '$uri'.");
return;
}
} # tryForHtml
###############################################################################
# Locate all the files and make a list in @files.
#
sub pickFiles {
my @files = ();
# If they gave -aiid instead of a filename, go find a file.
if (scalar @uri) {
fail("--uri is not yet implemented.\n");
#curl the HTMLs
}
elsif ($cvs) { # CVS versions
warn1("Trying for CVS files.");
my $baseFile = shift;
(-f $baseFile) || fail("Can't find CVS file '$baseFile'.\n");
(-d "CVS") || fail("This is not a CVS directory.\n");
my $tloc = "/tmp/emultiple" . int(rand(100000));
system "mkdir $tloc";
foreach my $r (@ARGV) {
my $fn = "$tloc/$baseFile" . "_$r";
warn1("Retrieving rev '$r' of file '$baseFile'.");
system "cvs update -p -r $r $baseFile >$fn"; # trap error?
push @files, $fn;
}
}
elsif ($listOfFiles) { # user's list of files
warn1("Trying files from command line: " . join(", ", @ARGV));
for my $f (@ARGV) {
if (! -f "$f") {
fail("Cannot find file '$f'.\n");
}
else {
push @files, "$f";
}
}
}
elsif ($listOfDirs) { # user's list of dirs
warn1("Trying for listOfDirs.");
my $fname = shift;
($fname =~ m|/|) &&
fail("First arg should be just a filename, not '$fname'.\n");
for my $d (@ARGV) {
if (! -d "$d") { warn0("Cannot find directory '$d'."); }
elsif (! -f "$d/$fname") { warn0("Cannot find file '$d/$fname'."); }
else { push @files, "$d/fname"; }
}
}
else {
warn0("No file-choice method.");
}
# Delete any files with extension the user specified -noext for
if (scalar(@noext) > 0) {
warn1("Before extension pruning: " . (scalar(@files)) . " files.");
for (my $f=0; $f<scalar(@files); $f++) {
for (my $x=0; $x<scalar(@noext); $x++) {
if ($files[$f] =~ m/\.$noext[$x]$/) {
delete $files[$f];
last;
}
}
}
}
warn1("Checking existence for " . (scalar(@files)) . " files.");
my @files2 = ();
for my $f (@files) {
if (! -f "$f") {
warn0(" Cannot find file '$f'.");
}
else {
warn1(" Found file '$f'.");
push @files2, "$f";
}
}
@files = @files2;
warn1("Files to show (total " . scalar(@files) . "):\n"
. " " . join("\n ",@files));
return @files;
}
###############################################################################
# Figure out display characteristics and what size windows we can make.
#
sub runGeometry {
my $filesRef = $_[0];
my @files = @{$filesRef};
my $nwindows = scalar @files;
if ($even && $nwindows%2) { $nwindows++; }
if ($dwidth > 0 && $dheight > 0) {
($verbose) &&
warn0("Display size set to $dwidth x $dheight.");
}
#elsif ($ENV{DISPLAY}) {
#}
else {
($verbose) &&
warn0("Trying to find display size via 'xdpyinfo'.");
my $info = `xdpyinfo | grep 'dimensions:'`;
chomp $info;
$info =~ m/.*dimensions:\s+([0-9]+)x([0-9]+)\spixels/;
if ($1 && $2) {
$dwidth = $1; $dheight = $2;
($verbose ) &&
warn0("Your display is $dwidth wide by $dheight high.");
}
else {
($quiet) || warn0("Couldn't get screen dimensions from xdpyinfo.");
$dwidth = $dwidth; $dheight = $dft_dheight;
}
}
$dheight -= 20; # allow a bit for windows footer or mac menubar
# Figure geometry parameters, first assuming one row of windows
my $windowXsize = int($dwidth / $nwindows + 0.5);
my $windowYsize = $dheight - $topSpace;
if ($topHalf) { $windowYsize /= 2; }
my $windowCols = int(($windowXsize - $sideBorders) / ($colpix+0.0) + 0.5);
my $windowRows = int(($windowYsize - $topBorders ) / ($rowpix+0.0) + 0.5);
# Recalculate for two rows if needed or requested
if ($twoRows || $windowCols < 60) {
my $perrow = int((1+ (scalar @files)) / 2);
$windowXsize = int($dwidth / $perrow + 0.5);
$windowYsize = int($windowYsize / 2);
$windowCols = int(($windowXsize - $sideBorders) / ($colpix+0.0) + 0.5);
$windowRows = int(($windowYsize - $topBorders ) / ($rowpix+0.0) + 0.5);
warn1("Can fit $windowCols columns per window with 2 rows.");
}
warn1(" Pane x $windowXsize pixels, $windowCols columns); "
. "y $windowYsize pixels, $windowRows rows.");
# Start editing the files in nicely-arranged windows.
#
my $vcmd = ""; # weird special-case for vim -O option.
my $x = 0; # left edge of current window
my $y = $topSpace; # top edge of current window
for (my $i=0; $i<scalar @files; $i++) {
my $f = $files[$i];
warn1("Editing '$f'.");
# Bail out if just testing
($test) && next;
my $color = $colors[ $i % scalar(@colors) ];
runEditor($f, $windowCols, $windowRows, $x, $y, $colors);
# Shift to next window position
$x += $windowXsize;
if ($x > $dwidth - 10) {
$x = 0;
$y += $windowYsize; # Calculate to clear first row
}
} # for each file to edit
} # runGeometry
###############################################################################
# Start up an editor on a file
#
sub runEditor {
my ($f, $windowCols, $windowRows, $x, $y, $color) = @_;
if (!$f || ! -f $f) {
warn0("File not found: $f.");
return;
}
# Set up the calculated window geometry
my $geom = "-geometry $windowCols" . "x$windowRows" . "+$x+$y";
# Are we supposed to scroll somewhere? Append '+' option.
my $scrollOption = "";
if ($line) {
$scrollOption = "+$line";
}
elsif ($find) {
my $fcmd = "grep -n -m 1 '$find' $f 2>/dev/null | sed 's/:.*\$//'";
my $fline = `$fcmd`; chomp $fline;
$fline -= 0;
warn1(" Searched for '$find': got line '$fline'.");
if ($fline > 0) {
$scrollOption = "+$fline";
}
}
# Assemble the editor startup commands
my $ecmd = ""; my $bg = 0;
if ($editor eq "emacs") {
$ecmd = "emacs -title $f $geom -bg $colors $scrollOption $f";
$bg = 0;
}
elsif ($editor eq "nedit") {
$ecmd = "nedit -wrap $geom -background $colors $scrollOption $f";
$bg = 1;
}
else {
warn0("Sorry, don't know \$EDITOR '$ENV{EDITOR}'. Trying...");
$ecmd = "$ENV{EDITOR} $f";
}
forkEditor($ecmd, 1)
}
sub forkEditor {
my ($ecmd, $bg) = @_;
if (my $pid = fork) { # parent process
push @pids, $pid;
warn1(" Forked process ID $pid: '$ecmd'");
}
else { # child process only
exec "$ecmd" . ($bg ? " &":"");
}
}
###############################################################################
# Main
#
my @files = pickFiles();
(scalar @files) || fail("No files found to work on.\n");
warn1(join("\n", @files));
# Diff them if asked
if ($diff) {
for (my $i=1; $i<scalar(@files); $i++) {
my $cmd = "diff -bq $files[$i-1] $files[$i]";
warn0("\n\nRunning: $cmd");
my $rc = system "$cmd";
($quiet) || warn0("******* Diff returned $rc.");
}
}
my $flist = "'" . join("', '", @files) . "'";
my $scrollOption = "";
if ($line) {
$scrollOption = "+$line";
}
my $ecmd = "";
if ($editor eq "vim") {
# vim lacks -geometry option, but has -O feature for side-by-side...
$ecmd = "vim -O " . (scalar @files) . $flist;
forkEditor($ecmd)
}
elsif ($editor eq "bbedit") {
# bbedit lacks -geometry option
my $ecmd = "bbedit $scrollOption $flist";
forkEditor($ecmd)
}
else {
runGeometry(\@files);
}
# Give user a handy way out of all the editor sessions
my $kcmd = "kill -9 " . join(" ", @pids);
if ($define) { # so user can alias or otherwise save it.
print "kill -9 $kcmd\n";
}
else {
my $killed = 0;
if (!$quiet) {
warn0("Type 'k' (+CR) when ready to kill all the editor processes,"
. " or anything else to exit without killing them.");
my $buf = "";
read STDIN, $buf, 1;
if ($buf eq "k") {
system $kcmd;
$killed = 1;
}
}
($killed) || warn0("\nTo kill the emacs processes, use: $kcmd");
}
exit;