Why am I getting a 'unary operator expected' error? - bash

I'm writing a shell script to streamline my development workflow.
It takes an argument as to which theme folder I'm going to be working in and starts grunt watch on that directory.
If I call the script without the necessary argument I'm currently printing a warning that a theme needs to be specified as a command line argument.
I'd like to print a list of the available options, e.g. theme directories
This is what I have so far...
THEME=$1
if [ $THEME == '' ]
then
echo 'Need to specify theme'
else
cd 'workspace/aws/ghost/'$THEME'/'
grunt watch
fi
Ideally I'd replace the output of the echo line with an ls of the themes parent directory like so
THEME=$1
if [ $THEME == '' ]
then
echo 'Need to specify theme from the following'
ls workspace/aws/ghost
else
cd 'workspace/aws/ghost/'$THEME'/'
grunt watch
fi
However this gives me the following error
./ghost_dev.sh: line 3: [: ==: unary operator expected

You need quotes around $THEME here:
if [ $THEME == '' ]
Otherwise, when you don't specify a theme, $THEME expands to nothing, and the shell sees this syntax error:
if [ == '' ]
With quotes added, like so:
if [ "$THEME" == '' ]
the expansion of an empty $THEMEyields this valid comparison instead:
if [ "" == '' ]
This capacity for runtime syntax errors can be surprising to those whose background is in more traditional programming languages, but command shells (at least those in the Bourne tradition) parse code somewhat differently. In many contexts, shell parameters behave more like macros than variables; this behavior provides flexibility, but also creates traps for the unwary.
Since you tagged this question bash, it's worth noting that there is no word-splitting performed on the result of parameter expansion inside the "new" test syntax available in bash (and ksh/zsh), namely [[...]]. So you can also do this:
if [[ $THEME == '' ]]
The places you can get away without quotes are listed here. But it's a fine habit to always quote parameter expansions anyway except when you explicitly want word-splitting (and even then, look to see if arrays will solve your problem instead).
It would be more idiomatic to use the -z test operator instead of equality with the empty string:
if [ -z "$THEME" ]
You technically don't need the quotation marks in this simple case; [ -z ] evaluates to true. But if you have a more complicated expression, the parser will get confused, so it's better to just always use the quotes. Of course, [[...]] doesn't require any here, either:
if [[ -z $THEME ]]
But [[...]] is not part of the POSIX standard; for that matter, neither is ==. So if you care about strict compatibility with other POSIX shells, stick to the quoting solution and use either -z or a single =.

[ "$THEME" ] will evaluate to false if $THEME is undefined or an empty string and true otherwise. See http://www.gnu.org/software/bash/manual/html_node/Bash-Conditional-Expressions.html#Bash-Conditional-Expressions. You can rearrange your if statement to exploit this behavior and have an even simpler conditional:
if [ "$THEME" ]; then
cd 'workspace/aws/ghost/'$THEME'/'
grunt watch
else
echo 'Need to specify theme from the following'
ls workspace/aws/ghost
fi
"$THEME" needs to be in double-quotes, in case its value contains whitespace.

Please correct the syntax with the double quotes.
if [ "$THEME" == "" ]; then
echo 'Need to specify theme from the following'
ls workspace/aws/ghost
fi

Related

Conditional ZSH_THEME by terminal

I'm trying to make that when .zshrc when invoked chooses a specific theme based on which terminal amulator i'm running. For this I came up with:
current_terminal="$(ps -p$PPID -o cmd=)"
function choose_theme {
if [ $current_terminal=~'tilix' ];
then echo 'powerlevel9k/powerlevel9k';
else echo 'robbyrussell';
fi
}
ZSH_THEME="$(choose_theme)"
I don't get any error message when running and when I open on tilix it works just fine with the powerlevel9k theme, but just that, it doesn't seem to respect the condition and I don't know where might my mstake be =/
The output for the variable current_terminal in each terminal emulator i'm using are:
Tilix:
/usr/bin/tilix --gapplication-service
Default Terminal:
/usr/lib/gnome-terminal/gnome-terminal-server
So it's getting things wright, but setting up always the first option for some reason
This does not work due to two reasons:
You are using [ ... ] instead of [[ ... ]] for the check. The difference is that [[ ... ]] is part of the ZSH's syntax and [ (aka test) is a built-in command that tries to emulate the external test program. This matters because [ does not support the =~ operator, it is only available inside [[ ... ]].
The =~ (like any other operator) needs to be surrounded by white spaces. As a Unix shell ZSH tokenizes command on white spaces. In this case ZSH I would guess that ZSH only checks whether $current_terminal=~'tilix' evaluates to a non-empty string instead of comparing $current_terminal to 'tilix'. This is always the case, hence why you always get the powerlevel9k theme.
So the condition should look like this:
current_terminal="$(ps -p$PPID -o cmd=)"
function choose_theme {
if [[ $current_terminal =~ 'tilix' ]];
then echo 'powerlevel9k/powerlevel9k';
else echo 'robbyrussell';
fi
}
ZSH_THEME="$(choose_theme)"

Declaration of an array valid for zsh and bash

I am looking for a way to declare an array that will work in bash and zsh.
I know I can do simply this in bash:
file_list=$(example_command)
And in zsh I can make it work like this:
file_list=($(example_command))
I know I can just do it with an if statement if I want to, but hope to do it without:
if [ `basename $SHELL`=bash ]; then
file_list=$(example_command)
elif [ `basename $SHELL`=zsh ]; then
file_list=($(example_command))
else
echo "ERROR: UNKNOWN SHELL"
fi
I do not really care about other shells (eg sh). Just looking for something working in zsh and bash which is not too exotic looking.
Your assumptions about bash are wrong. file_list=$(example_command) does not create an array there. file_list=( $(example_command) ) does create an array in both shells, though it's not good practice to do so (can't deal with files with spaces, files whose names could be expanded as a glob, etc).
The following is a good-practice approach (insofar as you can call handling filenames in a newline-delimited stream good-practice when newlines can be literals in filenames) that works in both shells:
file_list=( )
while IFS= read -r line; do file_list+=( "$line" ); done < <(example_command)
Importantly, to use an array in a way that works in both shells, you need to expand it as "${file_list[#]}", not $file_list.

Understanding indirection

I'm currently working on cleaning up some shell scripts. While doing that, I came across something that looks like this:
if [ ${#VARA} -eq 0 ] || [ ${#$VARB} -eq 0 ] || [ ${$VARC} -eq 0 ]; then
...
fi
As you can see, there are three different types of things going on here with variables: ${#V}, ${#$V}, and ${$V}. I would love an explanation of each of these please. (Plus Shellcheck is complaining about the last one.)
This syntax is part of POSIX (thence Bash) shell parameter expansion. It counts number of characters for a variable :
$ myvar="foo" && echo ${#myvar}
3
The last two ones ${#$VARB} and ${$VARC} have not a valid syntax. You cannot declare a variable this way.
Quite similar to your ${$VARC} is the valid \$$myvar, used for indirect reference with eval or echo. This syntax refers to the literal string "$foo" (when myvar="foo"). As #chepner mentionned, because of eval, it's not recommended to use it. The Bash 2.0 introduced the ${!myvar} syntax for indirect variable reference that is a much preferable alternative.
Note : you should avoid uppercase for variable names not to confuse with shell variables which are also uppercased by convention.

Is [ $var ] an acceptable way to check for variables set/empty in bash?

I wrote some bash before reading this popular question. I noticed the way I was doing things doesn't show up in any of the answers so I was curious if there is a danger/bug to my logic that I am not seeing.
if [ ! $1 ] || [ $2 ]; then
echo "Usage: only one var." && exit 1
fi
As you can see, I am testing to see if there is one and only one parameter given on the command line. As far as I have tested, this works. Can someone pass along some more knowledge to a new bash user?
./script one '' oops
[ $2 ] will be false when $2 is the empty string.
And as that other guy points out in a comment, bash will split both strings, opening up a range of issues, including potential security holes.
This is clearer:
if [ $# != 1 ] ; then
echo "Usage: only one var."; exit 1
fi
Things that will break your test:
The first parameter is empty ("")
The second parameter is empty ("")
The first or second parameter has more words ("bla bla")
The parameters contain something that will be interpreted by test (e.g.: -z)
It is simply less clearer than using $#. Also the mere fact that I have to think about all the corner-cases which could potentially break the code makes it inferior.

String contains in Bash that is a directory path

I am writing an SVN script that will export only changed files. In doing so I only want to export the files if they don't contain a specific file.
So, to start out I am modifying the script found here.
I found a way to check if a string contains using the functionality found here.
Now, when I try to run the following:
filename=`echo "$line" |sed "s|$repository||g"`
if [ ! -d $target_directory$filename ] && [[!"$filename" =~ *myfile* ]] ; then
fi
However I keep getting errors stating:
/home/home/myfile: "no such file or directory"
It appears that BASH is treating $filename as a literal. How do I get it so that it reads it as a string and not a path?
Thanks for your help!
You have some syntax issues (a shell script linter can weed those out):
You need a space after "[[", otherwise it'll be interpretted as a command (giving an error similar to what you posted).
You need a space after the "!", otherwise it'll be considered part of the operand.
You also need something in the then clause, but since you managed to run it, I'll assume you just left it out.
You combined two difference answers from the substring thing you posted, [[ $foo == *bar* ]] and [[ $foo =~ .*bar.* ]]. The first uses a glob, the second uses a regex. Just use [[ ! $filename == *myfile* ]]

Resources