Shell can run the command but ZSH can't - bash

Inside my project folder if I use Bash and run . ./setantenv.sh, it works.
But in the same folder if I use ZSH and try to run . ./setantenv.sh, it results in ./setantenv.sh:4: = not found
ZSH doesn't have any issues to run all the other commands. But it seems that it doesn't recognise the first . before the ./AnyCommand.sh
#!/bin/bash
OWN_NAME=setantenv.sh
if [ "$0" == "./$OWN_NAME" ]; then
echo * Please call as ". ./$OWN_NAME", not ./$OWN_NAME !!!---
echo * Also please DO NOT set back the executable attribute
echo * On this file. It was cleared on purpose.
chmod -x ./$OWN_NAME
exit
fi
PLATFORM_HOME=`pwd`
export -p PLATFORM_HOME
export -p ANT_OPTS="-Xmx400m -XX:MaxPermSize=128M"
export -p ANT_HOME=$PLATFORM_HOME/apache-ant-1.9.1
chmod +x "$ANT_HOME/bin/ant"
export -p PATH=$ANT_HOME/bin:$PATH
echo Setting ant home to: $ANT_HOME
ant -version
Any help?

if [ "$0" "==" "./$OWN_NAME" ]; then
or
if [ "$0" = "./$OWN_NAME" ]; then
rewrite like this when runs zsh.

this is the latest POSIX specification for test and its alias, [. as you can see, there's no == in the whole page.
freebsd test(1) man page says:
COMPATIBILITY
For compatibility with some other implementations, the = primary
can be substituted with == with the same meaning.
but zsh has [ builtin:
% for sh in sh bash mksh zsh; do $sh -c "printf '%-5s- ' $sh; which '['"; done
sh - /bin/[
bash - /bin/[
mksh - /bin/[
zsh - [: shell built-in command
... and zsh's version does not do ==. from zshbuiltins(1) (only the outer brackets on the second synopsis line are actual syntax, the rest is *BNF):
test [ arg ... ]
[ [ arg ... ] ]
Like the system version of test. Added for compatibility; use
conditional expressions instead (see the section `Conditional
Expressions'). The main differences between the conditional
expression syntax and the test and [ builtins are: these
commands are not handled syntactically, so for example an empty
variable expansion may cause an argument to be omitted; syntax
errors cause status 2 to be returned instead of a shell error;
and arithmetic operators expect integer arguments rather than
arithmetic expressions.
The command attempts to implement POSIX and its extensions where
these are specified. Unfortunately there are intrinsic
ambiguities in the syntax; in particular there is no distinction
between test operators and strings that resemble them. The
standard attempts to resolve these for small numbers of
arguments (up to four); for five or more arguments compatibility
cannot be relied on. Users are urged wherever possible to use
the `[[' test syntax which does not have these ambiguities.
btw, the error message you got says = not found because zsh uses leading = for filename expansion. from zshexpn(1):
'=' expansion
If a word begins with an unquoted '=' and the EQUALS option is set, the
remainder of the word is taken as the name of a command. If a command
exists by that name, the word is replaced by the full pathname of the
command.
apparently, there's no command named = anywhere in your $PATH. :)
% echo =ls
/bin/ls
% echo =fubar
zsh: fubar not found
% echo ==
zsh: = not found

Related

Bash to zsh actual syntax differences, such as brackets in 'if' [duplicate]

The zsh_test.sh is simple, as following:
#!/usr/bin/env zsh
if [ $USER == 'root' ]; then
echo "root"
else
echo "not root"
fi
Copy and paste the above codes into a zsh shell, it executed well.
$ #!/usr/bin/env zsh
$ if [ $USER == 'root' ]; then
then> echo "root"
then> else
else> echo "not root"
else> fi
not root
But directly execute script file zsh_test.sh, got an error.
$ ./zsh_test.sh
./zsh_test.sh:3: = not found
I now see what's wrong: You are the victim of a fairly obscure zsh mechanism, which is described in the zshexpn man page and is called '=' expansion. From the man-page:
If a word begins with an unquoted `=' and the EQUALS option is set, the remainder of the word is taken as the name of a command. If a command exists by that name, the word is replaced by the full pathname of the command.
You can try it with the command
echo ==
which also outputs this error message. For instance, on my platofm
echo =ruby
outputs /usr/bin/ruby, because this is where I have ruby installed. If you would have in your PATH a program named =, the == would resolve to this path.
While it is unusual to use a double == sign inside [ ... ], the zsh implementation of this command allows it, but you would have to quote the operator, to avoid =-expansion:
if [ $USER '==' root ]; then
An alternative would be to use [[ ... ]] instead. This is not a command, but a syntactic construct, and expansion rules are different inside it. Therefore
if [[ $USER == root ]]; then
would work as well.
I'm afraid that you are using test command wrong. Let's see why.
test command is defined since Unix version III. You can often find this command also as [ binary in your PATH. In most modern shells (let's pretend that bash is modern shell as well), there is also implementation of test or [ as builtin command. From the specification, the only valid way to compare two strings is this:
STRING1 = STRING2
the strings are equal
STRING1 != STRING2
the strings are not equal
Original strict POSIX implementation of test command is somehow limited and can be difficult to use. But it is portable, and that it's main strength. But what if you don't care about portability at all? Then there are Conditional Expressions.
Conditional Expressions, available as [[ builtin command, are improved, not POSIX compatible replacement for original test command. Look in the manual for the things you can compare with them to get the idea. Double equality sign (==) is also supported (and the documentation explicitly says it's for compatibility with other sorts of computer language.)
Conclusion?
When you are writing scripts for particular shell, like zsh, and you are absolutely sure that portability is not important for you, always use [[ instead of [. Your life will be easier and change to your script is minimal:
#!/usr/bin/env zsh
if [[ $USER == 'root' ]]; then
echo "root"
else
echo "not root"
fi
If portability between different shells and environments is necessary, you will have to use original test or [ command, and forget about zsh, == and many other things at all.
#!/bin/sh
if [ "$USER" = 'root' ]; then
printf '%s\n' "root"
else
printf '%s\n' "not root"
fi

Why the zsh codes like [ $var == 'str' ] runs well as a command but error as a script file?

The zsh_test.sh is simple, as following:
#!/usr/bin/env zsh
if [ $USER == 'root' ]; then
echo "root"
else
echo "not root"
fi
Copy and paste the above codes into a zsh shell, it executed well.
$ #!/usr/bin/env zsh
$ if [ $USER == 'root' ]; then
then> echo "root"
then> else
else> echo "not root"
else> fi
not root
But directly execute script file zsh_test.sh, got an error.
$ ./zsh_test.sh
./zsh_test.sh:3: = not found
I now see what's wrong: You are the victim of a fairly obscure zsh mechanism, which is described in the zshexpn man page and is called '=' expansion. From the man-page:
If a word begins with an unquoted `=' and the EQUALS option is set, the remainder of the word is taken as the name of a command. If a command exists by that name, the word is replaced by the full pathname of the command.
You can try it with the command
echo ==
which also outputs this error message. For instance, on my platofm
echo =ruby
outputs /usr/bin/ruby, because this is where I have ruby installed. If you would have in your PATH a program named =, the == would resolve to this path.
While it is unusual to use a double == sign inside [ ... ], the zsh implementation of this command allows it, but you would have to quote the operator, to avoid =-expansion:
if [ $USER '==' root ]; then
An alternative would be to use [[ ... ]] instead. This is not a command, but a syntactic construct, and expansion rules are different inside it. Therefore
if [[ $USER == root ]]; then
would work as well.
I'm afraid that you are using test command wrong. Let's see why.
test command is defined since Unix version III. You can often find this command also as [ binary in your PATH. In most modern shells (let's pretend that bash is modern shell as well), there is also implementation of test or [ as builtin command. From the specification, the only valid way to compare two strings is this:
STRING1 = STRING2
the strings are equal
STRING1 != STRING2
the strings are not equal
Original strict POSIX implementation of test command is somehow limited and can be difficult to use. But it is portable, and that it's main strength. But what if you don't care about portability at all? Then there are Conditional Expressions.
Conditional Expressions, available as [[ builtin command, are improved, not POSIX compatible replacement for original test command. Look in the manual for the things you can compare with them to get the idea. Double equality sign (==) is also supported (and the documentation explicitly says it's for compatibility with other sorts of computer language.)
Conclusion?
When you are writing scripts for particular shell, like zsh, and you are absolutely sure that portability is not important for you, always use [[ instead of [. Your life will be easier and change to your script is minimal:
#!/usr/bin/env zsh
if [[ $USER == 'root' ]]; then
echo "root"
else
echo "not root"
fi
If portability between different shells and environments is necessary, you will have to use original test or [ command, and forget about zsh, == and many other things at all.
#!/bin/sh
if [ "$USER" = 'root' ]; then
printf '%s\n' "root"
else
printf '%s\n' "not root"
fi

Bash Script has unexpected behavior if run with absolut path

So we have this script that is supposed to change the IP of a linux machine based on user input. This user input has to be validated.
If the script is run inside the directory in which it lays, everything works as expected, but as soon as it's run with an absolute path, it seems to break on some points.
I already tried to use the debug option set -x but the output stays almost the same.
read -p "Please enter the netmask (CIDR format): " netmask
if [ ! $(echo "$netmask" | egrep "^([12]?[0-9]?)$") ];
then
subnetok=0
fi
if [ "$subnetok" == "0" ];
then
echo -e "\033[41m\033[39m Subnetmask is invalid!\033[0m"
sleep 1
return 1
fi
This is the debug output if the script is run inside the directory:
++ echo 24
++ egrep '^([12]?[0-9]?)$'
+ '[' '!' 24 ']'
+ '[' '' == 0 ']'
and this is the debug output if the script is run with an absolute path
+++ echo 24
+++ egrep --color=auto '^([12]?[0-9]?)$'
++ '[' '!' 24 ']'
++ '[' 0 == 0 ']'
++ echo -e 'Subnetmask is invalid'
I expect the output to be the same with the same numbers
When you run the script with this:
. /usr/local/script/script.sh
This uses the . command, which runs the script in the current shell (equivalent to source). That is, it runs it in your interactive shell rather than forking a new shell to run it. See: What is the difference between ./somescript.sh and . ./somescript.sh
This has (at least) two effects:
The current shell is interactive, and apparently has an alias defined for egrep, which makes things a little weird. Not really a problem, just weird.
The current shell apparently already has a definition for the variable subnetok, and it's "0". It's probably left over from a previous time you ran the script this way. This is what's causing the problem.
The primary solution is that the script needs to explicitly initialize subnetok rather than just assuming that it's undefined:
subnetok=1
if ...
Alternately, if you don't need the variable for anything else, you could just skip it and handle the condition immediately:
if [ ! $(echo "$netmask" | egrep "^([12]?[0-9]?)$") ]; # See below for alternatives
then
echo -e "\033[41m\033[39m Subnetmask is invalid!\033[0m"
...
Other recommendations:
Run the script without the .:
/usr/local/script/script.sh
Give the script a proper shebang line (if it doesn't already have one) that specifies the bash shell (i.e. #!/bin/bash or #!/usr/bin/env bash).
Use a better method to check the subnet for validity, like:
if ! echo "$netmask" | grep -Eq "^[12]?[0-9]?$"
or (bash only):
if ! [[ "$netmask" =~ ^[12]?[0-9]?$ ]]
Don't use echo -e, as it's not portable (even between different versions of the same OS, modes of the shell, etc). Use printf instead (and I'd recommend single-quotes for strings that contain backslashes, because in some cases they'll get pre-parsed when in double-quotes):
printf '\033[41m\033[39m Subnetmask is invalid!\033[0m'
Note that printf is not a drop-in replacement for echo -e, it's considerably more complicated when you're using variables and/or multiple arguments. Read the man page.
Comment:
By running with an absolute path I mean . /usr/local/script/script.sh instead of cd into /usr/local/script/ and then ./script.sh
The difference is that in one case you are executing the script and in another case you are sourcing the script. See What is the difference between executing a Bash script vs sourcing it? for more information.
When you are running ./script.sh without a space between the dot and the slash you are executing the script in a new shell. When you are running . /usr/local/script/script.sh you are sourcing the script in the current shell. This can have implications if you have for example an alias set in your current shell that would not be present in a new shell, such as alias egrep='egrep --color=auto'. That's why there is a difference.
From the linked question:
Both sourcing and executing the script will run the commands in the script line by line, as if you typed those commands by hand line by line.
The differences are:
When you execute the script you are opening a new shell, type the commands in the new shell, copy the output back to your current shell, then close the new shell. Any changes to environment will take effect only in the new shell and will be lost once the new shell is closed.
When you source the script you are typing the commands in your current shell. Any changes to the environment will take effect and stay in your current shell.
Use source if you want the script to change the environment in your currently running shell. use execute otherwise.

Default test expression behaves different in zsh vs bash - why?

Here is a simple test case script which behaves differently in zsh vs bash when I run with $ source test_script.sh from the command line. I don't necessarily know why there is a difference if my shebang clearly states that I want bash to run my script other than the fact that the which command is a built-in in zsh and a program in bash. (FYI - the shebang directory is where my bash program lives which may not be the same as yours--I installed a new version using homebrew)
#!/usr/local/bin/bash
if [ "$(which ls)" ]; then
echo "ls command found"
else
echo "ls command not found"
fi
if [ "$(which foo)" ]; then
echo "foo command found"
else
echo "foo command not found"
I run this script with source ./test-script.sh from zsh and Bash.
Output in zsh:
ls command found
foo command found
Output in bash:
ls command found
foo command not found
My understanding is that default for test or [ ] (which are the same thing) evaluate a string to true if it's not empty/null. To illustrate:
zsh:
$ which foo
foo not found
bash:
$ which foo
$
Moreover if I redirect standard error in zsh like:
$ which foo 2> /dev/null
foo not found
zsh still seems to send foo not found to standard output which is why (I am guessing) my test case passed for both under the zshell; because the expansion of "$(which xxx)" returned a string in both cases (e.g. /some/directory and foo not found (zsh will ALWAYS return a string?).
Lastly, if I remove the double quotes (e.g. $(which xxx)), zsh gives me an error. Here is the output:
ls command found
test_scritp.sh:27: condition expected not:
I am guessing zsh wanted me to use [ ! "$(which xxx)" ]. I don't understand why? It never gave that error when running in bash (and isn't this supposed to run in bash anyway?!).
Why isn't my script using bash? Why is something so trivial as this not working? I understand how to make it work fine in both using the -e option but I simply want to understand why this is all happening. Its driving me bonkers.
There are two separate problems here.
First, the proper command to use is type, not which. Like you note, the command which is a zsh built-in, whereas in Bash, it will execute whatever which command happens to be on your system. There are many variants with different behaviors, which is why POSIX opted to introduce a replacement instead of trying to prescribe a particular behavior for which -- then there would be yet one more possible behavior, and no way to easily root out all the other legacy behaviors. (One early common problem was with a which command which would examine the csh environment, even if you actually used a different shell.)
Secondly, examining a command's string output is a serious antipattern, because strings differ between locales ("not found" vs. "nicht gefunden" vs. "ei löytynyt" vs. etc etc) and program versions -- the proper solution is to examine the command's exit code.
if type ls >/dev/null 2>&1; then
echo "ls command found"
else
echo "ls command not found"
fi
if type foo >/dev/null 2>&1; then
echo "foo command found"
else
echo "foo command not found"
fi
(A related antipattern is to examine $? explicitly. There is very rarely any need to do this, as it is done naturally and transparently by the shell's flow control statements, like if and while.)
Regarding quoting, the shell performs whitespace tokenization and wildcard expansion on unquoted values, so if $string is command not found, the expression
[ $string ]
without quotes around the value evaluates to
[ command not found ]
which looks to the shell like the string "command" followed by some cruft which isn't syntactically valid.
Lastly, as we uncovered in the chat session (linked from comments) the OP was confused about the precise meaning of source, and ended up running a Bash script in a separate process instead. (./test-script instead of source ./test-script). For the record, when you source a file, you cause your current shell to read and execute it; in this setting, the script's shebang line is simply a comment, and is completely ignored by the shell.

How to recognize whether bash or dash is being used within a script?

I'm writing a bash script and it throws an error when using "sh" command in Ubuntu (it seems it's not compatible with dash, I'm learning on this subject). So I would like to detect if dash is being used instead of bash to throw an error.
How can I detect it in a script context?. Is it even possible?
You can check for the presence of shell-specific variables:
For instance, bash defines $BASH_VERSION.
Since that variable won't be defined while running in dash, you can use it to make the distinction:
[ -n "$BASH_VERSION" ] && isBash=1
Afterthought: If you wanted to avoid relying on variables (which, conceivably, could be set incorrectly), you could try to obtain the ultimate name of the shell executable running your script, by determining the invoking executable and, if it is a symlink, following it to its (ultimate) target.
The shell function getTrueShellExeName() below does that; for instance, it would return 'dash' on Ubuntu for a script run with sh (whether explicitly or via shebang #!/bin/sh), because sh is symlinked to dash there.
Note that the function's goal is twofold:
Be portable:
Work with all POSIX-compatible (Bourne-like) shells,
across at least most platforms, with respect to what utilities and options are used - see caveats below.
Work in all invocation scenarios:
sourced (whether from a login shell or not)
executed stand-alone, via the shebang line
executed by being passed as a filename argument to a shell executable
executed by having its contents piped via stdin to a shell executable
Caveats:
On at least one platform - macOS - sh is NOT a symlink, even though it is effectively bash. There, the function would return 'sh' in a script run with sh.
The function uses readlink, which, while not mandated by POSIX, is present on most modern platforms - though with differing syntax and features. Therefore, using GNU readlink's -f option to find a symlink's ultimate target is not an option.
(The only modern platform I'm personally aware of that does not have a readlink utility is HP-UX - see https://stackoverflow.com/a/24114056/45375 for a recursive-readlink implementation that should work on all POSIX platforms.)
The function uses the which utility (except in zsh, where it's a builtin), which, while not mandated by POSIX, is present on most modern platforms.
Ideally, ps -p $$ -o comm= would be sufficient to determine the path of the executable underlying the process, but that doesn't work as intended when directly executing shell scripts with shebang lines on Linux, at least when using the ps implementation from the procps-ng package, as found on Ubuntu, for instance: there, such scripts report the script's file name rather than the underlying script engine's.Tip of the hat to ferdymercury for his help.
Therefore, the content of special file /proc/$$/cmdline is parsed on Linux, whose first NUL-separated field contains the true executable path.
Example use of the function:
[ "$(getTrueShellExeName)" = 'bash' ] && isBash=1
Shell function getTrueShellExeName():
getTrueShellExeName() {
local trueExe nextTarget 2>/dev/null # ignore error in shells without `local`
# Determine the shell executable filename.
if [ -r /proc/$$/cmdline ]; then
trueExe=$(cut -d '' -f1 /proc/$$/cmdline) || return 1
else
trueExe=$(ps -p $$ -o comm=) || return 1
fi
# Strip a leading "-", as added e.g. by macOS for login shells.
[ "${trueExe#-}" = "$trueExe" ] || trueExe=${trueExe#-}
# Determine full executable path.
[ "${trueExe#/}" != "$trueExe" ] || trueExe=$([ -n "$ZSH_VERSION" ] && which -p "$trueExe" || which "$trueExe")
# If the executable is a symlink, resolve it to its *ultimate*
# target.
while nextTarget=$(readlink "$trueExe"); do trueExe=$nextTarget; done
# Output the executable name only.
printf '%s\n' "$(basename "$trueExe")"
}
Use $0 (that is the name of the executable of the shell being called).The command for example
echo $0
gives
/usr/bin/dash
for the dash and
/bin/bash
for a bash.The parameter substitution
${0##*/}
gives just 'dash' or 'bash'. This can be used in a test.
An alternative approach might be to test if a shell feature is available, for example to give an idea...
[[ 1 ]] 2>/dev/null && echo could be bash || echo not bash, maybe dash
echo $0 and [[ 1 ]] 2>/dev/null && echo
could be bash || echo not bash, maybe bash worked for me running Ubuntu 19.
Done slight Pascal, Fortran and C in school, but need to become fluent in shell script.

Resources