shlint 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (2) hide show
  1. data/lib/checkbashisms +129 -40
  2. metadata +3 -3
data/lib/checkbashisms CHANGED
@@ -6,7 +6,7 @@
6
6
  # Copyright (C) 2002 Josip Rodin
7
7
  # This version is
8
8
  # Copyright (C) 2003 Julian Gilbey
9
- #
9
+ #
10
10
  # This program is free software; you can redistribute it and/or modify
11
11
  # it under the terms of the GNU General Public License as published by
12
12
  # the Free Software Foundation; either version 2 of the License, or
@@ -21,7 +21,8 @@
21
21
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
22
 
23
23
  use strict;
24
- use Getopt::Long;
24
+ use Getopt::Long qw(:config gnu_getopt);
25
+ use File::Temp qw/tempfile/;
25
26
 
26
27
  sub init_hashes;
27
28
 
@@ -47,6 +48,12 @@ EOF
47
48
 
48
49
  my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
49
50
  my ($opt_help, $opt_version);
51
+ my @filenames;
52
+
53
+ # Detect if STDIN is a pipe
54
+ if (scalar(@ARGV) == 0 && (-p STDIN or -f STDIN)) {
55
+ push(@ARGV, '-');
56
+ }
50
57
 
51
58
  ##
52
59
  ## handle command-line options
@@ -77,22 +84,34 @@ init_hashes;
77
84
  foreach my $filename (@ARGV) {
78
85
  my $check_lines_count = -1;
79
86
 
87
+ my $display_filename = $filename;
88
+
89
+ if ($filename eq '-') {
90
+ my $tmp_fh;
91
+ ($tmp_fh, $filename) = tempfile("chkbashisms_tmp.XXXX", TMPDIR => 1, UNLINK => 1);
92
+ while (my $line = <STDIN>) {
93
+ print $tmp_fh $line;
94
+ }
95
+ close($tmp_fh);
96
+ $display_filename = "(stdin)";
97
+ }
98
+
80
99
  if (!$opt_force) {
81
100
  $check_lines_count = script_is_evil_and_wrong($filename);
82
101
  }
83
102
 
84
103
  if ($check_lines_count == 0 or $check_lines_count == 1) {
85
- warn "script $filename does not appear to be a /bin/sh script; skipping\n";
104
+ warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
86
105
  next;
87
106
  }
88
107
 
89
108
  if ($check_lines_count != -1) {
90
- warn "script $filename appears to be a shell wrapper; only checking the first "
109
+ warn "script $display_filename appears to be a shell wrapper; only checking the first "
91
110
  . "$check_lines_count lines\n";
92
111
  }
93
112
 
94
113
  unless (open C, '<', $filename) {
95
- warn "cannot open script $filename for reading: $!\n";
114
+ warn "cannot open script $display_filename for reading: $!\n";
96
115
  $status |= 2;
97
116
  next;
98
117
  }
@@ -105,6 +124,7 @@ foreach my $filename (@ARGV) {
105
124
  my $found_rules = 0;
106
125
  my $buffered_orig_line = "";
107
126
  my $buffered_line = "";
127
+ my %start_lines;
108
128
 
109
129
  while (<C>) {
110
130
  next unless ($check_lines_count == -1 or $. <= $check_lines_count);
@@ -123,18 +143,18 @@ foreach my $filename (@ARGV) {
123
143
  next if $opt_force;
124
144
 
125
145
  if ($interpreter =~ m,/bash$,) {
126
- warn "script $filename is already a bash script; skipping\n";
146
+ warn "script $display_filename is already a bash script; skipping\n";
127
147
  $status |= 2;
128
148
  last; # end this file
129
149
  }
130
150
  elsif ($interpreter !~ m,/(sh|posh)$,) {
131
151
  ### ksh/zsh?
132
- warn "script $filename does not appear to be a /bin/sh script; skipping\n";
152
+ warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
133
153
  $status |= 2;
134
154
  last;
135
155
  }
136
156
  } else {
137
- warn "script $filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
157
+ warn "script $display_filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
138
158
  }
139
159
  }
140
160
 
@@ -163,6 +183,12 @@ foreach my $filename (@ARGV) {
163
183
  s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
164
184
  s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
165
185
 
186
+ # If inside a quoted string, remove everything before the quote
187
+ s/^.+?\'//
188
+ if ($quote_string eq "'");
189
+ s/^.+?[^\\]\"//
190
+ if ($quote_string eq '"');
191
+
166
192
  # If the remaining string contains what looks like a comment,
167
193
  # eat it. In either case, swap the unmodified script line
168
194
  # back in for processing.
@@ -201,7 +227,7 @@ foreach my $filename (@ARGV) {
201
227
  if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
202
228
  $found_rules = 1;
203
229
  $_ = $1 if $1;
204
- }
230
+ }
205
231
 
206
232
  last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
207
233
 
@@ -278,8 +304,26 @@ foreach my $filename (@ARGV) {
278
304
  my $otherquote = ($quote eq "\"" ? "\'" : "\"");
279
305
 
280
306
  # Remove balanced quotes and their content
281
- $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
282
- $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
307
+ while (1) {
308
+ my ($length_single, $length_double) = (0, 0);
309
+
310
+ # Determine which one would match first:
311
+ if ($templine =~ m/(^.+?(?:^|[^\\\"](?:\\\\)*)\')[^\']*\'/) {
312
+ $length_single = length($1);
313
+ }
314
+ if ($templine =~ m/(^.*?(?:^|[^\\\'](?:\\\\)*)\")(?:\\.|[^\\\"])+\"/) {
315
+ $length_double = length($1);
316
+ }
317
+
318
+ # Now simplify accordingly (shorter is preferred):
319
+ if ($length_single != 0 && ($length_single < $length_double || $length_double == 0)) {
320
+ $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/;
321
+ } elsif ($length_double != 0) {
322
+ $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/;
323
+ } else {
324
+ last;
325
+ }
326
+ }
283
327
 
284
328
  # Don't flag quotes that are themselves quoted
285
329
  # "a'b"
@@ -295,6 +339,7 @@ foreach my $filename (@ARGV) {
295
339
  # start of a quoted block.
296
340
  if ($count % 2 == 1) {
297
341
  $quote_string = $quote;
342
+ $start_lines{'quote_string'} = $.;
298
343
  $line =~ s/^(.*)$quote.*$/$1/;
299
344
  last;
300
345
  }
@@ -305,8 +350,8 @@ foreach my $filename (@ARGV) {
305
350
  # detect source (.) trying to pass args to the command it runs
306
351
  # The first expression weeds out '. "foo bar"'
307
352
  if (not $found and
308
- not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
309
- and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
353
+ not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/o
354
+ and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/o) {
310
355
  if ($2 =~ /^(\&|\||\d?>|<)/) {
311
356
  # everything is ok
312
357
  ;
@@ -314,7 +359,7 @@ foreach my $filename (@ARGV) {
314
359
  $found = 1;
315
360
  $match = $1;
316
361
  $explanation = "sourced script with arguments";
317
- output_explanation($filename, $orig_line, $explanation);
362
+ output_explanation($display_filename, $orig_line, $explanation);
318
363
  }
319
364
  }
320
365
 
@@ -330,50 +375,52 @@ foreach my $filename (@ARGV) {
330
375
  $found = 1;
331
376
  $match = $1;
332
377
  $explanation = $expl;
333
- output_explanation($filename, $orig_line, $explanation);
378
+ output_explanation($display_filename, $orig_line, $explanation);
334
379
  }
335
380
  }
336
381
 
337
382
  my $re='(?<![\$\\\])\$\'[^\']+\'';
338
- if ($line =~ m/(.*)($re)/){
383
+ if ($line =~ m/(.*)($re)/o){
339
384
  my $count = () = $1 =~ /(^|[^\\])\'/g;
340
385
  if( $count % 2 == 0 ) {
341
- output_explanation($filename, $orig_line, q<$'...' should be "$(printf '...')">);
386
+ output_explanation($display_filename, $orig_line, q<$'...' should be "$(printf '...')">);
342
387
  }
343
- }
388
+ }
344
389
 
345
390
  # $cat_line contains the version of the line we'll check
346
391
  # for heredoc delimiters later. Initially, remove any
347
392
  # spaces between << and the delimiter to make the following
348
- # updates to $cat_line easier.
393
+ # updates to $cat_line easier. However, don't remove the
394
+ # spaces if the delimiter starts with a -, as that changes
395
+ # how the delimiter is searched.
349
396
  my $cat_line = $line;
350
- $cat_line =~ s/(<\<-?)\s+/$1/g;
397
+ $cat_line =~ s/(<\<-?)\s+(?!-)/$1/g;
351
398
 
352
399
  # Ignore anything inside single quotes; it could be an
353
400
  # argument to grep or the like.
354
401
  $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
355
402
 
356
403
  # As above, with the exception that we don't remove the string
357
- # if the quote is immediately preceeded by a < or a -, so we
404
+ # if the quote is immediately preceded by a < or a -, so we
358
405
  # can match "foo <<-?'xyz'" as a heredoc later
359
406
  # The check is a little more greedy than we'd like, but the
360
407
  # heredoc test itself will weed out any false positives
361
408
  $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
362
409
 
363
410
  $re='(?<![\$\\\])\$\"[^\"]+\"';
364
- if ($line =~ m/(.*)($re)/){
411
+ if ($line =~ m/(.*)($re)/o){
365
412
  my $count = () = $1 =~ /(^|[^\\])\"/g;
366
413
  if( $count % 2 == 0 ) {
367
- output_explanation($filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
414
+ output_explanation($display_filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
368
415
  }
369
- }
416
+ }
370
417
 
371
418
  while (my ($re,$expl) = each %string_bashisms) {
372
419
  if ($line =~ m/($re)/) {
373
420
  $found = 1;
374
421
  $match = $1;
375
422
  $explanation = $expl;
376
- output_explanation($filename, $orig_line, $explanation);
423
+ output_explanation($display_filename, $orig_line, $explanation);
377
424
  }
378
425
  }
379
426
 
@@ -386,25 +433,46 @@ foreach my $filename (@ARGV) {
386
433
  $found = 1;
387
434
  $match = $1;
388
435
  $explanation = $expl;
389
- output_explanation($filename, $orig_line, $explanation);
436
+ output_explanation($display_filename, $orig_line, $explanation);
390
437
  }
391
438
  }
439
+ # This check requires the value to be compared, which could
440
+ # be done in the regex itself but requires "use re 'eval'".
441
+ # So it's better done in its own
442
+ if ($line =~ m/$LEADIN((?:exit|return)\s+(\d{3,}))/o && $2 > 255) {
443
+ $explanation = 'exit|return status code greater than 255';
444
+ output_explanation($display_filename, $orig_line, $explanation);
445
+ }
392
446
 
393
447
  # Only look for the beginning of a heredoc here, after we've
394
448
  # stripped out quoted material, to avoid false positives.
395
- if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
449
+ if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:(?!<|'|")((?:[^\s;>|]+(?:(?<=\\)[\s;>|])?)+)|[\'\"](.*?)[\'\"])/) {
396
450
  $cat_indented = ($1 && $1 eq '-')? 1 : 0;
397
- $cat_string = $2;
398
- $cat_string = $3 if not defined $cat_string;
451
+ my $quoted = defined($3);
452
+ $cat_string = $quoted? $3 : $2;
453
+ unless ($quoted) {
454
+ # Now strip backslashes. Keep the position of the
455
+ # last match in a variable, as s/// resets it back
456
+ # to undef, but we don't want that.
457
+ my $pos = 0;
458
+ pos($cat_string) = $pos;
459
+ while ($cat_string =~ s/\G(.*?)\\/$1/) {
460
+ # postition += length of match + the character
461
+ # that followed the backslash:
462
+ $pos += length($1)+1;
463
+ pos($cat_string) = $pos;
464
+ }
465
+ }
466
+ $start_lines{'cat_string'} = $.;
399
467
  }
400
468
  }
401
469
  }
402
470
 
403
- warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
471
+ warn "error: $display_filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>, opened in line $start_lines{'cat_string'}\n"
404
472
  if ($cat_string ne '');
405
- warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
473
+ warn "error: $display_filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>, opened in line $start_lines{'quote_string'}\n"
406
474
  if ($quote_string ne '');
407
- warn "error: $filename: EOF reached while on line continuation.\n"
475
+ warn "error: $display_filename: EOF reached while on line continuation.\n"
408
476
  if ($buffered_line ne '');
409
477
 
410
478
  close C;
@@ -454,8 +522,8 @@ sub script_is_evil_and_wrong {
454
522
  # Match expressions of the form '${1+$@}', '${1:+"$@"',
455
523
  # '"${1+$@', "$@", etc where the quotes (before the dollar
456
524
  # sign(s)) are optional and the second (or only if the $1
457
- # clause is omitted) parameter may be $@ or $*.
458
- #
525
+ # clause is omitted) parameter may be $@ or $*.
526
+ #
459
527
  # Finally the whole subexpression may be omitted for scripts
460
528
  # which do not pass on their parameters (i.e. after re-execing
461
529
  # they take their parameters (and potentially data) from stdin
@@ -495,13 +563,14 @@ sub script_is_evil_and_wrong {
495
563
  sub init_hashes {
496
564
 
497
565
  %bashisms = (
498
- qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
566
+ qr'(?:^|\s+)function [^<>\(\)\[\]\{\};|\s]+(\s|\(|\Z)' => q<'function' is useless>,
499
567
  $LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
500
568
  qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
501
569
  qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
502
570
  qr'\s\|\&' => q<pipelining is not POSIX>,
503
571
  qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
504
- qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
572
+ qr'\{\d+\.\.\d+(?:\.\.\d+)?\}' => q<brace expansion, {a..b[..c]}should be $(seq a [c] b)>,
573
+ qr'(?i)\{[a-z]\.\.[a-z](?:\.\.\d+)?\}' => q<brace expansion>,
505
574
  qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
506
575
  $LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
507
576
  $LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
@@ -536,7 +605,10 @@ sub init_hashes {
536
605
  $LEADIN . qr'alias\s+-p' => q<alias -p>,
537
606
  $LEADIN . qr'unalias\s+-a' => q<unalias -a>,
538
607
  $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
539
- qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
608
+ # function '=' is special-cased due to bash arrays (think of "foo=()")
609
+ qr'(?:^|\s)\s*=\s*\(\s*\)\s*([\{|\(]|\Z)'
610
+ => q<function names should only contain [a-z0-9_]>,
611
+ qr'(?:^|\s)(?<func>function\s)?\s*(?:[^<>\(\)\[\]\{\};|\s]*[^<>\(\)\[\]\{\};|\s\w][^<>\(\)\[\]\{\};|\s]*)(?(<func>)(?=)|(?<!=))\s*(?(<func>)(?:\(\s*\))?|\(\s*\))\s*([\{|\(]|\Z)'
540
612
  => q<function names should only contain [a-z0-9_]>,
541
613
  $LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
542
614
  $LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
@@ -552,15 +624,25 @@ sub init_hashes {
552
624
  $LEADIN . qr'jobs\s' => q<jobs>,
553
625
  # $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
554
626
  $LEADIN . qr'command\s+-[^p]\s' => q<'command' with option other than -p>,
627
+ $LEADIN . qr'setvar\s' => q<setvar 'foo' 'bar' should be eval 'foo="'"$bar"'"'>,
628
+ $LEADIN . qr'trap\s+["\']?.*["\']?\s+.*(?:ERR|DEBUG|RETURN)' => q<trap with ERR|DEBUG|RETURN>,
629
+ $LEADIN . qr'(?:exit|return)\s+-\d' => q<exit|return with negative status code>,
630
+ $LEADIN . qr'(?:exit|return)\s+--' => q<'exit --' should be 'exit' (idem for return)>,
631
+ $LEADIN . qr'sleep\s+(?:-|\d+(?:[.a-z]|\s+\d))' => q<sleep only takes one integer>,
632
+ $LEADIN . qr'hash(\s|\Z)' => q<hash>,
633
+ qr'(?:[:=\s])~(?:[+-]|[+-]?\d+)(?:[/\s]|\Z)' => q<non-standard tilde expansion>,
555
634
  );
556
635
 
557
636
  %string_bashisms = (
558
637
  qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
559
- qr'\$\{\w+\:\d+(?::\d+)?\}' => q<${foo:3[:1]}>,
638
+ qr'\$\{(?:\w+|@|\*)\:(?:\d+|\$\{?\w+\}?)+(?::(?:\d+|\$\{?\w+\}?)+)?\}' => q<${foo:3[:1]}>,
560
639
  qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
561
640
  qr'\$\{!\w+\}' => q<${!name}>,
562
- qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
563
- qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
641
+ qr'\$\{(?:\w+|@|\*)([,^]{1,2}.*?)\}' => q<${parm,[,][pat]} or ${parm^[^][pat]}>,
642
+ qr'\$\{[@*]([#%]{1,2}.*?)\}' => q<${[@|*]#[#]pat} or ${[@|*]%[%]pat}>,
643
+ qr'\$\{#[@*]\}' => q<${#@} or ${#*}>,
644
+ qr'\$\{(?:\w+|@|\*)(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
645
+ qr'\$\{\#?\w+\[.+\](?:[/,:#%^].+?)?\}' => q<bash arrays, ${name[0|*|@]}>,
564
646
  qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
565
647
  qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
566
648
  qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
@@ -572,6 +654,13 @@ sub init_hashes {
572
654
  qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
573
655
  qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
574
656
  qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
657
+ qr'\$\{?FUNCNAME\}?\b' => q<$FUNCNAME>,
658
+ qr'\$\{?TMOUT\}?\b' => q<$TMOUT>,
659
+ qr'(?:^|\s+)TMOUT=' => q<TMOUT=>,
660
+ qr'\$\{?TIMEFORMAT\}?\b' => q<$TIMEFORMAT>,
661
+ qr'(?:^|\s+)TIMEFORMAT=' => q<TIMEFORMAT=>,
662
+ qr'\$\{?_\}?\b' => q<$_>,
663
+ qr'(?:^|\s+)GLOBIGNORE=' => q<GLOBIGNORE=>,
575
664
  qr'<<<' => q<\<\<\< here string>,
576
665
  $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
577
666
  qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shlint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-25 00:00:00.000000000 Z
12
+ date: 2012-11-16 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Checks the syntax of your shellscript against known and available shells.
15
15
  email:
@@ -46,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
46
  version: 1.3.6
47
47
  requirements: []
48
48
  rubyforge_project:
49
- rubygems_version: 1.8.24
49
+ rubygems_version: 1.8.23
50
50
  signing_key:
51
51
  specification_version: 3
52
52
  summary: A linting tool for shell.