vagrant-salt 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/README.rst +131 -170
  2. data/example/complete/Vagrantfile +67 -0
  3. data/example/complete/salt/custom-bootstrap-salt.sh +2425 -0
  4. data/example/complete/salt/key/master.pem +30 -0
  5. data/example/complete/salt/key/master.pub +14 -0
  6. data/example/complete/salt/key/minion.pem +30 -0
  7. data/example/complete/salt/key/minion.pub +14 -0
  8. data/example/complete/salt/master +459 -0
  9. data/example/{salt/minion.conf → complete/salt/minion} +1 -2
  10. data/example/{salt → complete/salt}/roots/pillar/top.sls +0 -0
  11. data/example/complete/salt/roots/salt/nginx.sls +5 -0
  12. data/example/complete/salt/roots/salt/top.sls +3 -0
  13. data/example/masterless/Vagrantfile +18 -0
  14. data/example/masterless/salt/minion +219 -0
  15. data/example/{salt/roots/salt → masterless/salt/roots/pillar}/top.sls +0 -0
  16. data/example/masterless/salt/roots/salt/nginx.sls +5 -0
  17. data/example/masterless/salt/roots/salt/top.sls +3 -0
  18. data/lib/vagrant-salt.rb +16 -3
  19. data/lib/vagrant-salt/config.rb +103 -0
  20. data/lib/vagrant-salt/errors.rb +11 -0
  21. data/lib/vagrant-salt/plugin.rb +31 -0
  22. data/lib/vagrant-salt/provisioner.rb +211 -104
  23. data/lib/vagrant-salt/version.rb +5 -0
  24. data/scripts/.travis.yml +16 -0
  25. data/scripts/ChangeLog +39 -0
  26. data/scripts/LICENSE +16 -0
  27. data/scripts/README.rst +124 -35
  28. data/scripts/bootstrap-salt-minion.sh +1815 -381
  29. data/scripts/bootstrap-salt.sh +2425 -0
  30. data/scripts/salt-bootstrap.sh +2425 -0
  31. data/scripts/tests/README.rst +38 -0
  32. data/scripts/tests/bootstrap/__init__.py +11 -0
  33. data/{example/salt/key/KEYPAIR_GOES_HERE → scripts/tests/bootstrap/ext/__init__.py} +0 -0
  34. data/scripts/tests/bootstrap/ext/console.py +100 -0
  35. data/scripts/tests/bootstrap/ext/os_data.py +199 -0
  36. data/scripts/tests/bootstrap/test_install.py +586 -0
  37. data/scripts/tests/bootstrap/test_lint.py +27 -0
  38. data/scripts/tests/bootstrap/test_usage.py +28 -0
  39. data/scripts/tests/bootstrap/unittesting.py +216 -0
  40. data/scripts/tests/ext/checkbashisms +640 -0
  41. data/scripts/tests/install-testsuite-deps.py +99 -0
  42. data/scripts/tests/runtests.py +207 -0
  43. data/templates/locales/en.yml +14 -0
  44. data/vagrant-salt.gemspec +2 -2
  45. metadata +43 -10
  46. data/example/Vagrantfile +0 -26
  47. data/lib/vagrant_init.rb +0 -1
@@ -0,0 +1,27 @@
1
+ # -*- coding: utf-8 -*-
2
+ '''
3
+ bootstrap.test_lint
4
+ ~~~~~~~~~~~~~~~~~~~
5
+
6
+ :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
7
+ :copyright: © 2013 by the UfSoft.org Team, see AUTHORS for more details.
8
+ :license: BSD, see LICENSE for more details.
9
+ '''
10
+ from bootstrap.unittesting import *
11
+
12
+
13
+ class LintTestCase(BootstrapTestCase):
14
+ def test_bashisms(self):
15
+ '''
16
+ Lint check the bootstrap script for any possible bash'isms.
17
+ '''
18
+ if not os.path.exists('/usr/bin/perl'):
19
+ self.skipTest('\'/usr/bin/perl\' was not found on this system')
20
+ self.assert_script_result(
21
+ 'Some bashisms were found',
22
+ 0,
23
+ self.run_script(
24
+ script=os.path.join(EXT_DIR, 'checkbashisms'),
25
+ args=('-pxfn', BOOTSTRAP_SCRIPT_PATH)
26
+ )
27
+ )
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ bootstrap.test_usage
4
+ ~~~~~~~~~~~~~~~~~~~~
5
+
6
+ :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
7
+ :copyright: © 2013 by the UfSoft.org Team, see AUTHORS for more details.
8
+ :license: BSD, see LICENSE for more details.
9
+ """
10
+ from bootstrap.unittesting import *
11
+
12
+
13
+ class UsageTestCase(BootstrapTestCase):
14
+ def test_no_daemon_install_shows_warning(self):
15
+ '''
16
+ Passing '-N'(no minion) without passing '-M'(install master) or
17
+ '-S'(install syndic) shows a warning.
18
+ '''
19
+ rc, out, err = self.run_script(
20
+ args=('-N', '-n'),
21
+ )
22
+
23
+ self.assert_script_result(
24
+ 'Not installing any daemons nor configuring did not throw any '
25
+ 'warning',
26
+ 0, (rc, out, err)
27
+ )
28
+ self.assertIn(' * WARN: Nothing to install or configure', out)
@@ -0,0 +1,216 @@
1
+ # -*- coding: utf-8 -*-
2
+ '''
3
+ bootstrap.unittesting
4
+ ~~~~~~~~~~~~~~~~~~~~~
5
+
6
+ Unit testing related classes, helpers.
7
+
8
+ :codeauthor: :email:`Pedro Algarvio (pedro@algarvio.me)`
9
+ :copyright: © 2013 by the SaltStack Team, see AUTHORS for more details.
10
+ :license: Apache 2.0, see LICENSE for more details.
11
+ '''
12
+
13
+ # Import python libs
14
+ import os
15
+ import sys
16
+ import fcntl
17
+ import signal
18
+ import tempfile
19
+ import subprocess
20
+ from datetime import datetime, timedelta
21
+
22
+ # Import salt bootstrap libs
23
+ from bootstrap.ext.os_data import GRAINS
24
+
25
+
26
+ # support python < 2.7 via unittest2
27
+ if sys.version_info < (2, 7):
28
+ try:
29
+ from unittest2 import (
30
+ TestLoader,
31
+ TextTestRunner,
32
+ TestCase,
33
+ expectedFailure,
34
+ TestSuite,
35
+ skipIf,
36
+ )
37
+ except ImportError:
38
+ raise SystemExit('You need to install unittest2 to run the salt tests')
39
+ else:
40
+ from unittest import (
41
+ TestLoader,
42
+ TextTestRunner,
43
+ TestCase,
44
+ expectedFailure,
45
+ TestSuite,
46
+ skipIf,
47
+ )
48
+
49
+
50
+ TEST_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
51
+ EXT_DIR = os.path.join(TEST_DIR, 'ext')
52
+ PARENT_DIR = os.path.dirname(TEST_DIR)
53
+ BOOTSTRAP_SCRIPT_PATH = os.path.join(PARENT_DIR, 'bootstrap-salt.sh')
54
+
55
+
56
+ class NonBlockingPopen(subprocess.Popen):
57
+
58
+ def __init__(self, *args, **kwargs):
59
+ self.stream_stds = kwargs.pop('stream_stds', False)
60
+ super(NonBlockingPopen, self).__init__(*args, **kwargs)
61
+ if self.stdout is not None and self.stream_stds:
62
+ fod = self.stdout.fileno()
63
+ fol = fcntl.fcntl(fod, fcntl.F_GETFL)
64
+ fcntl.fcntl(fod, fcntl.F_SETFL, fol | os.O_NONBLOCK)
65
+ self.obuff = ''
66
+
67
+ if self.stderr is not None and self.stream_stds:
68
+ fed = self.stderr.fileno()
69
+ fel = fcntl.fcntl(fed, fcntl.F_GETFL)
70
+ fcntl.fcntl(fed, fcntl.F_SETFL, fel | os.O_NONBLOCK)
71
+ self.ebuff = ''
72
+
73
+ def poll(self):
74
+ poll = super(NonBlockingPopen, self).poll()
75
+
76
+ if self.stdout is not None and self.stream_stds:
77
+ try:
78
+ obuff = self.stdout.read()
79
+ self.obuff += obuff
80
+ sys.stdout.write(obuff)
81
+ except IOError, err:
82
+ if err.errno not in (11, 35):
83
+ # We only handle Resource not ready properly, any other
84
+ # raise the exception
85
+ raise
86
+ if self.stderr is not None and self.stream_stds:
87
+ try:
88
+ ebuff = self.stderr.read()
89
+ self.ebuff += ebuff
90
+ sys.stderr.write(ebuff)
91
+ except IOError, err:
92
+ if err.errno not in (11, 35):
93
+ # We only handle Resource not ready properly, any other
94
+ # raise the exception
95
+ raise
96
+
97
+ if poll is None:
98
+ # Not done yet
99
+ return poll
100
+
101
+ if not self.stream_stds:
102
+ # Allow the same attribute access even though not streaming to stds
103
+ try:
104
+ self.obuff = self.stdout.read()
105
+ except IOError, err:
106
+ if err.errno not in (11, 35):
107
+ # We only handle Resource not ready properly, any other
108
+ # raise the exception
109
+ raise
110
+ try:
111
+ self.ebuff = self.stderr.read()
112
+ except IOError, err:
113
+ if err.errno not in (11, 35):
114
+ # We only handle Resource not ready properly, any other
115
+ # raise the exception
116
+ raise
117
+ return poll
118
+
119
+
120
+ class BootstrapTestCase(TestCase):
121
+ def run_script(self,
122
+ script=BOOTSTRAP_SCRIPT_PATH,
123
+ args=(),
124
+ cwd=PARENT_DIR,
125
+ timeout=None,
126
+ executable='/bin/sh',
127
+ stream_stds=False):
128
+
129
+ cmd = [script] + list(args)
130
+
131
+ outbuff = errbuff = ''
132
+
133
+ popen_kwargs = {
134
+ 'cwd': cwd,
135
+ 'shell': True,
136
+ 'stderr': subprocess.PIPE,
137
+ 'stdout': subprocess.PIPE,
138
+ 'close_fds': True,
139
+ 'executable': executable,
140
+
141
+ 'stream_stds': stream_stds,
142
+
143
+ # detach from parent group (no more inherited signals!)
144
+ #'preexec_fn': os.setpgrp
145
+ }
146
+
147
+ cmd = ' '.join(filter(None, [script] + list(args)))
148
+
149
+ process = NonBlockingPopen(cmd, **popen_kwargs)
150
+
151
+ if timeout is not None:
152
+ stop_at = datetime.now() + timedelta(seconds=timeout)
153
+ term_sent = False
154
+
155
+ while process.poll() is None:
156
+
157
+ if timeout is not None:
158
+ now = datetime.now()
159
+
160
+ if now > stop_at:
161
+ if term_sent is False:
162
+ # Kill the process group since sending the term signal
163
+ # would only terminate the shell, not the command
164
+ # executed in the shell
165
+ os.killpg(os.getpgid(process.pid), signal.SIGINT)
166
+ term_sent = True
167
+ continue
168
+
169
+ # As a last resort, kill the process group
170
+ os.killpg(os.getpgid(process.pid), signal.SIGKILL)
171
+
172
+ return 1, [
173
+ 'Process took more than {0} seconds to complete. '
174
+ 'Process Killed! Current STDOUT: \n{1}'.format(
175
+ timeout, process.obuff
176
+ )
177
+ ], [
178
+ 'Process took more than {0} seconds to complete. '
179
+ 'Process Killed! Current STDERR: \n{1}'.format(
180
+ timeout, process.ebuff
181
+ )
182
+ ]
183
+
184
+ process.communicate()
185
+
186
+ try:
187
+ return (
188
+ process.returncode,
189
+ process.obuff.splitlines(),
190
+ process.ebuff.splitlines()
191
+ )
192
+ finally:
193
+ try:
194
+ process.terminate()
195
+ except OSError:
196
+ # process already terminated
197
+ pass
198
+
199
+ def assert_script_result(self, fail_msg, expected_rcs, process_details):
200
+ if not isinstance(expected_rcs, (tuple, list)):
201
+ expected_rcs = (expected_rcs,)
202
+
203
+ rc, out, err = process_details
204
+ if rc not in expected_rcs:
205
+ err_msg = '{0}:\n'.format(fail_msg)
206
+ if out:
207
+ err_msg = '{0}STDOUT:\n{1}\n'.format(err_msg, '\n'.join(out))
208
+ if err:
209
+ err_msg = '{0}STDERR:\n{1}\n'.format(err_msg, '\n'.join(err))
210
+ if not err and not out:
211
+ err_msg = (
212
+ '{0} No stdout nor stderr captured. Exit code: {1}'.format(
213
+ err_msg, rc
214
+ )
215
+ )
216
+ raise AssertionError(err_msg.rstrip())
@@ -0,0 +1,640 @@
1
+ #! /usr/bin/perl -w
2
+
3
+ # This script is essentially copied from /usr/share/lintian/checks/scripts,
4
+ # which is:
5
+ # Copyright (C) 1998 Richard Braakman
6
+ # Copyright (C) 2002 Josip Rodin
7
+ # This version is
8
+ # Copyright (C) 2003 Julian Gilbey
9
+ #
10
+ # This program is free software; you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation; either version 2 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
+
23
+ use strict;
24
+ use Getopt::Long qw(:config gnu_getopt);
25
+ use File::Temp qw/tempfile/;
26
+
27
+ sub init_hashes;
28
+
29
+ (my $progname = $0) =~ s|.*/||;
30
+
31
+ my $usage = <<"EOF";
32
+ Usage: $progname [-n] [-f] [-x] script ...
33
+ or: $progname --help
34
+ or: $progname --version
35
+ This script performs basic checks for the presence of bashisms
36
+ in /bin/sh scripts.
37
+ EOF
38
+
39
+ my $version = <<"EOF";
40
+ This is $progname, from the Debian devscripts package, version 2.11.6ubuntu1.4
41
+ This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>,
42
+ based on original code which is copyright 1998 by Richard Braakman
43
+ and copyright 2002 by Josip Rodin.
44
+ This program comes with ABSOLUTELY NO WARRANTY.
45
+ You are free to redistribute this code under the terms of the
46
+ GNU General Public License, version 2, or (at your option) any later version.
47
+ EOF
48
+
49
+ my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
50
+ my ($opt_help, $opt_version);
51
+ my @filenames;
52
+
53
+ # Detect if STDIN is a pipe
54
+ if (-p STDIN or -f STDIN) {
55
+ my ($tmp_fh, $tmp_filename) = tempfile("chkbashisms_tmp.XXXX", TMPDIR => 1, UNLINK => 1);
56
+ while (my $line = <STDIN>) {
57
+ print $tmp_fh $line;
58
+ }
59
+ close($tmp_fh);
60
+ push(@ARGV, $tmp_filename);
61
+ }
62
+
63
+ ##
64
+ ## handle command-line options
65
+ ##
66
+ $opt_help = 1 if int(@ARGV) == 0;
67
+
68
+ GetOptions("help|h" => \$opt_help,
69
+ "version|v" => \$opt_version,
70
+ "newline|n" => \$opt_echo,
71
+ "force|f" => \$opt_force,
72
+ "extra|x" => \$opt_extra,
73
+ "posix|p" => \$opt_posix,
74
+ )
75
+ or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
76
+
77
+ if ($opt_help) { print $usage; exit 0; }
78
+ if ($opt_version) { print $version; exit 0; }
79
+
80
+ $opt_echo = 1 if $opt_posix;
81
+
82
+ my $status = 0;
83
+ my $makefile = 0;
84
+ my (%bashisms, %string_bashisms, %singlequote_bashisms);
85
+
86
+ my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)';
87
+ init_hashes;
88
+
89
+ foreach my $filename (@ARGV) {
90
+ my $check_lines_count = -1;
91
+
92
+ my $display_filename = $filename;
93
+ if ($filename =~ /chkbashisms_tmp\.....$/) {
94
+ $display_filename = "(stdin)";
95
+ }
96
+
97
+ if (!$opt_force) {
98
+ $check_lines_count = script_is_evil_and_wrong($filename);
99
+ }
100
+
101
+ if ($check_lines_count == 0 or $check_lines_count == 1) {
102
+ warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
103
+ next;
104
+ }
105
+
106
+ if ($check_lines_count != -1) {
107
+ warn "script $display_filename appears to be a shell wrapper; only checking the first "
108
+ . "$check_lines_count lines\n";
109
+ }
110
+
111
+ unless (open C, '<', $filename) {
112
+ warn "cannot open script $display_filename for reading: $!\n";
113
+ $status |= 2;
114
+ next;
115
+ }
116
+
117
+ my $cat_string = "";
118
+ my $cat_indented = 0;
119
+ my $quote_string = "";
120
+ my $last_continued = 0;
121
+ my $continued = 0;
122
+ my $found_rules = 0;
123
+ my $buffered_orig_line = "";
124
+ my $buffered_line = "";
125
+
126
+ while (<C>) {
127
+ next unless ($check_lines_count == -1 or $. <= $check_lines_count);
128
+
129
+ if ($. == 1) { # This should be an interpreter line
130
+ if (m,^\#!\s*(\S+),) {
131
+ my $interpreter = $1;
132
+
133
+ if ($interpreter =~ m,/make$,) {
134
+ init_hashes if !$makefile++;
135
+ $makefile = 1;
136
+ } else {
137
+ init_hashes if $makefile--;
138
+ $makefile = 0;
139
+ }
140
+ next if $opt_force;
141
+
142
+ if ($interpreter =~ m,/bash$,) {
143
+ warn "script $display_filename is already a bash script; skipping\n";
144
+ $status |= 2;
145
+ last; # end this file
146
+ }
147
+ elsif ($interpreter !~ m,/(sh|posh)$,) {
148
+ ### ksh/zsh?
149
+ warn "script $display_filename does not appear to be a /bin/sh script; skipping\n";
150
+ $status |= 2;
151
+ last;
152
+ }
153
+ } else {
154
+ warn "script $display_filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
155
+ }
156
+ }
157
+
158
+ chomp;
159
+ my $orig_line = $_;
160
+
161
+ # We want to remove end-of-line comments, so need to skip
162
+ # comments that appear inside balanced pairs
163
+ # of single or double quotes
164
+
165
+ # Remove comments in the "quoted" part of a line that starts
166
+ # in a quoted block? The problem is that we have no idea
167
+ # whether the program interpreting the block treats the
168
+ # quote character as part of the comment or as a quote
169
+ # terminator. We err on the side of caution and assume it
170
+ # will be treated as part of the comment.
171
+ # s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
172
+
173
+ # skip comment lines
174
+ if (m,^\s*\#, && $quote_string eq '' && $buffered_line eq '' && $cat_string eq '') {
175
+ next;
176
+ }
177
+
178
+ # Remove quoted strings so we can more easily ignore comments
179
+ # inside them
180
+ s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
181
+ s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
182
+
183
+ # If the remaining string contains what looks like a comment,
184
+ # eat it. In either case, swap the unmodified script line
185
+ # back in for processing.
186
+ if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
187
+ $_ = $orig_line;
188
+ s/\Q$1\E//; # eat comments
189
+ } else {
190
+ $_ = $orig_line;
191
+ }
192
+
193
+ # Handle line continuation
194
+ if (!$makefile && $cat_string eq '' && m/\\$/) {
195
+ chop;
196
+ $buffered_line .= $_;
197
+ $buffered_orig_line .= $orig_line . "\n";
198
+ next;
199
+ }
200
+
201
+ if ($buffered_line ne '') {
202
+ $_ = $buffered_line . $_;
203
+ $orig_line = $buffered_orig_line . $orig_line;
204
+ $buffered_line ='';
205
+ $buffered_orig_line ='';
206
+ }
207
+
208
+ if ($makefile) {
209
+ $last_continued = $continued;
210
+ if (/[^\\]\\$/) {
211
+ $continued = 1;
212
+ } else {
213
+ $continued = 0;
214
+ }
215
+
216
+ # Don't match lines that look like a rule if we're in a
217
+ # continuation line before the start of the rules
218
+ if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
219
+ $found_rules = 1;
220
+ $_ = $1 if $1;
221
+ }
222
+
223
+ last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
224
+
225
+ # Remove "simple" target names
226
+ s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
227
+ s/^\t//;
228
+ s/(?<!\$)\$\((\w+)\)/\${$1}/g;
229
+ s/(\$){2}/$1/g;
230
+ s/^[\s\t]*[@-]{1,2}//;
231
+ }
232
+
233
+ if ($cat_string ne "" && (m/^\Q$cat_string\E$/ || ($cat_indented && m/^\t*\Q$cat_string\E$/))) {
234
+ $cat_string = "";
235
+ next;
236
+ }
237
+ my $within_another_shell = 0;
238
+ if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
239
+ $within_another_shell = 1;
240
+ }
241
+ # if cat_string is set, we are in a HERE document and need not
242
+ # check for things
243
+ if ($cat_string eq "" and !$within_another_shell) {
244
+ my $found = 0;
245
+ my $match = '';
246
+ my $explanation = '';
247
+ my $line = $_;
248
+
249
+ # Remove "" / '' as they clearly aren't quoted strings
250
+ # and not considering them makes the matching easier
251
+ $line =~ s/(^|[^\\])(\'\')+/$1/g;
252
+ $line =~ s/(^|[^\\])(\"\")+/$1/g;
253
+
254
+ if ($quote_string ne "") {
255
+ my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
256
+ # Inside a quoted block
257
+ if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
258
+ my $rest = $1;
259
+ my $templine = $line;
260
+
261
+ # Remove quoted strings delimited with $otherquote
262
+ $templine =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
263
+ # Remove quotes that are themselves quoted
264
+ # "a'b"
265
+ $templine =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
266
+ # "\""
267
+ $templine =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
268
+
269
+ # After all that, were there still any quotes left?
270
+ my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
271
+ next if $count == 0;
272
+
273
+ $count = () = $rest =~ /(^|[^\\])$quote_string/g;
274
+ if ($count % 2 == 0) {
275
+ # Quoted block ends on this line
276
+ # Ignore everything before the closing quote
277
+ $line = $rest || '';
278
+ $quote_string = "";
279
+ } else {
280
+ next;
281
+ }
282
+ } else {
283
+ # Still inside the quoted block, skip this line
284
+ next;
285
+ }
286
+ }
287
+
288
+ # Check even if we removed the end of a quoted block
289
+ # in the previous check, as a single line can end one
290
+ # block and begin another
291
+ if ($quote_string eq "") {
292
+ # Possible start of a quoted block
293
+ for my $quote ("\"", "\'") {
294
+ my $templine = $line;
295
+ my $otherquote = ($quote eq "\"" ? "\'" : "\"");
296
+
297
+ # Remove balanced quotes and their content
298
+ $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
299
+ $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
300
+
301
+ # Don't flag quotes that are themselves quoted
302
+ # "a'b"
303
+ $templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
304
+ # "\""
305
+ $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
306
+ # \' or \"
307
+ $templine =~ s/\\[\'\"]//g;
308
+ my $count = () = $templine =~ /(^|(?!\\))$quote/g;
309
+
310
+ # If there's an odd number of non-escaped
311
+ # quotes in the line it's almost certainly the
312
+ # start of a quoted block.
313
+ if ($count % 2 == 1) {
314
+ $quote_string = $quote;
315
+ $line =~ s/^(.*)$quote.*$/$1/;
316
+ last;
317
+ }
318
+ }
319
+ }
320
+
321
+ # since this test is ugly, I have to do it by itself
322
+ # detect source (.) trying to pass args to the command it runs
323
+ # The first expression weeds out '. "foo bar"'
324
+ if (not $found and
325
+ not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
326
+ and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
327
+ if ($2 =~ /^(\&|\||\d?>|<)/) {
328
+ # everything is ok
329
+ ;
330
+ } else {
331
+ $found = 1;
332
+ $match = $1;
333
+ $explanation = "sourced script with arguments";
334
+ output_explanation($display_filename, $orig_line, $explanation);
335
+ }
336
+ }
337
+
338
+ # Remove "quoted quotes". They're likely to be inside
339
+ # another pair of quotes; we're not interested in
340
+ # them for their own sake and removing them makes finding
341
+ # the limits of the outer pair far easier.
342
+ $line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
343
+ $line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
344
+
345
+ while (my ($re,$expl) = each %singlequote_bashisms) {
346
+ if ($line =~ m/($re)/) {
347
+ $found = 1;
348
+ $match = $1;
349
+ $explanation = $expl;
350
+ output_explanation($display_filename, $orig_line, $explanation);
351
+ }
352
+ }
353
+
354
+ my $re='(?<![\$\\\])\$\'[^\']+\'';
355
+ if ($line =~ m/(.*)($re)/){
356
+ my $count = () = $1 =~ /(^|[^\\])\'/g;
357
+ if( $count % 2 == 0 ) {
358
+ output_explanation($display_filename, $orig_line, q<$'...' should be "$(printf '...')">);
359
+ }
360
+ }
361
+
362
+ # $cat_line contains the version of the line we'll check
363
+ # for heredoc delimiters later. Initially, remove any
364
+ # spaces between << and the delimiter to make the following
365
+ # updates to $cat_line easier.
366
+ my $cat_line = $line;
367
+ $cat_line =~ s/(<\<-?)\s+/$1/g;
368
+
369
+ # Ignore anything inside single quotes; it could be an
370
+ # argument to grep or the like.
371
+ $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
372
+
373
+ # As above, with the exception that we don't remove the string
374
+ # if the quote is immediately preceeded by a < or a -, so we
375
+ # can match "foo <<-?'xyz'" as a heredoc later
376
+ # The check is a little more greedy than we'd like, but the
377
+ # heredoc test itself will weed out any false positives
378
+ $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
379
+
380
+ $re='(?<![\$\\\])\$\"[^\"]+\"';
381
+ if ($line =~ m/(.*)($re)/){
382
+ my $count = () = $1 =~ /(^|[^\\])\"/g;
383
+ if( $count % 2 == 0 ) {
384
+ output_explanation($display_filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
385
+ }
386
+ }
387
+
388
+ while (my ($re,$expl) = each %string_bashisms) {
389
+ if ($line =~ m/($re)/) {
390
+ $found = 1;
391
+ $match = $1;
392
+ $explanation = $expl;
393
+ output_explanation($display_filename, $orig_line, $explanation);
394
+ }
395
+ }
396
+
397
+ # We've checked for all the things we still want to notice in
398
+ # double-quoted strings, so now remove those strings as well.
399
+ $line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
400
+ $cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
401
+ while (my ($re,$expl) = each %bashisms) {
402
+ if ($line =~ m/($re)/) {
403
+ $found = 1;
404
+ $match = $1;
405
+ $explanation = $expl;
406
+ output_explanation($display_filename, $orig_line, $explanation);
407
+ }
408
+ }
409
+
410
+ # Only look for the beginning of a heredoc here, after we've
411
+ # stripped out quoted material, to avoid false positives.
412
+ if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
413
+ $cat_indented = ($1 && $1 eq '-')? 1 : 0;
414
+ $cat_string = $2;
415
+ $cat_string = $3 if not defined $cat_string;
416
+ }
417
+ }
418
+ }
419
+
420
+ warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
421
+ if ($cat_string ne '');
422
+ warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
423
+ if ($quote_string ne '');
424
+ warn "error: $filename: EOF reached while on line continuation.\n"
425
+ if ($buffered_line ne '');
426
+
427
+ close C;
428
+ }
429
+
430
+ exit $status;
431
+
432
+ sub output_explanation {
433
+ my ($filename, $line, $explanation) = @_;
434
+
435
+ warn "possible bashism in $filename line $. ($explanation):\n$line\n";
436
+ $status |= 1;
437
+ }
438
+
439
+ # Returns non-zero if the given file is not actually a shell script,
440
+ # just looks like one.
441
+ sub script_is_evil_and_wrong {
442
+ my ($filename) = @_;
443
+ my $ret = -1;
444
+ # lintian's version of this function aborts if the file
445
+ # can't be opened, but we simply return as the next
446
+ # test in the calling code handles reporting the error
447
+ # itself
448
+ open (IN, '<', $filename) or return $ret;
449
+ my $i = 0;
450
+ my $var = "0";
451
+ my $backgrounded = 0;
452
+ local $_;
453
+ while (<IN>) {
454
+ chomp;
455
+ next if /^#/o;
456
+ next if /^$/o;
457
+ last if (++$i > 55);
458
+ if (m~
459
+ # the exec should either be "eval"ed or a new statement
460
+ (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
461
+
462
+ # eat anything between the exec and $0
463
+ exec\s*.+\s*
464
+
465
+ # optionally quoted executable name (via $0)
466
+ .?\$$var.?\s*
467
+
468
+ # optional "end of options" indicator
469
+ (--\s*)?
470
+
471
+ # Match expressions of the form '${1+$@}', '${1:+"$@"',
472
+ # '"${1+$@', "$@", etc where the quotes (before the dollar
473
+ # sign(s)) are optional and the second (or only if the $1
474
+ # clause is omitted) parameter may be $@ or $*.
475
+ #
476
+ # Finally the whole subexpression may be omitted for scripts
477
+ # which do not pass on their parameters (i.e. after re-execing
478
+ # they take their parameters (and potentially data) from stdin
479
+ .?(\${1:?\+.?)?(\$(\@|\*))?~x) {
480
+ $ret = $. - 1;
481
+ last;
482
+ } elsif (/^\s*(\w+)=\$0;/) {
483
+ $var = $1;
484
+ } elsif (m~
485
+ # Match scripts which use "foo $0 $@ &\nexec true\n"
486
+ # Program name
487
+ \S+\s+
488
+
489
+ # As above
490
+ .?\$$var.?\s*
491
+ (--\s*)?
492
+ .?(\${1:?\+.?)?(\$(\@|\*))?.?\s*\&~x) {
493
+
494
+ $backgrounded = 1;
495
+ } elsif ($backgrounded and m~
496
+ # the exec should either be "eval"ed or a new statement
497
+ (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
498
+ exec\s+true(\s|\Z)~x) {
499
+
500
+ $ret = $. - 1;
501
+ last;
502
+ } elsif (m~\@DPATCH\@~) {
503
+ $ret = $. - 1;
504
+ last;
505
+ }
506
+
507
+ }
508
+ close IN;
509
+ return $ret;
510
+ }
511
+
512
+ sub init_hashes {
513
+
514
+ %bashisms = (
515
+ qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
516
+ $LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
517
+ qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
518
+ qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
519
+ qr'\s\|\&' => q<pipelining is not POSIX>,
520
+ qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
521
+ qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
522
+ qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
523
+ $LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
524
+ $LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
525
+ => q<read without variable>,
526
+ $LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>,
527
+ $LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>,
528
+ $LEADIN . qr'let\s' => q<let ...>,
529
+ qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>,
530
+ qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>,
531
+ qr'\&>' => q<should be \>word 2\>&1>,
532
+ qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
533
+ q<should be \>word 2\>&1>,
534
+ qr'\[\[(?!:)' => q<alternative test command ([[ foo ]] should be [ foo ])>,
535
+ qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>,
536
+ $LEADIN . qr'builtin\s' => q<builtin>,
537
+ $LEADIN . qr'caller\s' => q<caller>,
538
+ $LEADIN . qr'compgen\s' => q<compgen>,
539
+ $LEADIN . qr'complete\s' => q<complete>,
540
+ $LEADIN . qr'declare\s' => q<declare>,
541
+ $LEADIN . qr'dirs(\s|\Z)' => q<dirs>,
542
+ $LEADIN . qr'disown\s' => q<disown>,
543
+ $LEADIN . qr'enable\s' => q<enable>,
544
+ $LEADIN . qr'mapfile\s' => q<mapfile>,
545
+ $LEADIN . qr'readarray\s' => q<readarray>,
546
+ $LEADIN . qr'shopt(\s|\Z)' => q<shopt>,
547
+ $LEADIN . qr'suspend\s' => q<suspend>,
548
+ $LEADIN . qr'time\s' => q<time>,
549
+ $LEADIN . qr'type\s' => q<type>,
550
+ $LEADIN . qr'typeset\s' => q<typeset>,
551
+ $LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>,
552
+ $LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>,
553
+ $LEADIN . qr'alias\s+-p' => q<alias -p>,
554
+ $LEADIN . qr'unalias\s+-a' => q<unalias -a>,
555
+ $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
556
+ qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
557
+ => q<function names should only contain [a-z0-9_]>,
558
+ $LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
559
+ $LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
560
+ qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substituion>,
561
+ $LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
562
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
563
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>,
564
+ $LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>,
565
+ qr'\[\^[^]]+\]' => q<[^] should be [!]>,
566
+ $LEADIN . qr'printf\s+-v' => q<'printf -v var ...' should be var='$(printf ...)'>,
567
+ $LEADIN . qr'coproc\s' => q<coproc>,
568
+ qr';;?&' => q<;;& and ;& special case operators>,
569
+ $LEADIN . qr'jobs\s' => q<jobs>,
570
+ # $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
571
+ $LEADIN . qr'command\s+-[^p]\s' => q<'command' with option other than -p>,
572
+ );
573
+
574
+ %string_bashisms = (
575
+ qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
576
+ qr'\$\{\w+\:\d+(?::\d+)?\}' => q<${foo:3[:1]}>,
577
+ qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
578
+ qr'\$\{!\w+\}' => q<${!name}>,
579
+ qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
580
+ qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
581
+ qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
582
+ qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
583
+ qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
584
+ qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>,
585
+ qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">,
586
+ qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">,
587
+ qr'\$\{?SECONDS\}?\b' => q<$SECONDS>,
588
+ qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>,
589
+ qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
590
+ qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
591
+ qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
592
+ qr'<<<' => q<\<\<\< here string>,
593
+ $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
594
+ qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,
595
+ qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' => q<'$((++n))' should be '$((n=n+1))'>,
596
+ qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' => q<'$((n--))' should be '$n; $((n=n-1))'>,
597
+ qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' => q<'$((--n))' should be '$((n=n-1))'>,
598
+ qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>,
599
+ $LEADIN . qr'printf\s["\'][^"\']+?%[qb].+?["\']' => q<printf %q|%b>,
600
+ );
601
+
602
+ %singlequote_bashisms = (
603
+ $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => q<unsafe echo with backslash>,
604
+ $LEADIN . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
605
+ q<should be '.', not 'source'>,
606
+ );
607
+
608
+ if ($opt_echo) {
609
+ $bashisms{$LEADIN . qr'echo\s+-[A-Za-z]*n'} = q<echo -n>;
610
+ }
611
+ if ($opt_posix) {
612
+ $bashisms{$LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)'} = q<local foo>;
613
+ $bashisms{$LEADIN . qr'local\s+\w+='} = q<local foo=bar>;
614
+ $bashisms{$LEADIN . qr'local\s+\w+\s+\w+'} = q<local x y>;
615
+ $bashisms{$LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s'} = q<test -a/-o>;
616
+ $bashisms{$LEADIN . qr'kill\s+-[^sl]\w*'} = q<kill -[0-9] or -[A-Z]>;
617
+ $bashisms{$LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]'} = q<trap with signal numbers>;
618
+ }
619
+
620
+ if ($makefile) {
621
+ $string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} =
622
+ q<'$(\< foo)' should be '$(cat foo)'>;
623
+ } else {
624
+ $bashisms{$LEADIN . qr'\w+\+='} = q<should be VAR="${VAR}foo">;
625
+ $string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} = q<'$(\< foo)' should be '$(cat foo)'>;
626
+ }
627
+
628
+ if ($opt_extra) {
629
+ $string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
630
+ $string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
631
+ $string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
632
+ $string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
633
+ $string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
634
+ $string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
635
+ $string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
636
+ $string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
637
+ $string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
638
+ $string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;
639
+ }
640
+ }