2

So I'm currently just trying to traverse through my current directory where I'm calling the following bash script that prints 'We found a .c file' every time one is found. I have an if statement to check for args because I will be extending the script where if no args are found it will run anyway, and one arg will tell the script the directory to look in.

The issue is, this code does not work:

if [ -z "$#" ]
    then
        for i in *.c; do
            echo "We found a .c file"
       done
fi

But then if I add the echo "Test" in, it works?

if [ -z "$#" ]
    echo "Test"
    then
        for i in *.c; do
            echo "We found a .c file"
       done
fi

I'm new to bash and no clue why this is happening. Can anyone help me out?

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
Mr10k
  • 157
  • 1
  • 10
  • use shell debugging by inserting `set -vx` and `set +vx` around code that you want to trace. Good luck. – shellter Jun 15 '16 at 03:02

2 Answers2

7

$#, which reports the count of arguments, is NEVER an empty string - if you don't specify arguments, $# evaluates to 0, which is still a nonempty string (-z tests for empty strings).

Therefore, [ -z "$#" ] is always (logically) false.

What you're looking for - using idiomatic Bash - is:

if [[ $# -eq 0 ]]; then ...  # -eq compares *numerically*

As anishsane points out in a comment, the POSIX-compliant [ $# -eq 0 ] would work here as well; generally, though - unless your express intent is to write POSIX-compliant shell code - you're better off sticking with the more predictable, more feature-rich (and marginally faster) Bash-specific constructs.

or, using arithmetic evaluation:

if (( $# == 0 )); then ...

As for why your 2nd snippet caused the if branch to be entered:

Your misplaced echo "Test" - due to being placed before the then keyword, caused the echo command to be interpreted as part of the conditional.

In other words: the conditional that was evaluated was effectively [ -z "$#" ]; echo "Test", a list of (two) commands only whose last command's exit code determined the outcome of the conditional.

Since echo always succeeds (exit code 0)[1] , the conditional as a whole evaluated to (logical) true, and the if branch was entered.


[1] gniourf_gniourf points out in a comment that you can make a simple echo command fail (with exit code 1), if you use input/output redirection with an invalid source/target; e.g., echo 'fail' > /dev/full.
(Note that if the redirection source/target is fundamentally invalid - an nonexistent input file or an output file that can't be created / opened (as opposed to, say, an output target that can be opened with write permission but ultimately can't be written to, such as /dev/full on Linux) - Bash never even invokes the command at hand, as it "gives up" when it encounters the invalid redirection:
{ echo here >&2; echo hi; } >/dev/full # Linux: 'here' still prints (to stderr)
{ echo here >&2; echo hi; } >'' # invalid target: commands are never invoked)

Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Yep this makes sense. I tried using idiomatic bash but I forgot the [[ ]] where I was doing just [ ]. Thanks for that, noob error! – Mr10k Jun 15 '16 at 03:11
  • 1
    @above comment: No, you CAN use `[]` instead of `[[]]` here: `[ $# -ne 0 ]` would work just fine. `[ -z $# ]` was wrong. – anishsane Jun 15 '16 at 03:41
  • Well, `echo` doesn't always succeed: `echo 'fail' > /dev/full`. – gniourf_gniourf Jun 15 '16 at 09:05
  • @gniourf_gniourf: Fair point, thanks - I've added a footnote to the answer. (As a minor point of interest: the command itself never even gets to run with an invalid redirection; a moot point with `echo`, but may matter in other cases.) – mklement0 Jun 15 '16 at 12:18
  • 1
    Not quite. It's not Bash's redirection that fails in this case, it's really `echo`, as you can check like so: `{ echo >&2 hello; echo fail; echo >&2 "Return code: $?"; echo -n; echo >&2 "Return code of empty echo: $?"; } > /dev/full`. The redirection is opened once for all (and is successfully opened; `/dev/full` can be _opened_ for writing!), but it's when a write occurs (that is, in the `echo`) that an error occurs (and hence that `echo` fails). And that's what the manual specifies too: _Returns success unless a write error occurs._ – gniourf_gniourf Jun 15 '16 at 12:20
  • 1
    @gniourf_gniourf: Thanks for the clarification - see if my update makes sense. (On a side note: there is no `/dev/full` file on OS X.) – mklement0 Jun 15 '16 at 13:51
3

Problem

The following loop will never run:

if [ -z "$#" ]
    then
        for i in *.c; do
            echo "We found a .c file"
       done
fi

The reason is that $# is a number, 0, 1 or more. It will never be an empty string. Thus [ -z "$#" ] will always fail

This loop will always run:

if [ -z "$#" ]
    echo "Test"
    then
        for i in *.c; do
            echo "We found a .c file"
       done
fi

While [ -z "$#" ] always fails the second statement echo "Test" normally returns a success exit code.

Solution

If no arguments were specified on the command line, this sets the arguments to all .c files in the current directory:

[ "$1" ] || set -- *.c
for i in "$@"; do
    echo "We found a .c file: $i"
done

Thus, this allows you to specify the file names on the command line and the script runs on those. If you don't specify any, it runs on all the .c files.

John1024
  • 109,961
  • 14
  • 137
  • 171