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
  • 677
  • What's the question? – Chris Davies 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 – Chris Davies 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 – Chris Davies 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
  • 138,973
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 &quot;skipping : $var&quot;; 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,482
terdon
  • 242,166