-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathecmd
executable file
·814 lines (669 loc) · 26.2 KB
/
ecmd
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
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
#!/usr/bin/env perl -w
#
# ecmd: Find a command/alias/shell function/etc., and edit it.
# 2012-05-08: Written by Steven J. DeRose.
#
use strict;
use Getopt::Long;
our %metadata = (
'title' => "ecmd",
'description' => "Find a command/alias/shell function/etc., and edit it.",
'rightsHolder' => "Steven J. DeRose",
'creator' => "http://viaf.org/viaf/50334488",
'type' => "http://purl.org/dc/dcmitype/Software",
'language' => "Perl 5.18",
'created' => "2012-05-08",
'modified' => "2022-12-01",
'publisher' => "http://github.com/sderose",
'license' => "https://creativecommons.org/licenses/by-sa/3.0/"
);
our $VERSION_DATE = $metadata{'modified'};
=pod
=head1 Usage
ecmd [options] commandName
Find where I<commandName> is defined, and edit its definition (if feasible).
Binaries will not be opened. Scripts will be opened with your \$EDITOR.
For aliases and shell functions, the file where they are defined (if any)
should open at the right line.
The time since last modified is also displayed (unless I<--quiet>).
To just display the location without opening \$EDITOR, set I<--noedit>.
Commands are identified via the C<whence> and/or C<type> commands,
which are run under a fresh interactive bash shell. This means that your usual
shell profiles should be run; however, anything you manually defined
or added to \$PATH may be missed.
If C<whence> and C<type> fail, the directories in \$PATH will be searched.
For aliases and shell functions, the usual shell setup files
are also searched. When a defining file is found, it is searched for
the definition, and your \$EDITOR should open to the correct line.
To search extra files that define aliases or shell functions (perhaps that are
sourced from .bash_profile, .zprofile, etc...), list them in
environment variable C<\$ECMDFILES>, separated by colons like with \$PATH
(except that these should be specific files, not directories).
Or specify them with the (repeatable) I<--addProfile> option.
Use I<-v> to see a list of all the places to be searched (repeatable).
This script was originally written for use with C<bash>, but has been updated
to also handle zsh (such as knowing which profile files to look at).
It might still have problems that depend on the
particular shell (let me know if you hit any).
=head1 Options
(prefix 'no' to negate where applicable)
=over
=item * B<--addProfile> I<path>
Add the file at I<path> to (the end of) the list of places to look for
definitions of aliases and shell functions. Repeatable.
The usual suspects, such as F<.bash_profile>, F<.bashrc>, etc.
are already covered (use C<-v> for a list).
You can also/instead set environment variable C<\$ECMDFILES>.
=item * B<--edit>
If not binary, open the file defining the requested item, using \$EDITOR.
For aliases and shell functions, open it to the start of the actual definition.
Binaries are not opened, though their path is reported.
Use I<--no-edit> to skip opening the found file.
=item * B<--interactive>
When checking what kind of thing the command is,
run C<whence> and C<type> in an interactive shell (default).
=item * B<--login>
When checking what kind of thing the command is,
run C<whence> and C<type> in a login shell.
=item * B<-q> OR B<--quiet>
Suppress most messages.
=item * B<-v> OR B<--verbose>
Add more messages.
=item * B<--version>
Show version info and exit.
=back
=head1 Known Bugs and Limitations
Does not check all possible places that an alias or shell function
could be defined. In particular, does not consider files invoked by a
setup file, or cases where a setup file defines different things in different
situations. However, you can add such files by adding them in environment variable
C<$ECMDFILES> or option C<--addProfile>.
Does not (yet) have an option to keep looking, to find more than one possibility.
Does not expand C<\$BASH_ENV> or C<\$ENV> before trying it as a path.
Assumes your C<\$EDITOR> has a I<+n> option to take you to line number I<n>
(this is used to get you right to the definition of an alias or shell function).
C<emacs>, C<vi>, C<pico>, C<gedit>, C<bbedit>, and C<xedit> are all ok.
It does know a few specific exceptions, such as that C<SublimeText> uses C<:n> instead.
C<kompozer> doesn't seem to have a comparable option (but see
L<http://sourceforge.net/tracker/index.php?func=detail&aid=2941487&group_id=170132&atid=853125>).
C<whence>, C<type>, C<which>, and C<where> return stereotyped responses
for each kind of command.
If your local versions return significantly different forms,
you will need to adjust this script accordingly.
The categories used here are:
ALIAS shell aliases
BUILTIN shell builtin commands
COMMAND scripts and binaries along \$PATH
FUNCTION shell functions
HASHED (unused)
RESERVED shell keyword
NONE (fail)
=head1 Related commands
C<bash>, C<zsh>, C<type>, C<which>, C<shopt>.
L<https://unix.stackexchange.com/questions/322459/is-it-possible-to-check-where-an-alias-was-defined>
provides some useful information on how to locate aliases and such.
=head2 Auto-completion
*nix variants differ in how you configure auto-completion of commands, so you may have
to fiddle around a bit to get that working so you can use C<ecmd> without typing
the entire name for the target command.
To enable auto-completion with this script in C<bash> use
complete -F _command ecmd
On Mac OS X you may need to use the C<zsh> equivalent first,
or install bash_completion, for example as:
sudo port install bash-completion
bash-completion >=2.0 requires bash >=4.1. Also, add to your .bash_profile:
if [ -f /opt/local/etc/profile.d/bash_completion.sh ]; then
. /opt/local/etc/profile.d/bash_completion.sh
fi
See L<http://superuser.com/questions/288438/bash-completion-for-commands-in-mac-os>.
=head2 Categories of commands
The categories of items checked are those distinguished by the C<whence> and C<type>
commands. Commands are found by Bash in this order (same in zsh?):
aliases (can override shell keywords and/or shell builtins)
shell keywords
shell builtins
POSIX special builtins
shell functions (B<cannot> define one overriding a shell keyword)
files (per \$PATH), including:
hashed executables
other executables
This script declines to edit executable files that are binary.
The POSIX special builtins only apply in POSIX mode. They are:
break, :, ., continue, eval, exec, exit, export, readonly, return, set,
shift, trap, unset.
A nice page on bash's startup and other behavior is:
L<http://wiki.bash-hackers.org/scripting/bashbehaviour>.
=head1 To do
Add doc on how to set up autocompletion for this in zsh.
Be able to find the thing that an alias is aliased to and edit C<it>.
=head1 History
=over
=item * 2012-05-08f: Written by Steven J. DeRose.
=item * 2012-11-02: Find specific line for aliases and shell functions.
Check whole set of bash profile-ish files. Warn on non-bash.
Force 'type' in interactive shell so functions/aliases are visible.
=item * 2012-12-06: Add doc on order things are found. Add --noninteractive.
Make -q and -v do something.
=item * 2013-04-08: Start adding checks of files sourced from profiles.
=item * 2013-08-27: Fix emacs line-seek arg for BSD/Mac.
=item * 2014-12-30: Try to get working for shell functions. Add -l.
=item * 2015-02-05: Fix bug where it dropped output from 'type' command.
Add --addProfile.
=item * 2020-04-15: New layout. Add $ECMDPATH support.
=item * 2021-01-21: Map 'open to line number' as needed per EDITOR.
=item * 2021-03-10: Only add line arg if >1. Switch '+n' to ':n' for sublimeText.
=item * 2021-11-17: Clean up, nuke useless error messages, start C<mapType>().
=item * 2021-12-15ff: Rewrite findType() to use `zsh -c "type -w..."`, much cleaner.
=item * 2022-08-18: Refactor and clean up searching. Make use 'whence', which
hands back path info more often.
=item * 2022-12-01: Refactor and clean use of whence/type/which/where.
Show specific sample outputs for each. Clean up category names. Test.
=item * 2023-05-10: Introduce warn\d(). Fix alias and shell functions losing
line numbers. Add getShell(). Rename $ECMDPATHS to $ECMDFILES. Add --all.
=back
=head1 Rights
Copyright 2012-05-08 by Steven J. DeRose. This work is licensed under a
Creative Commons Attribution-Share-alike 3.0 unported license.
See L<http://creativecommons.org/licenses/by-sa/3.0> for more information.
For the most recent version, see L<http://www.derose.net/steve/utilities/>
or L<http://github.com/sderose>.
=cut
###############################################################################
# Options
#
my $doAllFiles = 0;
my @addProfile = ();
my $edit = 1;
my $interactive = 1;
my $login = 1;
my $quiet = 0;
my $verbose = 0;
my %getoptHash = (
"addProfile=s" => \@addProfile,
"all!" => \$doAllFiles,
"edit!" => \$edit,
"h|help" => sub { system "perldoc $0"; exit; },
"interactive!" => \$interactive,
"login!" => \$login,
"q|quiet!" => \$quiet,
"v|verbose+" => \$verbose,
"version" => sub {
die "Version of $VERSION_DATE, by Steven J. DeRose.\n";
},
);
Getopt::Long::Configure ("ignore_case");
(GetOptions(%getoptHash)) || die "Bad options.\n";
($ARGV[0]) || die "No command name specified.\n";
###############################################################################
# Globals
#
my @allPaths; # Paths to other scripts, from $ECMDFILES and --addProfile
my @profilePaths; # Paths to standard bash and zsh .-files
my %lineSeekPrefix = ( # lookups are via lowercased name
'emacs' => '+',
'vi' => '+',
'pico' => '+',
'gedit' => '+',
'bbedit' => '+',
'xedit' => '+',
'sublimetext' => ':',
'kompozer' => '', # not supported?
);
sub lineSeekOption {
my ($editorName, $foundLineNum) = @_;
($foundLineNum) || return '';
(defined $lineSeekPrefix{lc($editorName)}) || return '';
return $lineSeekPrefix{lc($editorName)} . $foundLineNum;
}
sub fmtList {
my (@theList) = @_;
return "\n " . join("\n ", @theList) | "\n";
}
sub warn0 { warn(join("", @_) . "\n"); }
sub warn1 { ($verbose >= 1) && warn(join("", @_) . "\n"); }
sub warn2 { ($verbose >= 2) && warn(join("", @_) . "\n"); }
sub warn3 { ($verbose >= 3) && warn(join("", @_) . "\n"); }
###############################################################################
#
my %typeNames = (
"ALIAS" => "ALIAS", # ll
"HASHED" => "HASHED", # cc
"BUILTIN" => "BUILTIN", # typeset
"COMMAND" => "COMMAND", # *.py
"FUNCTION" => "FUNCTION", # my utils
#"HASHED" => "HASHED", # ???
"RESERVED" => "RESERVED", # if
"NONE" => "NONE",
);
# See what shell commands say about the command. But inside Perl we don't
# have \$PATH or all the aliases, etc. user may have.
# Relevant commands:
# whence -- use -w to get [path]: [typekeyword] output
# type -- in zsh, equivalent to 'whence -v'
# which -- in zsh, equivalent to 'whence -c'
# where --
#
sub findType {
my ($login, $interactive, $cmd) = @_;
my ($typeEnum, $path) = tryWhence($login, $interactive, $cmd);
warn2("tryWhence for '$cmd' got type '$typeEnum', path '$path/'.");
if ($typeEnum eq "NONE") {
($typeEnum, $path) = tryType($login, $interactive, $cmd);
}
warn2("tryType for '$cmd' got type '$typeEnum', path '$path/'.");
# TODO: Maybe add tryWhich, tryWhere.
# Try manual search in the usual suspects
#if ($typeEnum eq "NONE") {
# ($typeEnum, $path) = trySearch($login, $interactive, $cmd);
#}
defined ($typeNames{$typeEnum}) || die
"Unexpected type '$typeEnum'.\n";
warn1("Type: '$typeEnum'. Path: '$path'");
return $typeEnum, $path;
}
# Sample 'whence -w' responses (MacOS):
# ALIAS ll: alias
# BUILTIN set: builtin
# COMMAND cc: command # Does not give path
# FUNCTION gitsubs: function
# HASHED
# RESERVED if: reserved
# NONE zork: none
# run via an interactive shell, so profiles are in effect.
#
sub tryWhence {
my ($login, $interactive, $cmd) = @_;
my $shellCmd = "whence -w '$cmd'";
my $tail = runShellCommandInRealEnv($login, $interactive, $shellCmd, 1);
warn2("'$shellCmd' got: $tail");
($tail =~ m/: alias/) && return "ALIAS", "";
($tail =~ m/: builtin/) && return "BUILTIN", "";
($tail =~ m/: command/) && return "COMMAND", `which $cmd`;
($tail =~ m/: function/) && return "FUNCTION", "";
# HASHED
($tail =~ m/: reserved/) && return "RESERVED", "";
($tail =~ m/: none/) && return "NONE", "";
warn0("Cannot understand '$shellCmd' response:\n $tail");
return "NONE", "";
}
# Sample 'which' / 'whence -c' responses (MacOS, zsh):
# ALIAS ll --> ll: aliased to ls -o
# BUILTIN set --> set: shell built-in command
# COMMAND cc --> /usr/bin/cc
# FUNCTION gitsubs --> gitsubs () { ... }
# HASHED -->
# RESERVED if --> if: shell reserved word
# NONE zork --> (nothing)
# run via an interactive shell, so profiles are in effect.
#
sub tryWhich {
my ($login, $interactive, $cmd) = @_;
my $shellCmd = "which '$cmd'";
my $tail = runShellCommandInRealEnv($login, $interactive, $shellCmd, 1);
warn2("'$shellCmd' got tail: $tail");
($tail =~ m/: aliased to /) && return "ALIAS", "";
($tail =~ m/: shell built-in command/) && return "BUILTIN", "";
(-x $tail) && return "COMMAND", $tail;
($tail =~ m/\S+ \(\) \{/) && return "FUNCTION", "";
# HASHED
($tail =~ m/: shell reserved word/) && return "RESERVED", "";
($tail =~ m/^$/) && return "NONE", "";
warn0("Cannot understand '$shellCmd' response: $tail.");
return "NONE", "";
}
# Sample 'type' / 'whence -v' responses (MacOS, zsh):
# ALIAS ll --> ll is an alias for ls -o
# BUILTIN set --> set is a shell builtin
# COMMAND cc --> cc is /usr/bin/cc
# FUNCTION gitsubs --> gitsubs is a shell function from [path]
# HASHED -->
# RESERVED if --> if is a reserved word
# NONE zork --> zork not found
# run via an interactive shell, so profiles are in effect.
#
sub tryType {
my ($login, $interactive, $cmd) = @_;
my $shellCmd = "type '$cmd'";
my $tail = runShellCommandInRealEnv($login, $interactive, $shellCmd, 1);
warn2("'$shellCmd' got tail: $tail");
($tail =~ m/ is an alias for /) && return "ALIAS", "";
($tail =~ m/ is a shell builtin/) && return "BUILTIN", "";
my $pathPart = $tail;
$pathPart =~ s/^\S+ is //;
(-x $pathPart) && return "COMMAND", $pathPart;
($tail =~ m/ is a shell function from (.*)/) && return "FUNCTION", $1;
# HASHED
($tail =~ m/ is a reserved word/) && return "RESERVED", "";
($tail =~ m/ not found/) && return "NONE", "";
warn0("Cannot understand '$shellCmd' response: $tail.");
return "NONE", "";
}
# Sample 'where' responses (MacOS, zsh):
# ALIAS ll: aliased to ls -o
# BUILTIN set: shell built-in command
# COMMAND /usr/bin/cc
# FUNCTION gitsubs () { ... }
# HASHED
# RESERVED if: shell reserved word
# NONE zork not found
# run via an interactive shell, so profiles are in effect.
#
sub tryWhere {
my ($login, $interactive, $cmd) = @_;
my $shellCmd = "where '$cmd'";
my $tail = runShellCommandInRealEnv($login, $interactive, $shellCmd, 1);
warn2("'$shellCmd' got tail: $tail");
($tail =~ m/: aliased to /) && return "ALIAS", "";
($tail =~ m/: shell built-in command/) && return "BUILTIN", "";
(-x $tail) && return "COMMAND", $tail;
($tail =~ m/\S+ \(\) \{/) && return "FUNCTION", "";
# HASHED
($tail =~ m/: shell reserved word/) && return "RESERVED", "";
($tail =~ m/ not found/) && return "NONE", "";
warn0("Cannot understand '$shellCmd' response: $tail.");
return "NONE", "";
}
###############################################################################
# Run a command via a shell, and grab the output. This is a bit of a pain
# because you need to duplicate the user's environment in a new shell, including
# aliases and other setup. Also, starting a shell may cause other output.
#
sub getShell {
# Ideally, this should return the CURRENT shell, not the default.
# But that's hard (see my 'whichShell' script).
my $shPath = $ENV{SHELL};
$shPath =~ s|.*/||;
return $shPath;
}
sub runShellCommandInRealEnv {
my ($login, $interactive, $shellCmd, $nTail) = @_;
my $toRun = "";
my $tmpFile = "/tmp/ecmd.tmp";
if (1) { # zsh
my $inter = $interactive ? "-i":"";
my $login = $login ? "-l":"";
# grab just last line, since launching zsh may cause other output.
my $typeCmd = "$shellCmd | tail -n $nTail >$tmpFile";
$toRun = "zsh $inter $login -c '$typeCmd' 2>/dev/null";
}
else { # bash
$toRun = "type -w '$shellCmd' 2>/dev/null >$tmpFile";
}
warn2("Running: $toRun");
my $cruft = `$toRun`;
warn3("### Result: <<<\n$cruft>>>");
my $tailLines = `cat $tmpFile`;
($tailLines) || die "No output from '$toRun'.\n";
chomp $tailLines;
warn2("### Last $nTail lines: <<<\n $tailLines>>>");
return $tailLines;
}
# Display the command, what it was found to be, and if the definition was found,
# the file and optional line number (latter for shell functions and aliases).
#
sub reportType {
my ($theCmdName, $typ, $file, $line) = @_;
if ($quiet) { return; }
if (!defined $line) { $line = "?"; }
my $msg = sprintf("'%s' is %s %s. ", $theCmdName, getArticle($typ), $typ);
if ($file) {
$msg .= "Found at $file, line $line.\n";
}
print "$msg\n";
}
sub getArticle {
my ($word) = @_;
# Haccidents ardly hever appen.
return ($word =~ m/^[aeiouh]/) ? "an":"a";
}
###############################################################################
# Places to look for startup/profile files
#
sub setupPaths {
@profilePaths = getProfilePaths();
my $ep = $ENV{"ECMDFILES"};
my @ECMDFILES = ();
if ($ep) {
@ECMDFILES = split /:/, $ENV{"ECMDFILES"};
}
warn3("\nEnv var \$ECMDFILES gives:" . fmtList(@ECMDFILES));
@allPaths = (@profilePaths, @ECMDFILES);
warn2(sprintf(
"profilePaths |%d|, allPaths |%d|\n", scalar @profilePaths, scalar @allPaths));
warn2("\n\@profilePaths for alias/function definitions: " . fmtList(@profilePaths));
my $sjdDir = $ENV{"sjdUtilsDir"};
if ($sjdDir && -d "$sjdDir") {
warn2("Adding sjdUtilDir children from '$sjdDir'.\n");
my $bset = "$sjdDir/*";
push @allPaths, $bset;
}
}
sub getProfilePaths {
my $HOME = $ENV{HOME};
my $profile = "$HOME/.bash_profile";
my @profilePaths = ();
my $whichShell = getShell();
if ($whichShell eq "zsh") {
@profilePaths = (
"/etc/zshenv", # All
"$HOME/.zshenv",
"/etc/zprofile", # Login
"$HOME/.zprofile",
"/etc/zshrc", # Interactive
"$HOME/.zshrc",
"/etc/zlogin", # Login
"$HOME/.zlogin",
);
}
elsif ($whichShell eq "bash") {
@profilePaths = (
"/etc/profile", #
"$HOME/.bash_profile", #
"$HOME/.bash_login", #
"$HOME/.profile", #
"/etc/bash.bashrc", # Interactive
"$HOME/.bashrc", # Interactive
);
if (defined $ENV{BASH_ENV}) {
warn2("Adding path: '$ENV{BASH_ENV}'");
push @profilePaths, $ENV{BASH_ENV};
}
}
else {
warn0("Unknown shell '$whichShell'.");
}
if (defined $ENV{ENV}) { # only if 'sh'
push @profilePaths, $ENV{ENV};
}
if (@addProfile) {
push @profilePaths, @addProfile;
}
#warn2("Profiles to search:" . fmtList(@profilePaths));
return @profilePaths;
}
###############################################################################
# Functions to find specific definitions within a file (just use grep).
#
# TODO: Perhaps add option to find class/function defs in prog langs
my %functionRegexes = (
"bash" => "^\\s*(\\w+)\\s*{",
"C" => "^\\s*(\\w+)\\s*\\*?(\\w+)\\(",
"javascript" => "^\\s*function\\s+(\\w+)\\s*\(",
"Perl" => "^\\s*sub\\s+\\w+\\s*{",
"Python" => "^\\s*def\\s+\\w+\\s*\(",
"Ruby" => "^\\s*def\\s+\\w+\\s*\(?",
"Scheme" => "^\\s*\\(defun\\s",
"XSLT" => "^\\s*(<xsl:function\\s",
# Java PHP C++ C# Rust PS SQL Ruby swift
);
# Search for a given regex in a path somebody else found already,
# or if that's nil then search all the files in passed lists.
# Return all the hits found in the first file that has any,
# or if $doAllFiles is set then all the hits in all the files.
# Returns: a list of path:lineNum strings (as from grep).
#
sub findInProfiles {
my ($regex, $path) = @_;
my @allHitLocs = ();
if ($path) {
warn1("findInProfiles: Searching only in path: $path.");
my $hitLocsRef = getAllLines($path, $regex);
push(@allHitLocs, @{$hitLocsRef});
}
else {
warn1(sprintf("findInProfiles: Searching in %d setup files.", scalar(@allPaths)));
warn2(" " . join("\n ", @allPaths));
foreach my $curPath (@allPaths) {
my $hitLocsRef = getAllLines($curPath, $regex);
my $nHitLocs = scalar @{$hitLocsRef};
if ($nHitLocs == 0) {
warn3(" 0 hits in '$curPath'");
next;
}
else {
warn1(sprintf(" %d hits in '%s', first line %d", $nHitLocs,
$curPath, ($nHitLocs) ? $hitLocsRef->[0] : -1));
push(@allHitLocs, "$curPath:" . $hitLocsRef->[0]);
}
if (scalar @allHitLocs && !$doAllFiles) { last; }
}
}
return(\@allHitLocs);
}
sub findAlongPath { # TODO Finish and hook up if needed
my ($regex, $path) = @_;
my @allHitLocs = ();
warn3(sprintf("findAlongPath: Searching in %d PATH entries for '%s'.", $regex));
foreach my $curPath (split($ENV{PATH}, ":")) {
warn2("Calling getAllHits to find /$regex/ in '$curPath'.");
die "Not yet supported";
my @hitLocs = `grep -n '$regex' $curPath/*`;
my $nHitLocs = scalar @hitLocs;
warn2(" $nHitLocs hits in '$curPath'");
if ($nHitLocs == 0) { next; }
push(@allHitLocs, @hitLocs);
if (scalar @allHitLocs && !$doAllFiles) { last; }
}
return(\@allHitLocs);
}
# Return ->array of lines matching the regex.
#
sub getAllLines {
my ($file, $regex) = @_;
my @hitLocs = ();
if (! -f $file) {
warn1("File not found: '$file'.");
return(\@hitLocs);
}
warn3("grepping in: '$file'.");
my @bufLines = `grep -n '$regex' $file`;
if ($? == 256) {
# No hits
}
elsif ($? > 0 && $? != 256) {
warn3("grep failed, rc $?");
}
else {
warn3(sprintf("grep got %d hits:\n %s", scalar @bufLines,
join("\n ", @bufLines)));
foreach my $hit (@bufLines) {
if ($hit =~ m/^ *(\d+):/) {
my $lineNum = $1;
push @hitLocs, $lineNum;
}
else {
warn0("Unexpected grep result line: $hit");
}
}
}
return(\@hitLocs);
}
sub notFoundMsg {
my ($cmdName, $type) = @_;
reportType($cmdName, $type, sprintf(
", not found in any of %d profiles, %d other files (see \$ECMDFILES).",
scalar @profilePaths, scalar @allPaths));
}
###############################################################################
# Main
#
my $cmd = $ARGV[0];
my $foundLineNum = 1;
# Warn about environment issues
#
my $shell = $ENV{SHELL} || "";
($shell ne "/bin/zsh" && $verbose) && print
"Your shell is $ENV{SHELL}, not /bin/zsh. This may not work....\n";
setupPaths();
# This only sets $path for some things (like, not aliases)
my ($typ, $path) = findType($login, $interactive, $cmd);
chomp $path;
warn1("Initial findType() says type '$typ', path '$path'.\n");
if ($typ eq "FUNCTION") {
my $hitLocsRef = findInProfiles("^ *$cmd()\\s{", $path);
if (scalar @{$hitLocsRef}) {
foreach my $hitLoc (@{$hitLocsRef}) {
$hitLoc =~ m/(.*):(.*)/;
$path = $1;
$foundLineNum = $2;
warn2("Reporting $hitLoc --> $1, $2.");
reportType($cmd, "FUNCTION", $1, $foundLineNum);
}
}
else {
notFoundMsg($cmd, "FUNCTION");
}
}
elsif ($typ eq "ALIAS") {
my $hitLocsRef = findInProfiles("^\\s*alias\\s+$cmd=", $path);
if (scalar @{$hitLocsRef}) {
foreach my $hitLoc (@{$hitLocsRef}) {
warn2("Reporting $hitLoc");
$hitLoc =~ m/(.*):(.*)/;
$path = $1;
$foundLineNum = $2;
reportType($cmd, "ALIAS", $1, $foundLineNum);
}
}
else {
notFoundMsg($cmd, "ALIAS");
}
}
elsif ($typ eq "BUILTIN") {
reportType($cmd, "BUILTIN", $path);
}
elsif ($typ eq "COMMAND") {
reportType($cmd, "COMMAND", $path);
}
elsif ($typ eq "HASHED") { # TODO: Not yet supported
reportType($cmd, "HASHED", $path);
}
elsif ($typ eq "RESERVED") {
reportType($cmd, "reserved shell keyword", "");
}
elsif ($typ eq "NONE") {
# Could still be alias/shell function that script doesn't see?
#
warn1("Could not identify '$cmd'.");
}
else {
($quiet) || warn0("Unrecognized response from findType for '$cmd':\n$typ");
}
#warn0("Item definition found at '$path'");
# If we found it, go edit it unless it's binary.
#
if ($path && $edit) {
($quiet) || printf(" Last modified %3.1f days ago.\n", -M($path));
if (-B $path) {
print(" It's binary, so not editing.\n");
exit;
}
if (defined $ENV{EDITOR}) {
my $edcmd = "$ENV{EDITOR} " . lineSeekOption($ENV{EDITOR}, $foundLineNum) . " $path";
warn1(" Editing with command: $edcmd");
system($edcmd) && warn0(" Failed.\n");
}
else {
warn0(" Environment variable \$EDITOR is not set. But file is at:\n $path");
}
}