shlint 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.