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.
- data/README.rst +131 -170
- data/example/complete/Vagrantfile +67 -0
- data/example/complete/salt/custom-bootstrap-salt.sh +2425 -0
- data/example/complete/salt/key/master.pem +30 -0
- data/example/complete/salt/key/master.pub +14 -0
- data/example/complete/salt/key/minion.pem +30 -0
- data/example/complete/salt/key/minion.pub +14 -0
- data/example/complete/salt/master +459 -0
- data/example/{salt/minion.conf → complete/salt/minion} +1 -2
- data/example/{salt → complete/salt}/roots/pillar/top.sls +0 -0
- data/example/complete/salt/roots/salt/nginx.sls +5 -0
- data/example/complete/salt/roots/salt/top.sls +3 -0
- data/example/masterless/Vagrantfile +18 -0
- data/example/masterless/salt/minion +219 -0
- data/example/{salt/roots/salt → masterless/salt/roots/pillar}/top.sls +0 -0
- data/example/masterless/salt/roots/salt/nginx.sls +5 -0
- data/example/masterless/salt/roots/salt/top.sls +3 -0
- data/lib/vagrant-salt.rb +16 -3
- data/lib/vagrant-salt/config.rb +103 -0
- data/lib/vagrant-salt/errors.rb +11 -0
- data/lib/vagrant-salt/plugin.rb +31 -0
- data/lib/vagrant-salt/provisioner.rb +211 -104
- data/lib/vagrant-salt/version.rb +5 -0
- data/scripts/.travis.yml +16 -0
- data/scripts/ChangeLog +39 -0
- data/scripts/LICENSE +16 -0
- data/scripts/README.rst +124 -35
- data/scripts/bootstrap-salt-minion.sh +1815 -381
- data/scripts/bootstrap-salt.sh +2425 -0
- data/scripts/salt-bootstrap.sh +2425 -0
- data/scripts/tests/README.rst +38 -0
- data/scripts/tests/bootstrap/__init__.py +11 -0
- data/{example/salt/key/KEYPAIR_GOES_HERE → scripts/tests/bootstrap/ext/__init__.py} +0 -0
- data/scripts/tests/bootstrap/ext/console.py +100 -0
- data/scripts/tests/bootstrap/ext/os_data.py +199 -0
- data/scripts/tests/bootstrap/test_install.py +586 -0
- data/scripts/tests/bootstrap/test_lint.py +27 -0
- data/scripts/tests/bootstrap/test_usage.py +28 -0
- data/scripts/tests/bootstrap/unittesting.py +216 -0
- data/scripts/tests/ext/checkbashisms +640 -0
- data/scripts/tests/install-testsuite-deps.py +99 -0
- data/scripts/tests/runtests.py +207 -0
- data/templates/locales/en.yml +14 -0
- data/vagrant-salt.gemspec +2 -2
- metadata +43 -10
- data/example/Vagrantfile +0 -26
- 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
|
+
}
|