12

I have a script that fails to detect zero length strings,the script uses [ -n $value ] in a bash conditional expression, i.e.

#!/usr/bin/env bash

value=""
if [ -n $value ]
then
    echo "value is non-zero"
fi

result

value is non-zero

If I use [[ -n $value ]] it works, i.e.

#!/usr/bin/env bash

value=""
if [[ -n $value ]]
then
    echo "value is non-zero"
fi

using [[ produces no output as expected. From the man page:

   [[ expression ]]
      Return a status of 0 or 1 depending on the evaluation of the conditional expression expression.  Expressions are composed of the pri‐
      maries described below under CONDITIONAL EXPRESSIONS.  Word splitting and pathname expansion are not performed on the  words  between
      the  [[  and ]]; tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, process substitution,
      and quote removal are performed.  Conditional operators such as -f must be unquoted to be recognized as primaries.

I can't make out an explanation for the behaviour from this.
Why does [[ detect zero length strings but [ doesn't?

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255

2 Answers2

19

this is because [[ takes an expression, and [ takes arguments which it translates into an expression.

[[ is syntax - it isn't a builtin command as [ is, but rather [[ is a compound command and is more akin to { or ( than it is to [.

in any case, because [[ is parsed alongside $expansions, it has no difficulty understanding the difference between a null-valued expansion and a missing operand.

[, however, is a routine run after all command-line expansions have already taken place, and by the time it evaluates its expressions, $null_expansion has already been expanded away into nothing, and so all it receives is [ -n ], which may not be a valid expression. [ is spec'd to return true for a not-null single-argument case - as it does for -n here - but the very same spec goes on to say...

The two commands:

  test "$1"
  test ! "$1"

could not be used reliably on some historical systems. Unexpected results would occur if such a string expression were used and $1 expanded to !, (, or a known unary primary (such as -n). Better constructs are:

  test -n "$1"
  test -z "$1" 

there are upsides and downsides to both forms. [ expressions can be constructed out of expansions, and so:

[ "-${z:-n}" "$var" ]

...could be a perfectly valid way to build a test, but is not doable with:

[[ "-${z:-n}" "$var" ]]

...which is a nonsense command. The differences are entirely due to the command-line parse-point at which the test is run.

mikeserv
  • 58,310
  • when you say [[ is a compound command - is that because it performs multiple operations? e.g. does more work to make its use more user friendly? – the_velour_fog Nov 30 '15 at 04:52
  • 1
    @user4668401 [[ is not more user-friendly, it is more innate. [ and test were, historically, separate executable commands found in $PATH, and so shell [ builtins were designed to behave as an outside command would. [[ has never had that distinction, however, and so it has no need to behave as an outside executable might, and it can address names and information the shell possesses as if it is its own (because it is). – mikeserv Nov 30 '15 at 04:55
  • 2
    ah right! NOW I see . so the reason for all the quoting and expansion issues is because bash is literally outsourcing the task to another program – the_velour_fog Nov 30 '15 at 05:02
  • 2
    almost right. [ and test ARE external commands, but many shells also have them as built-ins for convenience and speed - but the built-in versions are bug-for-bug compatible so they don't break existing scripts. – cas Nov 30 '15 at 05:05
  • 1
    @user4668401 - not exactly. [ is a bash builtin as well, but it is a more separate routine than is [[. – mikeserv Nov 30 '15 at 05:05
  • 1
    @cas - not exactly. [ and test are external commands if you have installed them. historically, they always were, and had nothing to do with the shell at all. – mikeserv Nov 30 '15 at 05:07
  • Name one unix that doesn't have them as external commands. BTW, both /usr/bin/[ and /usr/bin/test are in GNU coreutils. – cas Nov 30 '15 at 05:09
  • 4
    right, but regardless of which version of [ is installed and used by bash - whenever you use [ you need to have the mindset of I'm calling another program so I need to handle parameter passing accordingly – the_velour_fog Nov 30 '15 at 05:12
  • @cas - at&t openast (which is the package in which you get ksh93) includes neither. – mikeserv Nov 30 '15 at 05:13
  • @user4668401 - that's exactly right. – mikeserv Nov 30 '15 at 05:14
  • AT&T's AST is, according to the web page, an open source software collection, not a unix in itself. The fact that one particular software collection doesn't include them proves nothing. – cas Nov 30 '15 at 05:18
  • @cas - it is as much a unix as is GNU coreutils. well, it is moreso in that its components are UNIX certified. in any case, are you through? – mikeserv Nov 30 '15 at 05:20
  • Ok thanks. I'm going to accept this answer because I think it addresses the more fundamental issue of why the behaviour occurs. Which seems to be that despite recent changes to test, [ and their implementations, their history is that of separate programs so you need to treat them as such when you pass parameters to them. – the_velour_fog Nov 30 '15 at 05:28
11

Variables don't have to be double-quoted in [[ ]] but they should be quoted for [ ].

Without quotes, if $value is an empty string, [ -n $value ] is exactly the same as [ "-n" ]. "-n" is a string test and evaluates as not-empty and thus true, so the test succeeds.

(why? because -n STRING and STRING are the same for [ aka test - they're both a test for string-is-not-empty)

Further experiments indicate that this seems to be the case for all single-operand tests, including file operators - if there is no operand, [ ] treats the single argument as a string test. This would probably be considered a bug and fixed if it wasn't a historical fact that script writers have come to depend upon over the decades - and changing it would break an uncountable number of existing scripts.

With quotes, [ -n "$value" ] is exactly the same as [ -n "" ], so the test fails.

[[ ... ]] is a lot more forgiving about quoting of variables - quoting is optional and it works the same with or without quotes.

cas
  • 78,579