1

I have a loop that checks for certain criteria for whether or not to skip to the next iteration (A). I realized that if I invoke a function (skip) that calls continue, it is as if it is called in a sub-process for it does not see the loop (B). Also the proposed workaround that relies on eval-uating a string does not work (C).

# /usr/bin/bash env

skip()
{
    echo "skipping : $1"
    continue   
}

skip_str="echo \"skipping : $var\"; continue"

while read -r var;
do
    if [[ $var =~  ^bar$  ]];
    then
        # A
        #        echo "skipping : $var"
        #        continue
        # B
        #        skip "$var" #  continue: only meaningful in a `for', `while', or `until' loop
        # C
        eval "$skip_str"
    fi
    echo "processed: $var"
done < <(cat << EOF 
foo
bar
qux
EOF
        )

Method C:

$ source ./job-10.sh
processed: foo
skipping : 
processed: qux

Also see:

Do functions run as subprocesses in Bash?

PS1: could someone remind me why < < rather than < is needed after done?

PS2: no tag found for while hence for

Erwann
  • 605
  • 1
  • 8
  • 18
  • What's the question? – roaima Apr 08 '21 at 17:56
  • 1
    Note that your shebang is wrong (it won't affect the script). You want _either_ `/usr/bin/env bash` _or_ `/bin/bash` but not `/usr/bin/bash env`. – terdon Apr 08 '21 at 18:01
  • 2
    When you define `skip_str` you end up evaluating `$var` there and then, so when you call `eval` there's no variable to evaluate. But this is really poor practice anyway so don't do it like this – roaima Apr 08 '21 at 18:02
  • If you tell us what you're trying to do, I would imagine you'll get some better quality code in an answer – roaima Apr 08 '21 at 18:03
  • The syntax `<( command )` runs `command`, stores the stdout in a tmp file, and substitutes itself with the tmp filename. The preceding `<` is a normal redirect that attaches that tmp filename to stdin of the `while do done`, and is inherited by all of its child processes (in this case the `read` built-in). – Paul_Pedant Apr 08 '21 at 18:11
  • @terdon: And the shebang is missing the bang, too. – user unknown Apr 08 '21 at 18:31
  • @userunknown whoops, so it is, thanks. And thanks for fixing it in my answer. – terdon Apr 08 '21 at 18:40

2 Answers2

4

The function runs in the same process all right, it's just syntactically distinct from the loop. In pretty much any other programming language, you also can't use break or continue inside a function to affect a loop outside it.

But actually, in the shell, it's unspecified:

continue [n]
If n is specified, the continue utility shall return to the top of the nth enclosing for, while, or until loop. If n is not specified, continue shall behave as if n was specified as 1. [...]

[...] If there is no enclosing loop, the behavior is unspecified.

What you get depends on the shell:

Bash gives you the error and ignores the continue:

$ bash -c 'f() { continue; }; for x in a b c; do f; echo $x; done; echo done'
environment: line 0: continue: only meaningful in a ‘for’, ‘while’, or ‘until’ loop
a
environment: line 0: continue: only meaningful in a ‘for’, ‘while’, or ‘until’ loop
b
environment: line 0: continue: only meaningful in a ‘for’, ‘while’, or ‘until’ loop
c
done

Ksh silently ignores it:

$ ksh -c 'f() { continue; }; for x in a b c; do f; echo $x; done; echo done'
a
b
c
done

While Dash, Yash and Zsh actually do apply it:

$ dash -c 'f() { continue; }; for x in a b c; do f; echo $x; done; echo done'
done

I did expect similar considerations might apply to using eval, too, but it seems that's not the case. At least I can't see any differences between the shells.

All in all, don't do that. Just write it out:

while read -r var;
do
    if [[ $var =~  ^bar$  ]];
    then
        echo "skipping '$var'"
        continue
    fi
    echo "processed: $var"
done
ilkkachu
  • 133,243
  • 15
  • 236
  • 397
1

The problem is that when your function is executed, it is no longer inside a loop. It isn't in a subshell, no, but it is also not inside any loop. As far as the function is concerned, it is a self-contained piece of code and has no knowledge of where it was called from.

Then, when you run eval "$skip_str" there is no value in $var because you have set skip_string at a time when $var was not defined. This should actually work as you expect, it's just seriously convoluted and risky (if you don't control input 100%) for no reason:

#! /usr/bin/env bash

while read -r var;
do
  skip_str="echo \"skipping : $var\"; continue"
  if [[ $var =~  ^bar$  ]];
  then
    eval "$skip_str"
  fi
  echo "processed: $var"

done < <(cat << EOF 
foo
bar
qux
EOF
        )

That... really isn't very pretty. Personally, I would just use a function to do the test and then operate on the test's results. Like this:

#! /usr/bin/env bash

doTest(){
  if [[ $1 =~  ^bar$  ]];
  then
    return 1
  else
    return 0
  fi
  
}

while read -r var;
do
  if doTest "$var"; then
    echo "processed: $var"
  else
    echo "skipped: $var"
    continue
  fi

## Rest of your code here

done < <(cat << EOF 
foo
bar
qux
EOF
        )

I could probably give you something better if you explained what your objective is.

Finally, you don't need < < after done, you need < <(). The < is the normal input redirection, and the <() is called process substitution and is a trick that lets you treat the output of a command as though it were a file name.

If you are using the function just to avoid repeating the extra things like echo "skipping $1", you could simply move more of the logic into the function so that you have a loop there. Something like this: link

user unknown
  • 10,267
  • 3
  • 35
  • 58
terdon
  • 234,489
  • 66
  • 447
  • 667