1

I have a loop in a bash script, test.sh that reads as follows:

#!/bin/bash

CHOSEN_NQUEUE=0

foo(){
 for chunk in $(seq 0 $((${CHOSEN_NQUEUE}-1)));
    do
        echo "CHUNK = $(($chunk+1))"
 done
}

bar(){
  CHOSEN_NQUEUE=10
  foo
}

bar

This loop has previously been working fine up till now. If I run the program as . test.sh, I get the following error code in the loop:

-bash: 0
1
2
3
4
5
6
7
8
9+1: syntax error in expression (error token is "1
2
3
4
5
6
7
8
9+1")

If I run the program as bash test.sh, then the function produces the desired result:

CHUNK = 1
CHUNK = 2
CHUNK = 3
CHUNK = 4
CHUNK = 5
CHUNK = 6
CHUNK = 7
CHUNK = 8
CHUNK = 9
CHUNK = 10

This is a snippet from a much larger program; if I run this program with bash program.sh, the error I see in the first case persists.

Particularly, if I simply run foo, then the error does not occur. If I run foo from bar, then the error occurs. This occurs irrespective of using bash program.sh or . program.sh.

Can someone kindly suggest what I might be doing wrong? Is it poor practice to run functions from inside other functions in bash?

Kindest regards!

EDIT:

Thanks to everyone in the comments!

Upon realizing this problem arises from using select for arrays, I attempted the following code:

select opt in "${options[@]}"

    do
            next=false
            local IFS=@
            case "@${options[*]}@" in
            (*"@$opt@"*)
                    foo
            (*)
                    echo "Invalid option: $REPLY"
                    ;;
            esac
            echo ""
    done


    echo "IFS = $IFS"

The problem arises from IFS=@, which should not be @ outside of the loop.

However, if I run this code attempting to set IFS locally, i.e. local IFS=@, it appears the global IFS is modified. The code outputs:

IFS = @

Does anyone have an idea why this might be?

Kind regards again!

14
  • 1
    for me the given code is working well...
    – Siva
    Commented Oct 1, 2019 at 11:05
  • 1
    You would get that error if you double-quoted the command substitution of seq, i.e. for chunk in "$( seq ... )". Could you double check that you are showing us the code that you are running?
    – Kusalananda
    Commented Oct 1, 2019 at 11:05
  • The whole thing is 2000 lines long - I can do this if it would help?
    – Jack Rolph
    Commented Oct 1, 2019 at 11:10
  • 1
    check what your IFS is in the working and the non-working case. Put something like printf "IFS: %q\n" "$IFS" in front of the for loop
    – ilkkachu
    Commented Oct 1, 2019 at 11:12
  • 1
    The output it simply IFS: @ in the non working case and IFS: $' \t\n' in the working case
    – Jack Rolph
    Commented Oct 1, 2019 at 11:27

1 Answer 1

2

The error you get indicates that $chunk contains a multiline value, all the numbers from 0 to 9. That would happen if word-splitting doesn't happen on the result of $(seq ...) in the for.

Now, the usual way to prevent word-splitting is to put double-quotes around the expansion, so for chunk in "$(seq ...)" wouldn't expand. But that's not the case here since you'd know if you added double-quotes, and anyway, it works in some cases.

But word-splitting isn't always the same, it's based on the value of IFS, which by default contains a space, tab and a newline ($' \t\n' using the C-style quoting). If it contains something different, then those are the characters that will be taken as word separators.

And indeed you have modified IFS inside the select, just before calling foo:

local IFS=@
case "@${options[*]}@" in
(*"@$opt@"*)
        foo

The way the variable scoping works in Bash is that foo also sees the modified value of IFS. local doesn't mean the change is visible only to that function, but instead it's also visible to all subfunctions called from that level too:

$ x=999
$ a() { echo "a: x=$x"; }
$ b() { local x=$1; a; }
$ b 123
a: x=123

This is unlike what you'd have in, say, C.


A workaround would be to save IFS to another variable instead, so something like this:

local oldifs=$IFS
IFS=@
str="@${options[*]}@"
IFS=$oldifs
case $str in ...

or to change it in a subshell (hiding the IFS change there):

str=$(IFS=@; echo "@${options[*]}@")
case $str in ...

You could also make a function to do that string join (hiding the IFS change in the function), you just need name references to pass variables by name:

# join a b c:
# join the values of array 'a' with the character 'b', and put the result in 'c'
join() {
    local -n _arr=$1
    local IFS=$2
    local -n _res=$3
    _res="${_arr[*]}"
}
src=(11 22 33)
join src @ dst
echo "$dst"        # outputs "11@22@33"

(Of course, that's a bit unwieldy for one use, and name references aren't perfect either: a nameref inside a function can't refer to a variable with the same name outside it (at least in Bash 4). The minor upside of this over just using a command substitution is avoiding a fork to start the subshell.)

Or, just to be on the safe side, (re)set IFS every time you need it. Inside foo:

foo() {
    local IFS=$' \t\n'      # or just IFS=$'\n'
    for chunk in $(seq ...); do
        ...
}
3
  • A perfect answer once again! Thank you for the help.
    – Jack Rolph
    Commented Oct 1, 2019 at 14:22
  • 1
    Note that you do not have to change IFS: str="@$(printf '%s@' "${options[@]}")"
    – user232326
    Commented Oct 1, 2019 at 15:57
  • @Isaac, yep, that seems like a pretty good alternative.
    – ilkkachu
    Commented Oct 1, 2019 at 16:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .