Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bash script throws syntax errors when the 'extglob' option is set inside a subshell or function

Tags:

bash

Problem

The execution of a Bash script fails with the following error message when the 'extglob' option is set inside a subshell:

/tmp/foo.sh: line 7: syntax error near unexpected token `('
#!/usr/bin/env bash
set -euo pipefail
(
    shopt -s extglob
    for f in ?(.)!(|+(.)|vendor); do
        echo "$f"
    done
)

It fails in the same manner inside a function:

#!/usr/bin/env bash
set -euo pipefail

list_no_vendor () {
    shopt -s extglob
    for f in ?(.)!(|+(.)|vendor); do
        echo "$f"
    done
}

list_no_vendor

Investigation

In both cases, the script executes successfully when the option is set globally, outside of the subshell or function.

Surprisingly, when set locally, the 'extglob' option appears to be effectively enabled in both the subshell and function:

#!/usr/bin/env bash
set -euo pipefail

(
    shopt -s extglob
    echo 'In the subshell:' "$(shopt extglob)"
)

list_no_vendor () {
    shopt -s extglob
    echo 'In the function:' "$(shopt extglob)"
}

echo 'In the main shell:' "$(shopt extglob)"

list_no_vendor

Output:

In the subshell: extglob            on
In the main shell: extglob          off
In the function: extglob            on

This makes the syntax error extremely puzzling to me.

Workaround

Passing a heredoc to the bash command works.

#!/usr/bin/env bash
set -euo pipefail

bash <<'EOF'
    shopt -s extglob
    echo 'In the child:' "$(shopt extglob)"
EOF

echo 'In the parent:' "$(shopt extglob)"

Output:

In the child: extglob           on
In the parent: extglob          off

However I would be curious to understand the gist of the problem here.

like image 697
Antoine Cotten Avatar asked Feb 04 '23 01:02

Antoine Cotten


1 Answers

extglob is a flag used by the parser. Functions, compound commands, &c. are parsed in entirety ahead of execution. Thus, extglob must be set before that content is parsed; setting it at execution time but after parse time does not have any effect for previously-parsed content.

This is also why you can't run shopt -s extglob; ls !(*.txt) as a one-liner (when extglob is previously unset), but must have a newline between the two commands.


Not as an example of acceptable practice, but as an example demonstrating the behavior, consider the following:

#!/usr/bin/env bash
(
    shopt -s extglob
    # Parse of eval'd code is deferred, so this succeeds
    eval '
        for f in ?(.)!(|+(.)|vendor); do
            echo "$f"
        done
    '
)

No such error takes place here, because parsing of the content passed to eval happens only after the shopt -s extglob was executed, rather than when the block of code to be run in a subshell is parsed.

like image 184
Charles Duffy Avatar answered Feb 08 '23 17:02

Charles Duffy