13

This works on a shell (bash, dash) prompt:

[ -z "" ] && echo A || echo B
A

However, I am trying to write a POSIX shell script, it starts like this:

#!/bin/sh

[ "${#}" -eq 1 ] || echo "Invalid number of arguments, expected one."; exit 1

readonly raw_input_string=${1}

[ -z "${raw_input_string}" ] && echo "The given argument is empty."; exit 1

And I don't know why, but I don't get the message:

The given argument is empty.

if I call the script like this:

./test_empty_argument ""

Why is that?

ilkkachu
  • 138,973

4 Answers4

36

Note that your line

[ "${#}" -eq 1 ] || echo "Invalid number of arguments, expected one."; exit 1

this is the same as

[ "${#}" -eq 1 ] || echo "Invalid number of arguments, expected one."
exit 1

(an unquoted ; can, in most circumstances, be replaced by a newline character)

This means that the exit 1 statement is always executed regardless of how many arguments were passed to the script. This in turn means that the message The given argument is empty. would never have a chance of getting printed.

To execute more than a single statement after a test using the "short-circuit syntax", group the statements in { ...; }. The alternative is to use a proper if statement (which, IMHO, looks cleaner in a script):

if [ "$#" -ne 1 ]; then
    echo 'Invalid number of arguments, expected one.' >&2
    exit 1
fi

You have the same issue with your second test.


Regarding

[ -z "" ] && echo A || echo B

This would work for the given example, but the generic

some-test && command1 || command2

would not be the same as

if some-test; then
    command1
else
    command2
fi

Instead, it is more like

if ! { some-test && command1; }; then
    command2
fi

or

if some-test && command1; then
    :
else
    command2
fi

That is, if either the test or the first command fails, the second command executes, which means it has the potential to execute all three involved statements.

Kusalananda
  • 333,661
17

This:

[ "${#}" -eq 1 ] || echo "Invalid number of arguments, expected one."; exit 1

is not:

[ "${#}" -eq 1 ] || { echo "Invalid number of arguments, expected one."; exit 1; }

But instead is:

{ [ "${#}" -eq 1 ] || echo "Invalid number of arguments, expected one."; } 
exit 1

Your script is exiting regardless of how many arguments you passed to it.

muru
  • 72,889
8

One way to make it more readable is to define a die function (à la perl) like:

die() {
  printf >&2 '%s\n' "$@"
  exit 1
}

# then:

[ "$#" -eq 1 ] || die "Expected one argument, got $#"

[ -n "$1" ] || die "Empty argument not supported"

You can add more bells and whistles like colours, prefix, line number... if need be.

  • In practice, do you ever call your die function with multiple arguments? (If so, can you give an example?) I use an almost identical die function, but use "$*" instead, which may be more what you're intending? – jrw32982 Apr 03 '19 at 18:15
  • 3
    The value of "$@" is that it allows multi-line messages without needing to add literal newlines. – Charles Duffy Apr 03 '19 at 20:54
  • 1
    @jrw32982, using "$*" to join args with spaces also means you need to set $IFS to SPC for it to work in all contexts including those where $IFS has been modified. Alternatively with ksh/zsh, you can use print -r -- "$@" or echo -E - "$@" in zsh. – Stéphane Chazelas Apr 04 '19 at 07:17
  • @CharlesDuffy Yes, but I've never seen that done in the context of a die-type function. What I'm asking is: in practice, have you ever seen anyone write die "unable to blah:" "some error", for the purpose of getting a 2-line error message? – jrw32982 Apr 04 '19 at 13:24
  • @StéphaneChazelas Good point. So (in my formulation) it should be die() { IFS=" "; printf >&2 "%s\n" "$*"; exit 1; }. Have you ever personally used this kind of die function to have printf generate a multi-line error message by passing multiple arguments? Or do you only ever pass a single argument to die so that it only adds a single newline to its output? – jrw32982 Apr 04 '19 at 13:29
  • @jrw32982, I have, rarely. That's why I use "$@" instead of "$1". Generally I call die with one argument (so "$1", "$*", "$@" would make no difference). perl's die() does "$*" with empty $IFS (concatenates the arguments without separator). I sometimes use die() { [ "$#" -eq 0 ] || printf... so that die without argument acts like exit 1. – Stéphane Chazelas Apr 04 '19 at 13:29
  • @jrw32982, I've certainly written multi-line error messages that way myself in real, production code. – Charles Duffy Apr 04 '19 at 13:51
  • @CharlesDuffy I've found it to be more readable to use << here-strings which include their newlines literally. I'm curious about the use case when several strings without newlines is better than one here-string with embedded newlines. – jrw32982 Apr 04 '19 at 18:53
-1

I've often seen this as a test for an empty string:

if [ "x$foo" = "x" ]; then ...
wef
  • 472
  • Should have been "=" - fixed. – wef Apr 03 '19 at 06:56
  • 3
    That practice is literally from the 1970s. There is no reason whatsoever to use it with any shell that is compliant with the 1992 POSIX sh standard (so long as correct quoting is used and now-obsolescent functionality such as -a, -o, ( and ) as derectives to tell test to combine multiple operations in a single invocation are avoided; see the OB markers in http://pubs.opengroup.org/onlinepubs/9699919799/utilities/test.html). – Charles Duffy Apr 03 '19 at 20:52
  • Non-linux unices shipped with the original 'sh' well into the '90's - maybe still do. In my job at that time, I had to write portable install scripts and I used this construct. I just looked at the install scripts that NVidia ships for linux and they still use this construct. – wef Apr 03 '19 at 22:21
  • NVidia may use it, but that's not to say they have any technical justification to do so; cargo-cult development in commercial UNIX is sadly prevalent. Even Heirloom Bourne doesn't have the bug in question -- so the SunOS codebase (that being the last commercial UNIX to ship a non-POSIX /bin/sh, and Heirloom Bourne's immediate predecessor) didn't have it either. – Charles Duffy Apr 03 '19 at 23:13
  • 1
    I don't wear a hat, so I can't promise to post a YouTube video eating it should someone turn up a shell published on a commercial UNIX with this bug post-1990... but if I did, it would be tempting. :) – Charles Duffy Apr 03 '19 at 23:32