3 # This script is essentially copied from /usr/share/lintian/checks/scripts,
5 # Copyright (C) 1998 Richard Braakman
6 # Copyright (C) 2002 Josip Rodin
8 # Copyright (C) 2003 Julian Gilbey
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.
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.
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/>.
28 (my $progname = $0) =~ s|.*/||;
31 Usage: $progname [-n] [-f] [-x] script ...
33 or: $progname --version
34 This script performs basic checks for the presence of bashisms
38 my $version = <<"EOF";
39 This is $progname, from the Debian devscripts package, version ###VERSION###
40 This code is copyright 2003 by Julian Gilbey <jdg\@debian.org>,
41 based on original code which is copyright 1998 by Richard Braakman
42 and copyright 2002 by Josip Rodin.
43 This program comes with ABSOLUTELY NO WARRANTY.
44 You are free to redistribute this code under the terms of the
45 GNU General Public License, version 2, or (at your option) any later version.
48 my ($opt_echo, $opt_force, $opt_extra, $opt_posix);
49 my ($opt_help, $opt_version);
52 ## handle command-line options
54 $opt_help = 1 if int(@ARGV) == 0;
56 GetOptions("help|h" => \$opt_help,
57 "version|v" => \$opt_version,
58 "newline|n" => \$opt_echo,
59 "force|f" => \$opt_force,
60 "extra|x" => \$opt_extra,
61 "posix|p" => \$opt_posix,
63 or die "Usage: $progname [options] filelist\nRun $progname --help for more details\n";
65 if ($opt_help) { print $usage; exit 0; }
66 if ($opt_version) { print $version; exit 0; }
68 $opt_echo = 1 if $opt_posix;
72 my (%bashisms, %string_bashisms, %singlequote_bashisms);
74 my $LEADIN = qr'(?:(?:^|[`&;(|{])\s*|(?:if|then|do|while|shell)\s+)';
77 foreach my $filename (@ARGV) {
78 my $check_lines_count = -1;
81 $check_lines_count = script_is_evil_and_wrong($filename);
84 if ($check_lines_count == 0 or $check_lines_count == 1) {
85 warn "script $filename does not appear to be a /bin/sh script; skipping\n";
89 if ($check_lines_count != -1) {
90 warn "script $filename appears to be a shell wrapper; only checking the first "
91 . "$check_lines_count lines\n";
94 unless (open C, '<', $filename) {
95 warn "cannot open script $filename for reading: $!\n";
101 my $cat_indented = 0;
102 my $quote_string = "";
103 my $last_continued = 0;
106 my $buffered_orig_line = "";
107 my $buffered_line = "";
110 next unless ($check_lines_count == -1 or $. <= $check_lines_count);
112 if ($. == 1) { # This should be an interpreter line
113 if (m,^\#!\s*(\S+),) {
114 my $interpreter = $1;
116 if ($interpreter =~ m,/make$,) {
117 init_hashes if !$makefile++;
120 init_hashes if $makefile--;
125 if ($interpreter =~ m,/bash$,) {
126 warn "script $filename is already a bash script; skipping\n";
128 last; # end this file
130 elsif ($interpreter !~ m,/(sh|posh)$,) {
132 warn "script $filename does not appear to be a /bin/sh script; skipping\n";
137 warn "script $filename does not appear to have a \#! interpreter line;\nyou may get strange results\n";
144 # We want to remove end-of-line comments, so need to skip
145 # comments that appear inside balanced pairs
146 # of single or double quotes
148 # Remove comments in the "quoted" part of a line that starts
149 # in a quoted block? The problem is that we have no idea
150 # whether the program interpreting the block treats the
151 # quote character as part of the comment or as a quote
152 # terminator. We err on the side of caution and assume it
153 # will be treated as part of the comment.
154 # s/^(?:.*?[^\\])?$quote_string(.*)$/$1/ if $quote_string ne "";
157 if (m,^\s*\#, && $quote_string eq '' && $buffered_line eq '' && $cat_string eq '') {
161 # Remove quoted strings so we can more easily ignore comments
163 s/(^|[^\\](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
164 s/(^|[^\\](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
166 # If the remaining string contains what looks like a comment,
167 # eat it. In either case, swap the unmodified script line
168 # back in for processing.
169 if (m/(?:^|[^[\\])[\s\&;\(\)](\#.*$)/) {
171 s/\Q$1\E//; # eat comments
176 # Handle line continuation
177 if (!$makefile && $cat_string eq '' && m/\\$/) {
179 $buffered_line .= $_;
180 $buffered_orig_line .= $orig_line . "\n";
184 if ($buffered_line ne '') {
185 $_ = $buffered_line . $_;
186 $orig_line = $buffered_orig_line . $orig_line;
188 $buffered_orig_line ='';
192 $last_continued = $continued;
199 # Don't match lines that look like a rule if we're in a
200 # continuation line before the start of the rules
201 if (/^[\w%-]+:+\s.*?;?(.*)$/ and !($last_continued and !$found_rules)) {
206 last if m%^\s*(override\s|export\s)?\s*SHELL\s*:?=\s*(/bin/)?bash\s*%;
208 # Remove "simple" target names
209 s/^[\w%.-]+(?:\s+[\w%.-]+)*::?//;
211 s/(?<!\$)\$\((\w+)\)/\${$1}/g;
213 s/^[\s\t]*[@-]{1,2}//;
216 if ($cat_string ne "" && (m/^\Q$cat_string\E$/ || ($cat_indented && m/^\t*\Q$cat_string\E$/))) {
220 my $within_another_shell = 0;
221 if (m,(^|\s+)((/usr)?/bin/)?((b|d)?a|k|z|t?c)sh\s+-c\s*.+,) {
222 $within_another_shell = 1;
224 # if cat_string is set, we are in a HERE document and need not
226 if ($cat_string eq "" and !$within_another_shell) {
229 my $explanation = '';
232 # Remove "" / '' as they clearly aren't quoted strings
233 # and not considering them makes the matching easier
234 $line =~ s/(^|[^\\])(\'\')+/$1/g;
235 $line =~ s/(^|[^\\])(\"\")+/$1/g;
237 if ($quote_string ne "") {
238 my $otherquote = ($quote_string eq "\"" ? "\'" : "\"");
239 # Inside a quoted block
240 if ($line =~ /(?:^|^.*?[^\\])$quote_string(.*)$/) {
242 my $templine = $line;
244 # Remove quoted strings delimited with $otherquote
245 $templine =~ s/(^|[^\\])$otherquote[^$quote_string]*?[^\\]$otherquote/$1/g;
246 # Remove quotes that are themselves quoted
248 $templine =~ s/(^|[^\\])$otherquote.*?$quote_string.*?[^\\]$otherquote/$1/g;
250 $templine =~ s/(^|[^\\])$quote_string\\$quote_string$quote_string/$1/g;
252 # After all that, were there still any quotes left?
253 my $count = () = $templine =~ /(^|[^\\])$quote_string/g;
256 $count = () = $rest =~ /(^|[^\\])$quote_string/g;
257 if ($count % 2 == 0) {
258 # Quoted block ends on this line
259 # Ignore everything before the closing quote
266 # Still inside the quoted block, skip this line
271 # Check even if we removed the end of a quoted block
272 # in the previous check, as a single line can end one
273 # block and begin another
274 if ($quote_string eq "") {
275 # Possible start of a quoted block
276 for my $quote ("\"", "\'") {
277 my $templine = $line;
278 my $otherquote = ($quote eq "\"" ? "\'" : "\"");
280 # Remove balanced quotes and their content
281 $templine =~ s/(^|[^\\\"](?:\\\\)*)\'[^\']*\'/$1/g;
282 $templine =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1/g;
284 # Don't flag quotes that are themselves quoted
286 $templine =~ s/$otherquote.*?$quote.*?$otherquote//g;
288 $templine =~ s/(^|[^\\])$quote\\$quote$quote/$1/g;
290 $templine =~ s/\\[\'\"]//g;
291 my $count = () = $templine =~ /(^|(?!\\))$quote/g;
293 # If there's an odd number of non-escaped
294 # quotes in the line it's almost certainly the
295 # start of a quoted block.
296 if ($count % 2 == 1) {
297 $quote_string = $quote;
298 $line =~ s/^(.*)$quote.*$/$1/;
304 # since this test is ugly, I have to do it by itself
305 # detect source (.) trying to pass args to the command it runs
306 # The first expression weeds out '. "foo bar"'
308 not m/$LEADIN\.\s+(\"[^\"]+\"|\'[^\']+\'|\$\([^)]+\)+(?:\/[^\s;]+)?)\s*(\&|\||\d?>|<|;|\Z)/
309 and m/$LEADIN(\.\s+[^\s;\`:]+\s+([^\s;]+))/) {
310 if ($2 =~ /^(\&|\||\d?>|<)/) {
316 $explanation = "sourced script with arguments";
317 output_explanation($filename, $orig_line, $explanation);
321 # Remove "quoted quotes". They're likely to be inside
322 # another pair of quotes; we're not interested in
323 # them for their own sake and removing them makes finding
324 # the limits of the outer pair far easier.
325 $line =~ s/(^|[^\\\'\"])\"\'\"/$1/g;
326 $line =~ s/(^|[^\\\'\"])\'\"\'/$1/g;
328 while (my ($re,$expl) = each %singlequote_bashisms) {
329 if ($line =~ m/($re)/) {
332 $explanation = $expl;
333 output_explanation($filename, $orig_line, $explanation);
337 my $re='(?<![\$\\\])\$\'[^\']+\'';
338 if ($line =~ m/(.*)($re)/){
339 my $count = () = $1 =~ /(^|[^\\])\'/g;
340 if( $count % 2 == 0 ) {
341 output_explanation($filename, $orig_line, q<$'...' should be "$(printf '...')">);
345 # $cat_line contains the version of the line we'll check
346 # for heredoc delimiters later. Initially, remove any
347 # spaces between << and the delimiter to make the following
348 # updates to $cat_line easier.
349 my $cat_line = $line;
350 $cat_line =~ s/(<\<-?)\s+/$1/g;
352 # Ignore anything inside single quotes; it could be an
353 # argument to grep or the like.
354 $line =~ s/(^|[^\\\"](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
356 # As above, with the exception that we don't remove the string
357 # if the quote is immediately preceeded by a < or a -, so we
358 # can match "foo <<-?'xyz'" as a heredoc later
359 # The check is a little more greedy than we'd like, but the
360 # heredoc test itself will weed out any false positives
361 $cat_line =~ s/(^|[^<\\\"-](?:\\\\)*)\'(?:\\.|[^\\\'])+\'/$1''/g;
363 $re='(?<![\$\\\])\$\"[^\"]+\"';
364 if ($line =~ m/(.*)($re)/){
365 my $count = () = $1 =~ /(^|[^\\])\"/g;
366 if( $count % 2 == 0 ) {
367 output_explanation($filename, $orig_line, q<$"foo" should be eval_gettext "foo">);
371 while (my ($re,$expl) = each %string_bashisms) {
372 if ($line =~ m/($re)/) {
375 $explanation = $expl;
376 output_explanation($filename, $orig_line, $explanation);
380 # We've checked for all the things we still want to notice in
381 # double-quoted strings, so now remove those strings as well.
382 $line =~ s/(^|[^\\\'](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
383 $cat_line =~ s/(^|[^<\\\'-](?:\\\\)*)\"(?:\\.|[^\\\"])+\"/$1""/g;
384 while (my ($re,$expl) = each %bashisms) {
385 if ($line =~ m/($re)/) {
388 $explanation = $expl;
389 output_explanation($filename, $orig_line, $explanation);
393 # Only look for the beginning of a heredoc here, after we've
394 # stripped out quoted material, to avoid false positives.
395 if ($cat_line =~ m/(?:^|[^<])\<\<(\-?)\s*(?:[\\]?(\w+)|[\'\"](.*?)[\'\"])/) {
396 $cat_indented = ($1 && $1 eq '-')? 1 : 0;
398 $cat_string = $3 if not defined $cat_string;
403 warn "error: $filename: Unterminated heredoc found, EOF reached. Wanted: <$cat_string>\n"
404 if ($cat_string ne '');
405 warn "error: $filename: Unterminated quoted string found, EOF reached. Wanted: <$quote_string>\n"
406 if ($quote_string ne '');
407 warn "error: $filename: EOF reached while on line continuation.\n"
408 if ($buffered_line ne '');
415 sub output_explanation {
416 my ($filename, $line, $explanation) = @_;
418 warn "possible bashism in $filename line $. ($explanation):\n$line\n";
422 # Returns non-zero if the given file is not actually a shell script,
423 # just looks like one.
424 sub script_is_evil_and_wrong {
427 # lintian's version of this function aborts if the file
428 # can't be opened, but we simply return as the next
429 # test in the calling code handles reporting the error
431 open (IN, '<', $filename) or return $ret;
434 my $backgrounded = 0;
442 # the exec should either be "eval"ed or a new statement
443 (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
445 # eat anything between the exec and $0
448 # optionally quoted executable name (via $0)
451 # optional "end of options" indicator
454 # Match expressions of the form '${1+$@}', '${1:+"$@"',
455 # '"${1+$@', "$@", etc where the quotes (before the dollar
456 # sign(s)) are optional and the second (or only if the $1
457 # clause is omitted) parameter may be $@ or $*.
459 # Finally the whole subexpression may be omitted for scripts
460 # which do not pass on their parameters (i.e. after re-execing
461 # they take their parameters (and potentially data) from stdin
462 .?(\${1:?\+.?)?(\$(\@|\*))?~x) {
465 } elsif (/^\s*(\w+)=\$0;/) {
468 # Match scripts which use "foo $0 $@ &\nexec true\n"
475 .?(\${1:?\+.?)?(\$(\@|\*))?.?\s*\&~x) {
478 } elsif ($backgrounded and m~
479 # the exec should either be "eval"ed or a new statement
480 (^\s*|\beval\s*[\'\"]|(;|&&|\b(then|else))\s*)
481 exec\s+true(\s|\Z)~x) {
485 } elsif (m~\@DPATCH\@~) {
498 qr'(?:^|\s+)function \w+(\s|\(|\Z)' => q<'function' is useless>,
499 $LEADIN . qr'select\s+\w+' => q<'select' is not POSIX>,
500 qr'(test|-o|-a)\s*[^\s]+\s+==\s' => q<should be 'b = a'>,
501 qr'\[\s+[^\]]+\s+==\s' => q<should be 'b = a'>,
502 qr'\s\|\&' => q<pipelining is not POSIX>,
503 qr'[^\\\$]\{([^\s\\\}]*?,)+[^\\\}\s]*\}' => q<brace expansion>,
504 qr'\{\d+\.\.\d+\}' => q<brace expansion, should be $(seq a b)>,
505 qr'(?:^|\s+)\w+\[\d+\]=' => q<bash arrays, H[0]>,
506 $LEADIN . qr'read\s+(?:-[a-qs-zA-Z\d-]+)' => q<read with option other than -r>,
507 $LEADIN . qr'read\s*(?:-\w+\s*)*(?:\".*?\"|[\'].*?[\'])?\s*(?:;|$)'
508 => q<read without variable>,
509 $LEADIN . qr'echo\s+(-n\s+)?-n?en?\s' => q<echo -e>,
510 $LEADIN . qr'exec\s+-[acl]' => q<exec -c/-l/-a name>,
511 $LEADIN . qr'let\s' => q<let ...>,
512 qr'(?<![\$\(])\(\(.*\)\)' => q<'((' should be '$(('>,
513 qr'(?:^|\s+)(\[|test)\s+-a' => q<test with unary -a (should be -e)>,
514 qr'\&>' => q<should be \>word 2\>&1>,
515 qr'(<\&|>\&)\s*((-|\d+)[^\s;|)}`&\\\\]|[^-\d\s]+(?<!\$)(?!\d))' =>
516 q<should be \>word 2\>&1>,
517 qr'\[\[(?!:)' => q<alternative test command ([[ foo ]] should be [ foo ])>,
518 qr'/dev/(tcp|udp)' => q</dev/(tcp|udp)>,
519 $LEADIN . qr'builtin\s' => q<builtin>,
520 $LEADIN . qr'caller\s' => q<caller>,
521 $LEADIN . qr'compgen\s' => q<compgen>,
522 $LEADIN . qr'complete\s' => q<complete>,
523 $LEADIN . qr'declare\s' => q<declare>,
524 $LEADIN . qr'dirs(\s|\Z)' => q<dirs>,
525 $LEADIN . qr'disown\s' => q<disown>,
526 $LEADIN . qr'enable\s' => q<enable>,
527 $LEADIN . qr'mapfile\s' => q<mapfile>,
528 $LEADIN . qr'readarray\s' => q<readarray>,
529 $LEADIN . qr'shopt(\s|\Z)' => q<shopt>,
530 $LEADIN . qr'suspend\s' => q<suspend>,
531 $LEADIN . qr'time\s' => q<time>,
532 $LEADIN . qr'type\s' => q<type>,
533 $LEADIN . qr'typeset\s' => q<typeset>,
534 $LEADIN . qr'ulimit(\s|\Z)' => q<ulimit>,
535 $LEADIN . qr'set\s+-[BHT]+' => q<set -[BHT]>,
536 $LEADIN . qr'alias\s+-p' => q<alias -p>,
537 $LEADIN . qr'unalias\s+-a' => q<unalias -a>,
538 $LEADIN . qr'local\s+-[a-zA-Z]+' => q<local -opt>,
539 qr'(?:^|\s+)\s*\(?\w*[^\(\w\s]+\S*?\s*\(\)\s*([\{|\(]|\Z)'
540 => q<function names should only contain [a-z0-9_]>,
541 $LEADIN . qr'(push|pop)d(\s|\Z)' => q<(push|pop)d>,
542 $LEADIN . qr'export\s+-[^p]' => q<export only takes -p as an option>,
543 qr'(?:^|\s+)[<>]\(.*?\)' => q<\<() process substituion>,
544 $LEADIN . qr'readonly\s+-[af]' => q<readonly -[af]>,
545 $LEADIN . qr'(sh|\$\{?SHELL\}?) -[rD]' => q<sh -[rD]>,
546 $LEADIN . qr'(sh|\$\{?SHELL\}?) --\w+' => q<sh --long-option>,
547 $LEADIN . qr'(sh|\$\{?SHELL\}?) [-+]O' => q<sh [-+]O>,
548 qr'\[\^[^]]+\]' => q<[^] should be [!]>,
549 $LEADIN . qr'printf\s+-v' => q<'printf -v var ...' should be var='$(printf ...)'>,
550 $LEADIN . qr'coproc\s' => q<coproc>,
551 qr';;?&' => q<;;& and ;& special case operators>,
552 $LEADIN . qr'jobs\s' => q<jobs>,
553 # $LEADIN . qr'jobs\s+-[^lp]\s' => q<'jobs' with option other than -l or -p>,
554 $LEADIN . qr'command\s+-[^p]\s' => q<'command' with option other than -p>,
558 qr'\$\[[^][]+\]' => q<'$[' should be '$(('>,
559 qr'\$\{\w+\:\d+(?::\d+)?\}' => q<${foo:3[:1]}>,
560 qr'\$\{!\w+[\@*]\}' => q<${!prefix[*|@]>,
561 qr'\$\{!\w+\}' => q<${!name}>,
562 qr'\$\{\w+(/.+?){1,2}\}' => q<${parm/?/pat[/str]}>,
563 qr'\$\{\#?\w+\[[0-9\*\@]+\]\}' => q<bash arrays, ${name[0|*|@]}>,
564 qr'\$\{?RANDOM\}?\b' => q<$RANDOM>,
565 qr'\$\{?(OS|MACH)TYPE\}?\b' => q<$(OS|MACH)TYPE>,
566 qr'\$\{?HOST(TYPE|NAME)\}?\b' => q<$HOST(TYPE|NAME)>,
567 qr'\$\{?DIRSTACK\}?\b' => q<$DIRSTACK>,
568 qr'\$\{?EUID\}?\b' => q<$EUID should be "$(id -u)">,
569 qr'\$\{?UID\}?\b' => q<$UID should be "$(id -ru)">,
570 qr'\$\{?SECONDS\}?\b' => q<$SECONDS>,
571 qr'\$\{?BASH_[A-Z]+\}?\b' => q<$BASH_SOMETHING>,
572 qr'\$\{?SHELLOPTS\}?\b' => q<$SHELLOPTS>,
573 qr'\$\{?PIPESTATUS\}?\b' => q<$PIPESTATUS>,
574 qr'\$\{?SHLVL\}?\b' => q<$SHLVL>,
575 qr'<<<' => q<\<\<\< here string>,
576 $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\"[^\"]*(\\[abcEfnrtv0])+.*?[\"]' => q<unsafe echo with backslash>,
577 qr'\$\(\([\s\w$*/+-]*\w\+\+.*?\)\)' => q<'$((n++))' should be '$n; $((n=n+1))'>,
578 qr'\$\(\([\s\w$*/+-]*\+\+\w.*?\)\)' => q<'$((++n))' should be '$((n=n+1))'>,
579 qr'\$\(\([\s\w$*/+-]*\w\-\-.*?\)\)' => q<'$((n--))' should be '$n; $((n=n-1))'>,
580 qr'\$\(\([\s\w$*/+-]*\-\-\w.*?\)\)' => q<'$((--n))' should be '$((n=n-1))'>,
581 qr'\$\(\([\s\w$*/+-]*\*\*.*?\)\)' => q<exponentiation is not POSIX>,
582 $LEADIN . qr'printf\s["\'][^"\']+?%[qb].+?["\']' => q<printf %q|%b>,
585 %singlequote_bashisms = (
586 $LEADIN . qr'echo\s+(?:-[^e\s]+\s+)?\'[^\']*(\\[abcEfnrtv0])+.*?[\']' => q<unsafe echo with backslash>,
587 $LEADIN . qr'source\s+[\"\']?(?:\.\/|\/|\$|[\w~.-])\S*' =>
588 q<should be '.', not 'source'>,
592 $bashisms{$LEADIN . qr'echo\s+-[A-Za-z]*n'} = q<echo -n>;
595 $bashisms{$LEADIN . qr'local\s+\w+(\s+\W|\s*[;&|)]|$)'} = q<local foo>;
596 $bashisms{$LEADIN . qr'local\s+\w+='} = q<local foo=bar>;
597 $bashisms{$LEADIN . qr'local\s+\w+\s+\w+'} = q<local x y>;
598 $bashisms{$LEADIN . qr'((?:test|\[)\s+.+\s-[ao])\s'} = q<test -a/-o>;
599 $bashisms{$LEADIN . qr'kill\s+-[^sl]\w*'} = q<kill -[0-9] or -[A-Z]>;
600 $bashisms{$LEADIN . qr'trap\s+["\']?.*["\']?\s+.*[1-9]'} = q<trap with signal numbers>;
604 $string_bashisms{qr'(\$\(|\`)\s*\<\s*([^\s\)]{2,}|[^DF])\s*(\)|\`)'} =
605 q<'$(\< foo)' should be '$(cat foo)'>;
607 $bashisms{$LEADIN . qr'\w+\+='} = q<should be VAR="${VAR}foo">;
608 $string_bashisms{qr'(\$\(|\`)\s*\<\s*\S+\s*(\)|\`)'} = q<'$(\< foo)' should be '$(cat foo)'>;
612 $string_bashisms{qr'\$\{?BASH\}?\b'} = q<$BASH>;
613 $string_bashisms{qr'(?:^|\s+)RANDOM='} = q<RANDOM=>;
614 $string_bashisms{qr'(?:^|\s+)(OS|MACH)TYPE='} = q<(OS|MACH)TYPE=>;
615 $string_bashisms{qr'(?:^|\s+)HOST(TYPE|NAME)='} = q<HOST(TYPE|NAME)=>;
616 $string_bashisms{qr'(?:^|\s+)DIRSTACK='} = q<DIRSTACK=>;
617 $string_bashisms{qr'(?:^|\s+)EUID='} = q<EUID=>;
618 $string_bashisms{qr'(?:^|\s+)UID='} = q<UID=>;
619 $string_bashisms{qr'(?:^|\s+)BASH(_[A-Z]+)?='} = q<BASH(_SOMETHING)=>;
620 $string_bashisms{qr'(?:^|\s+)SHELLOPTS='} = q<SHELLOPTS=>;
621 $string_bashisms{qr'\$\{?POSIXLY_CORRECT\}?\b'} = q<$POSIXLY_CORRECT>;