37

I want to try simple script

flag=false
while !$flag
do
   read x
   if [ "$x" -eq "true" ]
   then
     flag=true
   fi
   echo "${x} : ${flag}"
done

But when I run it, if I type true, I will see that x="true" and flag="true", but the cycle doesn't end. What is wrong with the script? How can I properly invert a boolean variable?

2
  • 1
    See stackoverflow.com/questions/8108249/…
    – ata
    Commented Nov 12, 2011 at 23:19
  • It is very dangerious to execute a variable. First ensure your script sets it. and second ensure that the commands 'true' and 'false' really produce a true and false exist status. Someone may pass your script a 'exported function' call 'true'!
    – anthony
    Commented Aug 15, 2023 at 2:45

8 Answers 8

28

There are two errors in your script. The first is that you need a space between ! and $flag, otherwise the shell looks for a command called !$flag. The second error is that -eq is for integer comparisons, but you're using it on a string. Depending on your shell, either you'll see an error message and the loop will continue forever because the condition [ "$x" -eq "true" ] cannot be true, or every non-integer value will be treated as 0 and the loop will exit if you enter any string (including false) other than a number different from 0.

While ! $flag is correct, it's a bad idea to treat a string as a command. It would work, but it would be very sensitive to changes in your script, since you'd need to make sure that $flag can never be anything but true or false. It would be better to use a string comparison here, like in the test below.

flag=false
while [ "$flag" != "true" ]
do
   read x
   if [ "$x" = "true" ]
   then
     flag=true
   fi
   echo "${x} : ${flag}"
done

There's probably a better way to express the logic you're after. For example, you could make an infinite loop and break it when you detect the termination condition.

while true; do
  read -r x
  if [ "$x" = "true" ]; then break; fi
  echo "$x: false"
done
1
  • With the [ builtin of ksh, [ x -eq y ] return true if the x arithmetic expression resolves to the same number as the y arithmetic expression so for instance x=2 true=1+1 ksh -c '[ x -eq true ] && echo yes' would output yes. Commented Aug 21, 2019 at 19:51
15

While there are no Boolean variables in Bash, it is very easy to emulate them using arithmetic evaluation.

flag= # False
flag=0 # False
flag=1 # True (actually any integer number != 0 will do, but see remark below about toggling)
flag="some string" # Maybe False (make sure that the string isn't interpreted as a number)

if ((flag)) # Test for True
then
  : # do something
fi

if ! ((flag)) # Test for False
then
  : # do something
fi

flag=$((1-flag)) # Toggle flag (only works when flag is either empty or unset, 0, 1, or a string which doesn't represent a number)

This also works in ksh. I wouldn't be surprised if it works in all POSIX-compliant shells, but haven't checked the standard.

3
  • 1
    Does ! ! ((val)) work in Bash, like in C or C++? For example, if val is 0 it remains 0 after ! !. If val is 1 it remains 1 after ! !. If val is 8 it is pulled down to 1 after ! !. Or do I need to ask another question?
    – user56041
    Commented Jul 21, 2016 at 9:07
  • 2
    -1 | In POSIX sh, standalone ((..)) is undefined; please remove the reference to it Commented Jan 29, 2018 at 0:57
  • 1
    Vlastimil the question specifically is about bash - not posix shell. Why down vote when the question isn't about that topic?
    – Nick Bull
    Commented Sep 10, 2018 at 22:57
15

If you can be absolutely sure that the variable will contain either 0 or 1, you can use the bitwise XOR-equal operator to flip between the two values:

$ foo=0
$ echo $foo
0
$ ((foo ^= 1))
$ echo $foo
1
$ ((foo ^= 1))
$echo $foo
0
2
  • Very nice. Works only with a variable. i.e. echo $((1 ^= 1)) didn't work.
    – Artfaith
    Commented Jun 5, 2021 at 4:15
  • 1
    @Artfaith: That's because of the ^= assignment operator. The expression (( foo ^= 1 )) is equivalent to (( foo = foo ^ 1 )). Your example of ((1 ^= 1)) expands to (( 1 = 1 ^ 1 )) which fails because you can't change the value of 1. To make your example work, just use the regular XOR operator ^ instead of assignment, e.g.: echo $(( 1 ^ 1 )).
    – macserv
    Commented Feb 8 at 15:55
3

This works for me:

flag=false
while [[ "$flag" == "false" ]]
do
   read x
   if [[ "$x" == "true" ]]
   then
     flag=true
   fi
   echo "${x} : ${flag}"
done

But, actually, what you should do is replace your while with:

while ! $flag
1

There is no concept of a boolean variable in the shell.
Shell variables could only be text (an string), and, in some cases, that text may be interpreted as an integer (1, 0xa, 010, etc. ).

Therefore, a flag=true implies no truthfulness or falseness to the shell at all.

String

What could be done is either a string comparison [ "$flag" == "true" ] or use the variable content in some command and check its consequences, like execute true (because there are both an executable called true and one called false) as a command and check if the exit code of that command is zero (successful).

$flag; if [ "$?" -eq 0 ]; then ... fi

Or shorter:

if "$flag"; then ... fi

If the content of a variable is used as a command, a ! could be used to negate the exit status of the command, if an space exists between both (! cmd), as in:

if ! "$flag"; then ... fi

The script should change to:

flag=false
while ! "$flag" 
do
   read x
   if [ "$x" == "true" ]
   then
     flag=true
   fi
   echo "${x} : ${flag}"
done

Integer

Use numeric values and Arithmetic Expansions.

In this case, the exit code of $((0)) is 1 and the exit code of $((1)) is 0.
In bash, ksh and zsh the arithmetic could be carried out inside a ((..)) (note that the starting $ is missing).

flag=0; if ((flag)); then ... fi

A portable version of this code is more convoluted:

flag=0; if [ "$((flag))" -eq 0 ]; then ... fi      # test for a number
flag=0; if [ "$((flag))"  == 0 ]; then ... fi      # test for the string "0"

In bash/ksh/zsh you could do:

flag=0    
while ((!flag)) 
do
   read x
   [ "$x" == "true" ] && flag=1
   echo "${x} : ${flag}"
done

Alternatively

You can "Invert a boolean variable" (provided it contains a numeric value) as:

((flag=!flag))

That will change the value of flag to either 0 or 1.


Note: Please check for errors in https://www.shellcheck.net/ before posting your code as a question, many times that is enough to find the problem.

1

until is short for while ! (and until, contrary to ! is Bourne), so you can do:

flag=false
until "$flag"
do
   IFS= read -r x
   if [ "$x" = true ]
   then
     flag=true
   fi
   printf '%s\n' "$x : $flag"
done

Which should be the same as:

flag=false
while ! "$flag"
do
   IFS= read -r x
   if [ "$x" = true ]
   then
     flag=true
   fi
   printf '%s\n' "$x : $flag"
done

You could also write it as:

until
   IFS= read -r x
   printf '%s\n' "$x"
   [ "$x" = true ]
do
   continue
done

The part between the until/while and do doesn't have to be a single command.

! is a keyword in the POSIX shell syntax. It needs to be delimited, a token separated from what follows and be the first token that preceded a pipeline (! foo | bar negates the pipeline and foo | ! bar is invalid though some shells accept it as an extension).

!true is interpreted as the !true command which is unlikely to exist. ! true should work. !(true) should work in POSIX-compliant shells as that ! is delimited as it's followed by a ( token, but in practice it doesn't work in ksh / bash -O extglob where it conflicts with the !(pattern) extended glob operator nor zsh (except in sh emulation) where it conflicts with glob(qualifiers) with bareglobqual and glob(group) without.

0
[ ${FOO:-0} == 0 ] && FOO=1 || FOO=0

or even:

[ ${FOO:-false} == false ] && FOO=true || FOO=false

should do the job

2
  • 2
    You don't need the brackets: ${FOO:-false} && FOO=false || FOO=true flips it. (This works because true and false are built-ins.)
    – ingyhere
    Commented May 22, 2020 at 22:35
  • BUt executing a variable can be 'unsafe' especially if the variable was set by a environment variable outside the script! You best ensure variable initially set by script.
    – anthony
    Commented Aug 15, 2023 at 2:40
0

You can use this function:

function invert_boolean() {
  if [[ "$1" == "true" ]]; then
    echo "false";
  else
    echo "true";
  fi;
}

And use it like this:

VAR=false;
INVERTED=$(invert_boolean $VAR);

echo "INVERTED: $INVERTED";
# prints INVERTED: true

This works for both strings (eg. "true") and booleans (eg. false).

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .