4

I am having a hard time tracking down the reason my 'boolean' flag variable won't stay false when a test fails inside a while loop.

The script involves a few loops but essentially it is intended to play two random albums every morning when it is triggered by cron. I used to have a simple one-liner script that selected two albums but I wanted to blacklist any albums that I didn't want playing (i.e. christmas music albums).

To do this, I had it read a wakeUp_blacklist.txt file with the names of albums I didn't want to hear so it can reject any matches. This compares the blacklisted albums line-by-line with one randomly selected album and tests for a match. If a match is found, it flags the match as a failure (pass = false) and SHOULD loop back and select a different album. For some reason, as soon as the execution exits the read | while loop, the flag switches back to true and so another album is never selected!

Here is my code and an example output:

#!/bin/bash

wd="/media/External1/albums"

pass="false"
for ((i=0; i<=1; i++)); do
    while [ "$pass" == "false" ]; do
        album=`ls -d "$wd"/*/*/ | sort -R | tail -n 1`
        echo "album selected:"
        echo "$album"
        pass="true"
        echo "pass reset; pass=$pass"
        cat "$wd/wakeUp_blacklist.txt" | while read black; do
            echo "pass check 2:$pass"
            echo "black: $wd/$black"
            if [ "$album" == "$wd/$black" ] || [ "$album\n" == "$albums" ]; then
                pass="false"
                echo "test failed; pass=$pass"
            fi
            echo "pass check 1:$pass"
        done
        echo "Blacklist check complete; pass=$pass" #Why is it true again?!
    done
    pass="false"
    albums="$albums$album\n"
    echo "album assigned:"
    echo "$album"
done

echo
echo

for album in "$albums"; do
#    play "$album*"
     echo -e "$album" #List album names rather than actually play them
done

So, if I run this a few times, it will eventually chose one of the blacklisted albums and give output that looks like:

album selected:
/media/External1/albums/Adele/21/
pass reset; pass=true
pass check 2:true
black: /media/External1/albums/Vince_Guaraldi_Trio/A_Charlie_Brown_Christmas/
pass check 1:true
pass check 2:true
black: /media/External1/albums/Sound_Inventions/Stille_Nacht-_A_German_Christmas/
pass check 1:true
Blacklist check complete; pass=true
album assigned:
/media/External1/albums/Adele/21/
album selected:
/media/External1/albums/Sound_Inventions/Stille_Nacht-_A_German_Christmas/
pass reset; pass=true
pass check 2:true
black: /media/External1/albums/Vince_Guaraldi_Trio/A_Charlie_Brown_Christmas/
pass check 1:true
pass check 2:true
black: /media/External1/albums/Sound_Inventions/Stille_Nacht-_A_German_Christmas/
test failed; pass=false
pass check 1:false
Blacklist check complete; pass=true
album assigned:
/media/External1/albums/Sound_Inventions/Stille_Nacht-_A_German_Christmas/


/media/External1/albums/Adele/21/
/media/External1/albums/Sound_Inventions/Stille_Nacht-_A_German_Christmas/

You can see that towards the end, when the blacklist entry is a match for the selected album (German_Christmas) the test fails, the pass variable is false but the first output that checks pass value outside the read | while loop shows it being true again!

I understand that my script may not be as efficient as possible yet but I want to understand what is happening here before I move on. Any suggestions?

Volker Siegel
  • 17,283

1 Answers1

13

You do:

cat "$wd/wakeUp_blacklist.txt" | while read black; do

In Bash:

Each command in a pipeline is executed in its own subshell

A subshell is another instance of bash, with its own state. That means that any variable changes you make on the right-hand side of a | conceptually "belong" to a different bash instance — the existing values and declarations at the start are copied in, but the variables have different storage locations and modifications to them are visible only inside that subshell (you can think of this as working like fork, if you like).

When the subshell completes, that copy of the variable ceases to exist. The "outer" shell only ever sees its own, unmodified, copy of the variable, which is why it appears that you're losing your modifications outside the loop.


In this case, you can avoid using a subshell with simple redirection:

    while read black; do
        ...
        pass="false"
        ...
    done < "$wd/wakeUp_blacklist.txt"

Now, the while loop runs in your main shell, and no subshell is created. The contents of the blacklist file are still given as standard input to the loop using < file redirection. All variables exist inside the same environment. This approach also avoids a useless use of cat.

Some other shells (notably zsh), don't have this behaviour to start with, and you can modify variables on the right-hand side of pipelines freely.

Michael Homer
  • 76,565