shlint 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Ross Duggan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ #shlint - shell linting utility.
2
+
3
+ Shlint uses locally available shells to test a shellscript for
4
+ portability issues. It also runs `checkbashisms` against the code.
5
+
6
+ Default shells tested are:
7
+ zsh ksh bash dash sh
8
+
9
+ ## Customize testing
10
+ Place a .shlintrc file in your homedir to override default shells.
11
+ This is expected to be shell syntax, specified as:
12
+
13
+ ```
14
+ shlint_shells="list installed shells here separated by spaces"
15
+ ```
16
+
17
+ ## OSX Users:
18
+ Use brew (http://mxcl.github.com/homebrew/) to install additional
19
+ shells if you're missing any.
20
+
21
+ ## Install
22
+ If you're a ruby user, can install using `gem install shlint`
23
+
24
+ Any other nix platform, just drop the contents of `lib` into your `$PATH`
data/bin/checkbashisms ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ spec = Gem::Specification.find_by_name("shlint")
4
+ gem_root = spec.gem_dir
5
+ gem_lib = gem_root + "/lib"
6
+
7
+ shell_output = ""
8
+ IO.popen("#{gem_lib}/checkbashisms #{ARGV.join(" ")}", 'r+') do |pipe|
9
+ pipe.close_write
10
+ shell_output = pipe.read
11
+ end
12
+
13
+ puts shell_output
14
+
data/bin/shlint ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ spec = Gem::Specification.find_by_name("shlint")
4
+ gem_root = spec.gem_dir
5
+ gem_lib = gem_root + "/lib"
6
+
7
+ shell_output = ""
8
+ IO.popen("#{gem_lib}/shlint #{ARGV.join(" ")}", 'r+') do |pipe|
9
+ pipe.close_write
10
+ shell_output = pipe.read
11
+ end
12
+
13
+ puts shell_output
data/lib/checkbashisms ADDED
@@ -0,0 +1,608 @@
1
+ #! /usr/bin/perl -w
2
+ #
3
+ # checkbashisms.perl
4
+ #
5
+ # Version: 2.0.0.2
6
+ # Date: 30th January 2011
7
+ #
8
+ # (C) Copyright 1998-2003 Richard Braakman, Josip Rodin and Julian Gilbey
9
+ # Additional programming by Mark Hobley
10
+ #
11
+ # This script is based on source code taken from the lintian project
12
+ #
13
+ # This program can be redistributed under the terms of version 2 of the
14
+ # GNU General Public Licence as published by the Free Software Foundation
15
+ #
16
+
17
+ use strict;
18
+ use Getopt::Long;
19
+
20
+ sub init_hashes;
21
+
22
+ (my $progname = $0) =~ s|.*/||;
23
+
24
+ my $usage = <<"EOF";
25
+ Usage: $progname [-n] [-f] [-x] script ...
26
+ or: $progname --help
27
+ or: $progname --version
28
+ This script performs basic checks for the presence of bashisms
29
+ in /bin/sh scripts.
30
+ EOF
31
+
32
+ my $version = <<"EOF";
33
+ This is $progname version 2.0.0.1
34
+ (C) Copyright 1998-2003 Richard Braakman, Josip Rodin and Julian Gilbey
35
+ Additional programming by Mark Hobley
36
+ EOF
37
+
38
+ my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
39
+ my ($opt_help, $opt_version);
40
+
41
+ ##
42
+ ## handle command-line options
43
+ ##
44
+ $opt_help = 1 if int(@ARGV) == 0;
45
+
46
+ GetOptions("help|h" => \$opt_help,
47
+ "version|v" => \$opt_version,
48
+ "newline|n" => \$opt_echo,
49
+ "force|f" => \$opt_force,
50
+ "extra|x" => \$opt_extra,
51
+ "posix|p" => \$opt_posix,
52
+ )
53
+ or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
54
+
55
+ if ($opt_help) { print $usage; exit 0; }
56
+ if ($opt_version) { print $version; exit 0; }
57
+
58
+ $opt_echo = 1 if $opt_posix;
59
+
60
+ my $status = 0;
61
+ my $makefile = 0;
62
+ my (%bashisms, %string_bashisms, %singlequote_bashisms);
63
+ my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)';
64
+
65
+ init_hashes;
66
+
67
+ foreach my $filename (@ARGV) {
68
+ my $check_lines_count = -1;
69
+
70
+ if (!$opt_force) {
71
+ $check_lines_count = script_is_evil_and_wrong($filename);
72
+ }
73
+
74
+ if ($check_lines_count == 0 or $check_lines_count == 1) {
75
+ warn "script $filename does not appear to be a /bin/sh script; skipping\n";
76
+ next;
77
+ }
78
+
79
+ if ($check_lines_count != -1) {
80
+ warn "script $filename appears to be a shell wrapper; only checking the first "
81
+ . "$check_lines_count lines\n";
82
+ }
83
+
84
+ unless (open C, '<', "$filename") {
85
+ warn "cannot open script $filename for reading: $!\n";
86
+ $status |= 2;
87
+ next;
88
+ }
89
+
90
+ my $cat_string = "";
91
+ my $cat_indented = 0;
92
+ my $quote_string = "";
93
+ my $last_continued = 0;
94
+ my $continued = 0;
95
+ my $found_rules = 0;
96
+ my $buffered_orig_line = "";
97
+ my $buffered_line = "";
98
+ while (<C>) {
99
+ next unless ($check_lines_count == -1 or $. <= $check_lines_count);
100
+
101
+ if ($. == 1) { # This should be an interpreter line
102
+ if (m,^\#!\s*(\S+),) {
103
+ my $interpreter = $1;
104
+
105
+ if ($interpreter =~ m,/make$,) {
106
+ init_hashes if !$makefile++;
107
+ $makefile = 1;
108
+ } else {
109
+ init_hashes if $makefile--;
110
+ $makefile = 0;
111
+ }
112
+ next if $opt_force;
113
+
114
+ if ($interpreter !~ m,/(sh|ash|hsh|posh)$,) {
115
+ warn "script $filename does not appear to be a /bin/sh script\n";
116
+ }
117
+ } else {
118
+ warn "script $filename does not appear to have a \#! interpreter line\n";
119
+ }
120
+ }
121
+
122
+ chomp;
123
+ my $orig_line = $_;
124
+
125
+ # We want to remove end-of-line comments, so need to skip
126
+ # comments that appear inside balanced pairs
127
+ # of single or double quotes
128
+
129
+ # Remove comments in the "quoted" part of a line that starts
130
+ # in a quoted block? The problem is that we have no idea
131
+ # whether the program interpreting the block treats the
132
+ # quote character as part of the comment or as a quote
133
+ # terminator. We err on the side of caution and assume it
134
+ # will be treated as part of the comment.
135
+ # s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
136
+
137
+ # skip comment lines
138
+ if (m,^\s*\#, && $quote_string eq '' && $buffered_line eq '' && $cat_string eq '') {
139
+ next;
140
+ }
141
+
142
+ # Remove quoted strings so we can more easily ignore comments
143
+ # inside them
144
+ s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
145
+ s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
146
+
147
+ # If the remaining string contains what looks like a comment,
148
+ # eat it. In either case, swap the unmodified script line
149
+ # back in for processing.
150
+ if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
151
+ $_ = $orig_line;
152
+ s/\Q$1\E//; # eat comments
153
+ } else {
154
+ $_ = $orig_line;
155
+ }
156
+
157
+ # Handle line continuation
158
+ if (!$makefile && $cat_string eq '' && m/\\$/) {
159
+ chop;
160
+ $buffered_line .= $_;
161
+ $buffered_orig_line .= $orig_line . "\n";
162
+ next;
163
+ }
164
+
165
+ if ($buffered_line ne '') {
166
+ $_ = $buffered_line . $_;
167
+ $orig_line = $buffered_orig_line . $orig_line;
168
+ $buffered_line ='';
169
+ $buffered_orig_line ='';
170
+ }
171
+
172
+ if ($makefile) {
173
+ $last_continued = $continued;
174
+ if (/[^\\]\\$/) {
175
+ $continued = 1;
176
+ } else {
177
+ $continued = 0;
178
+ }
179
+
180
+ # Don't match lines that look like a rule if we're in a
181
+ # continuation line before the start of the rules
182
+ if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
183
+ $found_rules = 1;
184
+ $_ = $1 if $1;
185
+ }
186
+
187
+ # Fixes for makefiles by Raphael Geissert
188
+ last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
189
+ # Remove "simple" target names
190
+ s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
191
+ s/^\t//;
192
+ s/(?<!\$)\$\((\w+)\)/\${$1}/g;
193
+ s/(\$){2}/$1/g;
194
+ s/^[\s\t]*[@-]{1,2}//;
195
+ }
196
+
197
+ if ($cat_string ne "" && (m/^\Q$cat_string\E$/ || ($cat_indented && m/^\t*\Q$cat_string\E$/))) {
198
+ $cat_string = "";
199
+ next;
200
+ }
201
+ my $within_another_shell = 0;
202
+ if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
203
+ $within_another_shell = 1;
204
+ }
205
+ # if cat_string is set, we are in a HERE document and need not
206
+ # check for things
207
+ if ($cat_string eq "" and !$within_another_shell) {
208
+ my $found = 0;
209
+ my $match = '';
210
+ my $explanation = '';
211
+ my $line = $_;
212
+
213
+ # Remove "" / '' as they clearly aren't quoted strings
214
+ # and not considering them makes the matching easier
215
+ $line =~ s/(^|[^\\])(\'\')+/$1/g;
216
+ $line =~ s/(^|[^\\])(\"\")+/$1/g;
217
+
218
+ if ($quote_string ne "") {
219
+ my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
220
+ # Inside a quoted block
221
+ if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
222
+ my $rest = $1;
223
+ my $templine = $line;
224
+
225
+ # Remove quoted strings delimited with $otherquote
226
+ $templine =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
227
+ # Remove quotes that are themselves quoted
228
+ # "a'b"
229
+ $templine =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
230
+ # "\""
231
+ $templine =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
232
+
233
+ # After all that, were there still any quotes left?
234
+ my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
235
+ next if $count == 0;
236
+
237
+ $count = () = $rest =~ /(^|[^\\])$quote_string/g;
238
+ if ($count % 2 == 0) {
239
+ # Quoted block ends on this line
240
+ # Ignore everything before the closing quote
241
+ $line = $rest || '';
242
+ $quote_string = "";
243
+ } else {
244
+ next;
245
+ }
246
+ } else {
247
+ # Still inside the quoted block, skip this line
248
+ next;
249
+ }
250
+ }
251
+
252
+ # Check even if we removed the end of a quoted block
253
+ # in the previous check, as a single line can end one
254
+ # block and begin another
255
+ if ($quote_string eq "") {
256
+ # Possible start of a quoted block
257
+ for my $quote ("\"", "\'") {
258
+ my $templine = $line;
259
+ my $otherquote = ($quote eq "\"" ? "\'" : "\"");
260
+
261
+ # Remove balanced quotes and their content
262
+ $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
263
+ $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
264
+
265
+ # Don't flag quotes that are themselves quoted
266
+ # "a'b"
267
+ $templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
268
+ # "\""
269
+ $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
270
+ # \' or \"
271
+ $templine =~ s/\\[\'\"]//g;
272
+ my $count = () = $templine =~ /(^|(?!\\))$quote/g;
273
+
274
+ # If there's an odd number of non-escaped
275
+ # quotes in the line it's almost certainly the
276
+ # start of a quoted block.
277
+ if ($count % 2 == 1) {
278
+ $quote_string = $quote;
279
+ $line =~ s/^(.*)$quote.*$/$1/;
280
+ last;
281
+ }
282
+ }
283
+ }
284
+
285
+ # since this test is ugly, I have to do it by itself
286
+ # detect source (.) trying to pass args to the command it runs
287
+ # The first expression weeds out '. "foo bar"'
288
+ if (not $found and
289
+ not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
290
+ and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
291
+ if ($2 =~ /^(\&|\||\d?>|<)/) {
292
+ # everything is ok
293
+ ;
294
+ } else {
295
+ $found = 1;
296
+ $match = $1;
297
+ $explanation = "sourced script with arguments";
298
+ output_explanation($filename, $orig_line, $explanation);
299
+ }
300
+ }
301
+
302
+ # Remove "quoted quotes". They're likely to be inside
303
+ # another pair of quotes; we're not interested in
304
+ # them for their own sake and removing them makes finding
305
+ # the limits of the outer pair far easier.
306
+ $line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
307
+ $line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
308
+
309
+ while (my ($re,$expl) = each %singlequote_bashisms) {
310
+ if ($line =~ m/($re)/) {
311
+ $found = 1;
312
+ $match = $1;
313
+ $explanation = $expl;
314
+ output_explanation($filename, $orig_line, $explanation);
315
+ }
316
+ }
317
+
318
+ my $re='(?<![\$\\\])\$\'[^\']+\'';
319
+ if ($line =~ m/(.*)($re)/){
320
+ my $count = () = $1 =~ /(^|[^\\])\'/g;
321
+ if( $count % 2 == 0 ) {
322
+ output_explanation($filename, $orig_line, q<$'...' should be "$(printf '...')">);
323
+ }
324
+ }
325
+
326
+ # $cat_line contains the version of the line we'll check
327
+ # for heredoc delimiters later. Initially, remove any
328
+ # spaces between << and the delimiter to make the following
329
+ # updates to $cat_line easier.
330
+ my $cat_line = $line;
331
+ $cat_line =~ s/(<\<-?)\s+/$1/g;
332
+
333
+ # Ignore anything inside single quotes; it could be an
334
+ # argument to grep or the like.
335
+ $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
336
+
337
+ # As above, with the exception that we don't remove the string
338
+ # if the quote is immediately preceeded by a < or a -, so we
339
+ # can match "foo <<-?'xyz'" as a heredoc later
340
+ # The check is a little more greedy than we'd like, but the
341
+ # heredoc test itself will weed out any false positives
342
+ $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
343
+
344
+ $re='(?<![\$\\\])\$\"[^\"]+\"';
345
+ if ($line =~ m/(.*)($re)/){
346
+ my $count = () = $1 =~ /(^|[^\\])\"/g;
347
+ if( $count % 2 == 0 ) {
348
+ output_explanation($filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
349
+ }
350
+ }
351
+
352
+ while (my ($re,$expl) = each %string_bashisms) {
353
+ if ($line =~ m/($re)/) {
354
+ $found = 1;
355
+ $match = $1;
356
+ $explanation = $expl;
357
+ output_explanation($filename, $orig_line, $explanation);
358
+ }
359
+ }
360
+
361
+ # We've checked for all the things we still want to notice in
362
+ # double-quoted strings, so now remove those strings as well.
363
+ $line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
364
+ $cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
365
+
366
+ while (my ($re,$expl) = each %bashisms) {
367
+ if ($line =~ m/($re)/) {
368
+ $found = 1;
369
+ $match = $1;
370
+ $explanation = $expl;
371
+ output_explanation($filename, $orig_line, $explanation);
372
+ }
373
+ }
374
+
375
+ # Only look for the beginning of a heredoc here, after we've
376
+ # stripped out quoted material, to avoid false positives.
377
+ if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
378
+ $cat_indented = ($1 && $1 eq '-')? 1 : 0;
379
+ $cat_string = $2;
380
+ $cat_string = $3 if not defined $cat_string;
381
+ }
382
+ }
383
+ }
384
+ warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
385
+ if ($cat_string ne '');
386
+ warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
387
+ if ($quote_string ne '');
388
+ warn "error: $filename: EOF reached while on line continuation.\n"
389
+ if ($buffered_line ne '');
390
+
391
+ close C;
392
+ }
393
+
394
+ exit $status;
395
+
396
+ sub output_explanation {
397
+ my ($filename, $line, $explanation) = @_;
398
+
399
+ warn "possible bashism in $filename line $. ($explanation):\n$line\n";
400
+ $status |= 1;
401
+ }
402
+
403
+ # Returns non-zero if the given file is not actually a shell script,
404
+ # just looks like one.
405
+ sub script_is_evil_and_wrong {
406
+ my ($filename) = @_;
407
+ my $ret = -1;
408
+ # lintian's version of this function aborts if the file
409
+ # can't be opened, but we simply return as the next
410
+ # test in the calling code handles reporting the error
411
+ # itself
412
+ open (IN, '<', $filename) or return $ret;
413
+ my $i = 0;
414
+ my $var = "0";
415
+ my $backgrounded = 0;
416
+ local $_;
417
+ while (<IN>) {
418
+ chomp;
419
+ next if /^#/o;
420
+ next if /^$/o;
421
+ last if (++$i > 55);
422
+ if (m~
423
+ # the exec should either be "eval"ed or a new statement
424
+ (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
425
+
426
+ # eat anything between the exec and $0
427
+ exec\s*.+\s*
428
+
429
+ # optionally quoted executable name (via $0)
430
+ .?\$$var.?\s*
431
+
432
+ # optional "end of options" indicator
433
+ (--\s*)?
434
+
435
+ # Match expressions of the form '${1+$@}', '${1:+"$@"',
436
+ # '"${1+$@', "$@", etc where the quotes (before the dollar
437
+ # sign(s)) are optional and the second (or only if the $1
438
+ # clause is omitted) parameter may be $@ or $*.
439
+ #
440
+ # Finally the whole subexpression may be omitted for scripts
441
+ # which do not pass on their parameters (i.e. after re-execing
442
+ # they take their parameters (and potentially data) from stdin
443
+ .?(\${1:?\+.?)?(\$(\@|\*))?~x) {
444
+ $ret = $. - 1;
445
+ last;
446
+ } elsif (/^\s*(\w+)=\$0;/) {
447
+ $var = $1;
448
+ } elsif (m~
449
+ # Match scripts which use "foo $0 $@ &\nexec true\n"
450
+ # Program name
451
+ \S+\s+
452
+
453
+ # As above
454
+ .?\$$var.?\s*
455
+ (--\s*)?
456
+ .?(\${1:?\+.?)?(\$(\@|\*))?.?\s*\&~x) {
457
+
458
+ $backgrounded = 1;
459
+ } elsif ($backgrounded and m~
460
+ # the exec should either be "eval"ed or a new statement
461
+ (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
462
+ exec\s+true(\s|\Z)~x) {
463
+
464
+ $ret = $. - 1;
465
+ last;
466
+ } elsif (m~\@DPATCH\@~) {
467
+ $ret = $. - 1;
468
+ last;
469
+ }
470
+
471
+ }
472
+ close IN;
473
+ return $ret;
474
+ }
475
+
476
+ sub init_hashes {
477
+ my $LEADIN = qr'(?:(^|[`&;(|{])\s*|(if|then|do|while|shell)\s+)';
478
+
479
+ %bashisms = (
480
+ qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
481
+ $LEADIN . qr'select\s+\w+' => q<'select' is not portable>,
482
+ qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
483
+ qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
484
+ qr'\s\|\&' => q<pipelining is not portable>,
485
+ qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
486
+ qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
487
+ qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
488
+ $LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
489
+ $LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
490
+ => q<read without variable>,
491
+ $LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>,
492
+ $LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>,
493
+ $LEADIN . qr'let\s' => q<let ...>,
494
+ qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>,
495
+ qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>,
496
+ qr'\&>' => q<should be \>word 2\>&1>,
497
+ qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
498
+ q<should be \>word 2\>&1>,
499
+ $LEADIN . qr'kill\s+-[^sl]\w*' => q<kill -[0-9] or -[A-Z]>,
500
+ $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]' => q<trap with signal numbers>,
501
+ $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*ERR' => q<trap ERR>,
502
+ qr'\[\[(?!:)' => q<alternative test command ([[ foo ]] should be [ foo ])>,
503
+ qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>,
504
+ $LEADIN . qr'alias\s' => q<alias>,
505
+ $LEADIN . qr'unalias\s' => q<unalias>,
506
+ $LEADIN . qr'builtin\s' => q<builtin>,
507
+ $LEADIN . qr'caller\s' => q<caller>,
508
+ $LEADIN . qr'complete\s' => q<complete>,
509
+ $LEADIN . qr'compgen\s' => q<compgen>,
510
+ $LEADIN . qr'declare\s' => q<declare>,
511
+ $LEADIN . qr'dirs(\s|\Z)' => q<dirs>,
512
+ $LEADIN . qr'disown\s' => q<disown>,
513
+ $LEADIN . qr'enable\s' => q<enable>,
514
+ $LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
515
+ $LEADIN . qr'export\s+.+=' => q<export foo=bar should be foo=bar; export foo>,
516
+ $LEADIN . qr'mapfile\s' => q<mapfile>,
517
+ $LEADIN . qr'readarray\s' => q<readarray>,
518
+ $LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
519
+ $LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
520
+ $LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>,
521
+ $LEADIN . qr'shopt(\s|\Z)' => q<shopt>,
522
+ $LEADIN . qr'suspend\s' => q<suspend>,
523
+ $LEADIN . qr'time\s' => q<time>,
524
+ $LEADIN . qr'type\s' => q<type>,
525
+ $LEADIN . qr'typeset\s' => q<typeset>,
526
+ $LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>,
527
+ $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
528
+ qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
529
+ => q<function names should only contain [a-z0-9_]>,
530
+ qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substituion>,
531
+ qr'(?:^|\s+)readonly\s+-[af]' => q<readonly -[af]>,
532
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
533
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>,
534
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>,
535
+ qr'\[\^[^]]+\]' => q<[^] should be [!]>,
536
+ $LEADIN . qr'printf\s+-v' => q<'printf -v var ...' should be var='$(printf ...)'>,
537
+ $LEADIN . qr'coproc\s' => q<coproc>,
538
+ qr';;?&' => q<;;& and ;& special case operators>,
539
+ );
540
+
541
+ %string_bashisms = (
542
+ qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
543
+ qr'\$\[\w+\]' => q<arithmetic not allowed>,
544
+ qr'\$\{\w+\:\d+(?::\d+)?\}' => q<${foo:3[:1]}>,
545
+ qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
546
+ qr'\$\{!\w+\}' => q<${!name}>,
547
+ qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
548
+ qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
549
+ qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
550
+ qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
551
+ qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
552
+ qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>,
553
+ qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">,
554
+ qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">,
555
+ qr'\$\{?LINENO\}?\b' => q<$LINENO>,
556
+ qr'\$\{?SECONDS\}?\b' => q<$SECONDS>,
557
+ qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>,
558
+ qr'\$\{?KSH_[A-Z]+\}?\b' => q<$KSH_SOMETHING>,
559
+ qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
560
+ qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
561
+ qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
562
+ qr'<<<' => q<\<\<\< here string>,
563
+ qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,
564
+ qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' => q<'$((++n))' should be '$((n=n+1))'>,
565
+ qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' => q<'$((n--))' should be '$n; $((n=n-1))'>,
566
+ qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' => q<'$((--n))' should be '$((n=n-1))'>,
567
+ qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>,
568
+ $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
569
+ $LEADIN . qr'printf\s["\'][^"\']+?%[qb].+?["\']' => q<printf %q|%b>,
570
+ );
571
+
572
+ %singlequote_bashisms = (
573
+ $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => q<unsafe echo with backslash>,
574
+ $LEADIN . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
575
+ q<should be '.', not 'source'>,
576
+ );
577
+
578
+ if ($opt_echo) {
579
+ $bashisms{$LEADIN . qr'echo\s+-[A-Za-z]*n'} = q<echo -n>;
580
+ }
581
+ if ($opt_posix) {
582
+ $bashisms{$LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)'} = q<local foo>;
583
+ $bashisms{$LEADIN . qr'local\s+\w+='} = q<local foo=bar>;
584
+ $bashisms{$LEADIN . qr'local\s+\w+\s+\w+'} = q<local x y>;
585
+ $bashisms{$LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s'} = q<test -a/-o>;
586
+ }
587
+
588
+ if ($makefile) {
589
+ $string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} =
590
+ q<'$(\< foo)' should be '$(cat foo)'>;
591
+ } else {
592
+ $bashisms{$LEADIN . qr'\w+\+='} = q<should be VAR="${VAR}foo">;
593
+ $string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} = q<'$(\< foo)' should be '$(cat foo)'>;
594
+ }
595
+
596
+ if ($opt_extra) {
597
+ $string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
598
+ $string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
599
+ $string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
600
+ $string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
601
+ $string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
602
+ $string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
603
+ $string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
604
+ $string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
605
+ $string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
606
+ $string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;
607
+ }
608
+ }
data/lib/shlint ADDED
@@ -0,0 +1,72 @@
1
+ #!/bin/sh
2
+
3
+ # Prevent variables from being unset.
4
+ # This helps avoid things like the debug parameter being repurposed.
5
+ set -o nounset
6
+ # Force the entire script to exit on error.
7
+ # This prevents the script from ploughing on ahead through errors.
8
+ set -o errexit
9
+
10
+ # List of shells to test.
11
+ # Add or remove from this list as required.
12
+ shlint_shells="zsh ksh bash dash sh"
13
+
14
+ shlint_debug=0
15
+ shlint_commands=""
16
+
17
+ if [ $# -eq 0 ]; then
18
+ cat <<-DOC
19
+ shlint - shell linting utility.
20
+
21
+ Usage: shlint [OPTIONS] <FILE...>
22
+
23
+ Drop me into your PATH for great justice!
24
+
25
+ Default shells tested are:
26
+ $shlint_shells
27
+
28
+ Place a .shlintrc file in your homedir to override default shells.
29
+ This is expected to be shell syntax, specified as:
30
+ shlint_shells="list installed shells here separated by spaces"
31
+
32
+ OSX Users:
33
+ Use brew (http://mxcl.github.com/homebrew/) to install additional
34
+ shells if you're missing any.
35
+
36
+ Options:
37
+ --debug Prints additional output.
38
+ DOC
39
+ exit 0
40
+ fi
41
+
42
+ for shlint_cmd in $@
43
+ do
44
+ if [ "--debug" = "$shlint_cmd" ]; then
45
+ shlint_debug=1
46
+ else
47
+ shlint_commands="$shlint_commands $shlint_cmd"
48
+ fi
49
+ done
50
+
51
+ # Override default shell tests
52
+ # Regular shell syntax here.
53
+ if [ -f ~/.shlintrc ]; then
54
+ . ~/.shlintrc
55
+ fi
56
+
57
+ for shlint_shell in $shlint_shells
58
+ do
59
+ if [ $shlint_debug = 1 ]; then
60
+ echo "Using $shlint_shell..."
61
+ fi
62
+ for shlint_cmd in $shlint_commands
63
+ do
64
+ if [ $shlint_debug = 1 ]; then
65
+ echo "Testing $shlint_cmd"
66
+ $shlint_shell -n $shlint_cmd
67
+ fi
68
+ done
69
+ done
70
+
71
+ # Check for bashisms
72
+ checkbashisms $shlint_commands
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shlint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ross Duggan
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-24 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Checks the syntax of your shellscript against known and available shells.
15
+ email:
16
+ - rduggan@engineyard.com
17
+ executables:
18
+ - shlint
19
+ - checkbashisms
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - bin/checkbashisms
24
+ - bin/shlint
25
+ - lib/checkbashisms
26
+ - lib/shlint
27
+ - LICENSE
28
+ - README.md
29
+ homepage: http://github.com/duggan/shlint
30
+ licenses: []
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.6
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 1.8.24
50
+ signing_key:
51
+ specification_version: 3
52
+ summary: A linting tool for shell.
53
+ test_files: []