A variable modified inside a while loop is not remembered

后端 未结 8 2175
挽巷
挽巷 2020-11-21 05:06

In the following program, if I set the variable $foo to the value 1 inside the first if statement, it works in the sense that its value is remember

相关标签:
8条回答
  • 2020-11-21 05:13

    This is an interesting question and touches on a very basic concept in Bourne shell and subshell. Here I provide a solution that is different from the previous solutions by doing some kind of filtering. I will give an example that may be useful in real life. This is a fragment for checking that downloaded files conform to a known checksum. The checksum file look like the following (Showing just 3 lines):

    49174 36326 dna_align_feature.txt.gz
    54757     1 dna.txt.gz
    55409  9971 exon_transcript.txt.gz
    

    The shell script:

    #!/bin/sh
    
    .....
    
    failcnt=0 # this variable is only valid in the parent shell
    #variable xx captures all the outputs from the while loop
    xx=$(cat ${checkfile} | while read -r line; do
        num1=$(echo $line | awk '{print $1}')
        num2=$(echo $line | awk '{print $2}')
        fname=$(echo $line | awk '{print $3}')
        if [ -f "$fname" ]; then
            res=$(sum $fname)
            filegood=$(sum $fname | awk -v na=$num1 -v nb=$num2 -v fn=$fname '{ if (na == $1 && nb == $2) { print "TRUE"; } else { print "FALSE"; }}')
            if [ "$filegood" = "FALSE" ]; then
                failcnt=$(expr $failcnt + 1) # only in subshell
                echo "$fname BAD $failcnt"
            fi
        fi
    done | tail -1) # I am only interested in the final result
    # you can capture a whole bunch of texts and do further filtering
    failcnt=${xx#* BAD } # I am only interested in the number
    # this variable is in the parent shell
    echo failcnt $failcnt
    if [ $failcnt -gt 0 ]; then
        echo $failcnt files failed
    else
        echo download successful
    fi
    

    The parent and subshell communicate through the echo command. You can pick some easy to parse text for the parent shell. This method does not break your normal way of thinking, just that you have to do some post processing. You can use grep, sed, awk, and more for doing so.

    0 讨论(0)
  • 2020-11-21 05:16
    echo -e $lines | while read line 
        ...
    done
    

    The while loop is executed in a subshell. So any changes you do to the variable will not be available once the subshell exits.

    Instead you can use a here string to re-write the while loop to be in the main shell process; only echo -e $lines will run in a subshell:

    while read line
    do
        if [[ "$line" == "second line" ]]
        then
            foo=2
            echo "Variable \$foo updated to $foo inside if inside while loop"
        fi
        echo "Value of \$foo in while loop body: $foo"
    done <<< "$(echo -e "$lines")"
    

    You can get rid of the rather ugly echo in the here-string above by expanding the backslash sequences immediately when assigning lines. The $'...' form of quoting can be used there:

    lines=$'first line\nsecond line\nthird line'
    while read line; do
        ...
    done <<< "$lines"
    
    0 讨论(0)
  • 2020-11-21 05:16

    Hmmm... I would almost swear that this worked for the original Bourne shell, but don't have access to a running copy just now to check.

    There is, however, a very trivial workaround to the problem.

    Change the first line of the script from:

    #!/bin/bash
    

    to

    #!/bin/ksh
    

    Et voila! A read at the end of a pipeline works just fine, assuming you have the Korn shell installed.

    0 讨论(0)
  • 2020-11-21 05:24

    Though this is an old question and asked several times, here's what I'm doing after hours fidgeting with here strings, and the only option that worked for me is to store the value in a file during while loop sub-shells and then retrieve it. Simple.

    Use echo statement to store and cat statement to retrieve. And the bash user must chown the directory or have read-write chmod access.

    #write to file
    echo "1" > foo.txt
    
    while condition; do 
        if (condition); then
            #write again to file
            echo "2" > foo.txt      
        fi
    done
    
    #read from file
    echo "Value of \$foo in while loop body: $(cat foo.txt)"
    
    0 讨论(0)
  • 2020-11-21 05:28

    I use stderr to store within a loop, and read from it outside. Here var i is initially set and read inside the loop as 1.

    # reading lines of content from 2 files concatenated
    # inside loop: write value of var i to stderr (before iteration)
    # outside: read var i from stderr, has last iterative value
    
    f=/tmp/file1
    g=/tmp/file2
    i=1
    cat $f $g | \
    while read -r s;
    do
      echo $s > /dev/null;  # some work
      echo $i > 2
      let i++
    done;
    read -r i < 2
    echo $i
    

    Or use the heredoc method to reduce the amount of code in a subshell. Note the iterative i value can be read outside the while loop.

    i=1
    while read -r s;
    do
      echo $s > /dev/null
      let i++
    done <<EOT
    $(cat $f $g)
    EOT
    let i--
    echo $i
    
    0 讨论(0)
  • 2020-11-21 05:30

    UPDATED#2

    Explanation is in Blue Moons's answer.

    Alternative solutions:

    Eliminate echo

    while read line; do
    ...
    done <<EOT
    first line
    second line
    third line
    EOT
    

    Add the echo inside the here-is-the-document

    while read line; do
    ...
    done <<EOT
    $(echo -e $lines)
    EOT
    

    Run echo in background:

    coproc echo -e $lines
    while read -u ${COPROC[0]} line; do 
    ...
    done
    

    Redirect to a file handle explicitly (Mind the space in < <!):

    exec 3< <(echo -e  $lines)
    while read -u 3 line; do
    ...
    done
    

    Or just redirect to the stdin:

    while read line; do
    ...
    done < <(echo -e  $lines)
    

    And one for chepner (eliminating echo):

    arr=("first line" "second line" "third line");
    for((i=0;i<${#arr[*]};++i)) { line=${arr[i]}; 
    ...
    }
    

    Variable $lines can be converted to an array without starting a new sub-shell. The characters \ and n has to be converted to some character (e.g. a real new line character) and use the IFS (Internal Field Separator) variable to split the string into array elements. This can be done like:

    lines="first line\nsecond line\nthird line"
    echo "$lines"
    OIFS="$IFS"
    IFS=$'\n' arr=(${lines//\\n/$'\n'}) # Conversion
    IFS="$OIFS"
    echo "${arr[@]}", Length: ${#arr[*]}
    set|grep ^arr
    

    Result is

    first line\nsecond line\nthird line
    first line second line third line, Length: 3
    arr=([0]="first line" [1]="second line" [2]="third line")
    
    0 讨论(0)
提交回复
热议问题