vagrant-salt 0.3.2 → 0.4.0

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 (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
+ }