How to syntax check portable POSIX shell scripts? [duplicate] - shell

This question already has answers here:
How do I syntax check a Bash script without running it?
(10 answers)
Closed 6 years ago.
The following shell script executes well when provided /bin/true for the first argument, but may otherwise fail with a syntax error during execution!
#!/bin/sh
if $1 ; then exit; fi
/tmp/asdf <<< ASDF # Something with syntax error in POSIX
Surely some syntax errors (if not all?) can be avoided by static checking? How do I statically check whether a given Shell Command Language script is syntactically valid?
EDIT: Checking for syntax errors in Bash scripts has been answered in this question.
EDIT #2: Note that Bash fails to properly check whether the syntax adheres to POSIX even when executed with the +B and --posix flags in addition to -n.

All POSIX-compatible Shell Command Language shells support the set -n built-in which can be used to check the syntax of the script. Therefore it is possible to prepend
set -n
to your code to syntax check it. Note also that standard sh utility is also required to support a command-line -n flag, which has equivalent semantics to using set -n. Bash and possibly other shells also support this command-line flag. Therefore you can simply run the following to syntax check your script:
sh -n yourScriptFilename.sh
WARNING: This does not give you a guarantee that the script has fully POSIX compatible syntax. For example, Bash allows bashisms (e.g. arrays and c{a,u}t) to go unnoticed even when using the --posix (and/or +B) command line option in addition to -n when invoked as sh. Other shells might have similar issues.

With bash you can use -n:
bash -n file.sh
Output:
a.sh: line 3: syntax error near unexpected token `then'
a.sh: line 3: `if then fi # Something with syntax error'
Since bash supports the --posix options you may run
bash --posix -n file.sh
to perform a posix compatible check. I don't know how posixly correct that mode is in detail.

Related

Failing script as process substitution not working in Dart

Is there any way to make the script:
diff <(echo 'hello') <(echo 'hello-2')
work, as currently it fails with error in Dart when run using Process.run or Process.start:
syntax error near unexpected token `('
I tried to drill down on it and found out that since process substitution is not available when running the script through Process.run or Process.start, hence it's failing. So, is there any way to make it work?
I found out that we need to use set +o posix to make process substitution available if it's not, but I don't know how to do it in Dart.
According to the documentation and common expectations, the shell will be /bin/sh; and so, you can't use Bash features like process substitutions.
You can always force it by running Bash explicitly:
Process.run("bash", ["-c", "diff <(echo 'hello') <(echo 'hello-2')"])
You could add set +o posix; to the beginning of the argument to bash -c (the argument can be an arbitrarily complex string with multiple commands) but of course, it's not necessary here, and actually a red herring in this context.
Here's an example of a slightly more complex command which uses the Bash-only "C-style" for loop syntax.
Process.run("bash", ["-c", "for ((i=0; i<=255; i++)); do ping -c 3 10.9.8.$i || break; done; echo 'finished'"])
... though generally speaking, it's probably better to keep your subprocesses as simple as possible, and write any necessary plumbing in the host language.
Perhaps see also Difference between sh and bash

Converting a BASH script to run on SH (via BusyBox)

I have an Asus router running a recent version of FreshTomato - that comes with BusyBox.
I need to run a script that was made with BASH in mind - it is an adaptation of this script - but it fails to run with this error: line 41: syntax error: bad substitution
Checking the script with shellcheck.net yields these errors:
Line 41:
for optionvarname in ${!foreign_option_*} ; do
^-- SC3053: In POSIX sh, indirect expansion is undefined.
^-- SC3056: In POSIX sh, name matching prefixes are undefined.
Line 42:
option="${!optionvarname}"
^-- SC3053: In POSIX sh, indirect expansion is undefined.
These are the lines that are causing problems:
for optionvarname in ${!foreign_option_*} ; do # line 41
option="${!optionvarname}" # line 42
# do some stuff with $option...
done
If my understanding is correct, the original script simply does something with all variables that have a name starting with foreign_option_
However, as far as I could determine, both ${!foreign_option_*} and ${!optionvarname} constructs are BASH-specific and not POSIX compliant, so there is no direct "bash to sh" code conversion possible.
I have tried to create a /bin/bash symlink that points to busybox, but I got the Read-only file system error.
So, how can I get this script to run on my router? I see only two options, but I cant figure out how to implement either:
Make BusyBox interpret the script as BASH instead of SH - can I use a specific shebang for this?
Seems like the fastest option, but only if BusyBox has a "complete" implementation of BASH
Alter the script code to not use BASH specifics.
This is safer, but since there is no "collect al variables starting with X" for SH, how can I do it?
how can I get this script to run on my router?
That easy, either:
install bash on your router or
port the script to busybox/posix compatible shell.
Make BusyBox interpret the script as BASH instead of SH - can I use a specific shebang for this?
That doesn't make sense. Busybox comes with ash shell interpreter and bash is bash. Bash can interpret bash extensions, ash can't interpret them. You can't "make busybox interpret bash" - cars don't fly, planes are for that. If you want to make a car fly, you add wings to it and make it faster. The answer to Make BusyBox interpret the script as BASH instead of SH would be: patch busybox and implement all bash extensions in it.
Shebang is used to run a file under different interpreter. Using #!/bin/bash would invoke bash, which would be unrelated to anything busybox related and busybox wouldn't be involved in it.
how can I do it?
Decide on a unrealistic maximum, iterate over variables named foreign_option_{1...some_max}, for each variable see if it is set, if it is set, cotinue the script.
for i in $(seq 100); do
optionvarname="foreign_option_${i}"
# https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash
if eval "[ -z \"\${${optionvarname}+x}\" ]"; then continue; fi;
With enough luck maybe you can use the set output. The following will fail if any variable contains a value as newline + the string that matches the regex:
for optionvarname in $(set | grep -o '^foreign_option_[0-9]\+=' | sed 's/=//'); then
Indirect expansion can be easily replaced by eval:
eval "option=\"\$${optionvarname}\""
If you really cannot install Bash on that router, here is one possible workaround, which seems to work for me in BusyBox on a Qnap NAS :
foreign_option_one=1
foreign_option_two=2
for x in one two; do
opt_var=foreign_option_${x}
eval "opt_value=\$$opt_var"
echo "$opt_var = $opt_value"
done
(But you will probably encounter more problems with moving a Bash script to busybox, so you might want to first consider alternatives like replacing the router)

What does '#!/bin/sh -xe' mean? [duplicate]

This question already has answers here:
What does the line '!/bin/sh -e' do? [closed]
(2 answers)
How to echo shell commands as they are executed
(14 answers)
Closed 4 years ago.
In the repository on GitHub of Tribler, I see a file with the interpreter: #!/bin/sh -xe.
On Mac and Linux man sh redirects to BASH(1) (same as man bash).
In this man file, under the section:
OPTIONS
,there is no mention of an option -x , nor an option -e.
What is the meaning of the interpreter #!/bin/sh -xe ?
I found this:
man sh:
-x xtrace Write each command to standard error (preceded by
a ‘+ ’) before it is executed. Useful for debug‐
ging.
-e errexit If not interactive, exit immediately if any
untested command fails. The exit status of a com‐
mand is considered to be explicitly tested if the
command is used to control an if, elif, while, or
until; or if the command is the left hand operand
of an “&&” or “||” operator.
It seems these are just error control options that make sure nothing worse happens if a command fails.
Per man page: read more
The -a, -b, -C, -e, -f, -m, -n, -o option, -u, -v, and -x options are described as part of the set utility in Special Built-In Utilities. The option letters derived from the set special built-in shall also be accepted with a leading plus sign ( '+' ) instead of a leading hyphen (meaning the reverse case of the option as described in this volume of IEEE Std 1003.1-2001).
when you read set man pages you get:
-x The shell shall write to standard error a trace for each command after it expands the command and before it executes it. It is unspecified whether the command that turns tracing off is traced.
and
-e When this option is on, if a simple command fails for any of the reasons listed in Consequences of Shell Errors or returns an exit status value >0, and is not part of the compound list following a while, until, or if keyword, and is not a part of an AND or OR list, and is not a pipeline preceded by the ! reserved word, then the shell shall immediately exit.

Bash strips "N" character when using newline-delimiter

I am using Windows Linux Subsystem (Ubuntu). When I try to set a newline-delimiter, I lose my 'n'-characters. My simplified script;
#!/bin/sh
echo $HOME #gives /home/hennio
IFS=$'\n'
echo $HOME #gives /home/he io
IFS=$'\n\b' didnt solve the problem. I checked my shebang with $(which sh), it is correct (although using zsh).
Searching on internet didnt give any results. Can someone please tell me whats going on? It driving me nuts..
To maintain compatibility, a shell invoked as /bin/sh usually tries to emulate a POSIX shell or some variant of a Bourne shell. Neither POSIX nor Bourne support $'...'. There are two possible solutions:
Method 1: Use the shebang of a shell, like bash, that supports $'...'.
Or,
Method 2: Use a POSIX method to assigning a newline to IFS:
IFS='
'
(Hat tip: Gordon Davisson)
Documentation
From man zsh:
Zsh tries to emulate sh or ksh when it is invoked as sh or ksh
respectively
From man bash:
If bash is invoked with the name sh, it tries to mimic the
startup behavior of historical versions of sh as closely as possible,
while conforming to the POSIX standard as well.

vim red highlight $( )

I was writing a script when I decided to move the functions to a lib file, but when I open the lib file all the $( and the consecutive ) are red highlighted, here are some examples of the script
TAB="$(printf '\t')"
percent=$(echo "scale=2; $number/$total*100" | bc | sed -e 's/\.[[:digit:]]*//g')
if [[ -z $(grep $site/post $max_lim) ]];then
The filetype is conf but I've set it as sh syntax in .vimrc
Any idea of what is happenning?
Thank you
Edit: Thanks for the quick answers, I found that this line makes vim match the files with the extension specified behind the * with the syntax sh
au BufReadPost * set syntax=sh
I've also thought that using shebang in the libraries was not allowed, but is a nice solution
Anyway using g:is_bash in .vimrc returns an error of pattern not found
So what I would like to do is as I only write in bash, to vim recognize any file without extension as bash
The syntax file for sh actually handles several different kinds of shell syntax: bash, ksh, and plain old sh. Since your conf file isn't recognized as bash or ksh, it falls back to sh. $(...) isn't a valid construct in sh, so it is highlighted as an error.
To fix this, you can make sure "g:is_bash" is set for the file, so that the sh syntax script will know your file should be highlighted as bash code. Please edit your question to include what you added to your .vimrc to make the file use sh syntax highlighting. This will make it easier to suggest the correct way of setting "g:is_bash".
UPDATE: As Alok commented, you should be able to add the following to the file
#!/bin/bash
to let vim know the correct syntax highlighting to use as well.
In my case, I wanted to preserve #!/bin/sh as the shebang line because not every system has /bin/bash available.
Whereas the original Bourne shell may have not supported the $(...) syntax, most sh shells nowadays are POSIX-compliant, and the POSIX spec supports this syntax. For example,
On Ubuntu, /bin/sh is /bin/dash.
On MacOS, /bin/sh is /bin/bash.
On Alpine, /bin/sh is /bin/ash.
All of which satisfy the POSIX spec. Traditionally, if we'd like to write portable Shell, we should leave the shebang line as #!/bin/sh. We shouldn't change it to #!/bin/bash just for syntax highlighting if we're not going to use any Bashisms.
Okay, but what about the erroneous red highlighting? The problem is with Vim interpreting #!/bin/sh as a reference to the original Bourne shell from 1979 with no support for $(...). Maybe this is a testament to Vim's backwards compatibility, or maybe not enough people care. Here's a related GitHub issue describing the same behavior.
In any case, the best solution for me was to set let g:is_posix = 1 in my config. Interestingly, if you look through Vim's runtime files, it's equivalent to setting let g:is_kornshell = 1.
A brief interesting history on how the Bourne shell was bourne, bourne again as bash as a substitute for /bin/sh on Ubuntu, and eventually replaced in favor of dash can be found at https://askubuntu.com/a/976504.

Resources