bashshellvariablesglobal-variablesuntil-loop

How to set parent variable within a loop in a shell script?


I have been trying to learn loops, and I have the following:

Guessing_Game() {
echo "Guessing Game!"
set loopSwitch = 0
until [[ $loopSwitch == 1 ]];
    echo $loopSwitch
    do
        read -p "Guess a number between 1 and 10: " userGuess
        if [[ $userGuess == 6 ]];
        then
            echo "Well done! You win."
        else
            echo "Incorrect guess."
        fi

    read -p "Would you like to play again? (y/n)" playAgain
    if [[ $playAgain == "n" ]];
        then
            set loopSwitch = 1
    fi

done
}

From what I have read, as the loopSwitch variable was set outside of the subshell, it can only change the value within that subshell?

How can I go about setting the loopSwitch variable so that I can exit the loop?

I have tried echoing out the variable $loopSwitch through several portions of the code - just after the entry into the until loop, inside the last if statement - and nothing is output, so I'm thinking it just doesn't exist within the until loop?

For the

set loopSwitch = 1 

portion, I've tried:

set loopSwitch = 1
set $loopSwitch = 1
loopSwitch = 1
$loopSwitch = 1

Any help would be appreciated, whether that be with the code or constructs as a whole.


Solution

  • As others have mentioned, there is no subshell in your code. I think you probably saw code like

    num=27; seq 3 | while IFS= read -r line; do num="$line"; echo "inside: $num"; done; echo "outside: $num"
    

    which would output 1, 2, 3, 27 and misunderstood why that outputs 27 instead of 3 at the end. It's not because loops run in a subshell, but because the commands on both sides of a pipe (|) run in a subshell. That's why you'd use command substitution (<(command)) to write

    num=27; while IFS= read -r line; do num="$line"; echo "inside: $num"; done < <(seq 3); echo "outside: $num"
    

    which would output 1, 2, 3, 3 instead. See mywiki.wooledge.org/BashFAQ/024.

    Regarding the code in your question, FWIW I'd write it as:

    $ cat tst.sh
    #!/usr/bin/env bash
    
    Guessing_Game() {
        local keepPlaying theNumber userGuess
        echo 'Guessing Game!'
    
        theNumber=$(shuf -n 1 -i 1-10)
    
        until [[ "$keepPlaying" =~ [nN] ]]; do
            echo "$keepPlaying"
            IFS= read -r -p 'Guess a number between 1 and 10: ' userGuess
            if (( userGuess == theNumber )); then
                echo "Well done! You win."
            else
                echo "Incorrect guess."
            fi
    
            IFS= read -r -p 'Would you like to play again? ([y]/n)' keepPlaying
        done
    }
    
    Guessing_Game
    

    The changes I made were:

    1. Added a shebang to find the first bash in the callers PATH.
    2. Added the call to the function.
    3. Used local variables.
    4. Used arithmetic operators ((...)) for numeric comparisons.
    5. Used IFS= and -r on the reads as you always should unless you have a specific need to remove either of them, see why-is-using-a-shell-loop-to-process-text-considered-bad-practice.
    6. Quoted the variables as you always should unless you have a specific need to remove them, see https://mywiki.wooledge.org/Quotes.
    7. Got rid of loopSwitch as it meant almost exactly the same thing as playAgain and then renamed playAgain to keepPlaying so it made slightly more sense for the first loop iteration too.
    8. Defined a variable to hold the target number instead of hard-coding it in the comparison for ease of future maintenance/enhancements (e.g. if you wanted to add a print statement to dump it).
    9. Make the target number a random value between 1 and 10 instead of a hard-coded 6. If you don't have shuf that can do this job just google "shell random number between 1 and 10" or similar.
    10. Made the test for keepPlaying case-independent.
    11. Removed the sets as they're unnecessary and I don't know why anyone would use that.
    12. Used a more common style of indenting.