while 루프 안에서 수정 된 변수는 기억되지 않습니다


187

다음 프로그램 $foo에서 첫 번째 if명령문 내 에서 변수 를 값 1로 설정 하면 if 문 뒤에 값이 기억된다는 의미에서 작동합니다. 그러나 명령문 if내에있는 내부의 값 2로 동일한 변수를 설정 while하면 while루프 후에 잊어 버립니다 . 루프 $foo내에서 일종의 변수 사본을 사용 while하고 특정 사본 만 수정 하는 것처럼 작동 합니다. 완벽한 테스트 프로그램은 다음과 같습니다.

#!/bin/bash

set -e
set -u 
foo=0
bar="hello"  
if [[ "$bar" == "hello" ]]
then
    foo=1
    echo "Setting \$foo to 1: $foo"
fi

echo "Variable \$foo after if statement: $foo"   
lines="first line\nsecond line\nthird line" 
echo -e $lines | 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 "Variable \$foo after while loop: $foo"

# Output:
# $ ./testbash.sh
# Setting $foo to 1: 1
# Variable $foo after if statement: 1
# Value of $foo in while loop body: 1
# Variable $foo updated to 2 inside if inside while loop
# Value of $foo in while loop body: 2
# Value of $foo in while loop body: 2
# Variable $foo after while loop: 1

# bash --version
# GNU bash, version 4.1.10(4)-release (i686-pc-cygwin)


shellcheck 유틸리티는 이것을 잡습니다 ( github.com/koalaman/shellcheck/wiki/SC2030 참조 ); shellcheck.net 에서 위 코드를 잘라 붙여 넣으면 19 행에 대한이 피드백을 발행합니다.SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

답변:


244
echo -e $lines | while read line 
    ...
done

while루프 서브 쉘 실행된다. 따라서 서브 쉘이 종료되면 변수에 대한 변경 사항을 사용할 수 없습니다.

대신 here 문자열 을 사용 하여 while 루프를 기본 셸 프로세스에 있도록 다시 작성할 수 있습니다. echo -e $lines서브 쉘 에서만 실행됩니다 :

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")"

echo할당 할 때 즉시 백 슬래시 시퀀스를 확장하여 위의 문자열에서 오히려 추악한 것을 제거 할 수 있습니다 lines. $'...'인용의 형태는 사용할 수있다 :

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"

20
더 나은 <<< "$(echo -e "$lines")"단순한 변화<<< "$lines"
beliy

소스가 tail -f고정 텍스트 가 아닌 소스 인 경우 어떻게 합니까?
mt eee

2
@mteee 당신은 사용할 수 있습니다 while read -r line; do echo "LINE: $line"; done < <(tail -f file)(분명히 루프는 입력을 계속 기다리기 때문에 종료되지 않습니다 tail).
PP

나는 / bin / sh로 제한되어 있습니다-이전 쉘에서 작동하는 다른 방법이 있습니까?
user9645

1
@AvinashYadav 문제는 실제로 while루프 나 for루프 와 관련이 없습니다 . 오히려 서브 쉘 즉의 사용에서 cmd1 | cmd2, cmd2서브 쉘입니다. 따라서 for서브 쉘에서 루프가 실행되면 예기치 않은 / 문제적인 동작이 나타납니다.
PP

48

업데이트 된 # 2

설명은 Blue Moons의 답변에 있습니다.

대체 솔루션 :

죽이다 echo

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

여기 문서 안에 에코를 추가하십시오

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

echo백그라운드에서 실행 :

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

파일 핸들로 명시 적으로 리디렉션하십시오 (공백에 < <!).

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

또는 그냥 다음으로 리디렉션하십시오 stdin.

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

그리고 하나는 chepner(제거 echo) :

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

$lines새로운 서브 쉘을 시작하지 않고 변수 를 배열로 변환 할 수 있습니다. 문자 \n이 어떤 문자로 변환 (예 : 실제 새 라인 문자) 및 배열 요소로 문자열을 분할하는 IFS (내부 필드 구분) 변수를 사용합니다. 이것은 다음과 같이 수행 할 수 있습니다 :

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

결과는

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")

lines변수의 유일한 목적은 while루프 를 공급하는 것으로 보이므로 here-doc의 경우 +1입니다 .
chepner 2016 년

@chepner : Thx! 나는 당신에게 헌신 된 또 하나를 추가했습니다!
TrueY

여기에 또 다른 해결책이 있습니다 :for line in $(echo -e $lines); do ... done
dma_k

@dma_k 귀하의 의견에 감사드립니다! 이 솔루션은 단일 단어를 포함하는 6 줄을 생성합니다. OP의 요청은 달랐다 ...
TrueY

공감. 여기에있는 서브 쉘에서 에코를 실행하는 것은 화산재에서 효과가있는 몇 안되는 솔루션 중 하나입니다.
Hamy

9

bash FAQ 를 요청한 사용자는 742342 입니다. 또한 파이프로 생성 된 서브 쉘에 설정된 변수의 일반적인 경우도 설명합니다.

E4) 명령의 출력을에 파이프 하면 읽기 명령이 완료 될 때 read variable출력이 나타나지 않는 이유는 $variable무엇입니까?

이것은 유닉스 프로세스 간의 부모-자식 관계와 관련이 있습니다. 단순한 호출뿐만 아니라 파이프 라인에서 실행되는 모든 명령에 영향을줍니다 read. 예를 들어, 명령의 출력 while을 반복적으로 호출 하는 루프 로 파이핑 read하면 동일한 동작이 발생합니다.

파이프 라인의 각 요소 (내장 또는 셸 함수 포함)는 별도의 프로세스, 파이프 라인을 실행하는 셸의 자식으로 실행됩니다. 서브 프로세스는 상위 환경에 영향을 줄 수 없습니다. 때 read명령이 입력 변수를 설정하고, 해당 변수는 서브 쉘 아니라 부모 쉘 설정된다. 서브 쉘이 종료되면 변수 값이 유실됩니다.

로 끝나는 많은 파이프 라인은 read variable명령 대체로 변환되어 지정된 명령의 출력을 캡처합니다. 그런 다음 출력을 변수에 할당 할 수 있습니다.

grep ^gnu /usr/lib/news/active | wc -l | read ngroup

로 변환 될 수있다

ngroup=$(grep ^gnu /usr/lib/news/active | wc -l)

불행히도 이것은 여러 변수 인수가 주어 졌을 때 읽기처럼 텍스트를 여러 변수로 나누는 작업이 아닙니다. 이 작업을 수행해야하는 경우 위의 명령 대체를 사용하여 출력을 변수로 읽고 bash 패턴 제거 확장 연산자를 사용하여 변수를 잘라내거나 다음 방법의 변형을 사용할 수 있습니다.

/ usr / local / bin / ipaddr이 다음 쉘 스크립트라고 가정하십시오.

#! /bin/sh
host `hostname` | awk '/address/ {print $NF}'

사용하는 대신

/usr/local/bin/ipaddr | read A B C D

로컬 컴퓨터의 IP 주소를 별도의 옥텟으로 나누려면

OIFS="$IFS"
IFS=.
set -- $(/usr/local/bin/ipaddr)
IFS="$OIFS"
A="$1" B="$2" C="$3" D="$4"

그러나이 경우 쉘의 위치 매개 변수가 변경됩니다. 필요한 경우이를 수행하기 전에 저장해야합니다.

이것은 일반적인 접근 방식입니다. 대부분의 경우 $ IFS를 다른 값으로 설정할 필요가 없습니다.

다른 사용자 제공 대안은 다음과 같습니다.

read A B C D << HERE
    $(IFS=.; echo $(/usr/local/bin/ipaddr))
HERE

공정 대체가 가능한 경우

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))

7
가족 불화 문제를 잊었습니다 . 때로는 답을 쓴 사람과 같은 단어 조합을 찾기가 매우 어려워서 잘못된 결과로 범람하거나 답을 걸러 내지 않아도됩니다.
Evi1M4chine

3

흠 ... 나는 이것이 원래 Bourne 쉘에서 효과가 있다고 맹세하지만 지금은 실행중인 사본에 액세스 할 수는 없습니다.

그러나이 문제에 대한 간단한 해결 방법이 있습니다.

스크립트의 첫 번째 줄을 다음에서 변경하십시오.

#!/bin/bash

#!/bin/ksh

짜잔! Korn 쉘이 설치되어 있다고 가정하면 파이프 라인 끝의 읽기는 정상적으로 작동합니다.


0

아주 간단한 방법은 어떻습니까

    +call your while loop in a function 
     - set your value inside (nonsense, but shows the example)
     - return your value inside 
    +capture your value outside
    +set outside
    +display outside


    #!/bin/bash
    # set -e
    # set -u
    # No idea why you need this, not using here

    foo=0
    bar="hello"

    if [[ "$bar" == "hello" ]]
    then
        foo=1
        echo "Setting  \$foo to $foo"
    fi

    echo "Variable \$foo after if statement: $foo"

    lines="first line\nsecond line\nthird line"

    function my_while_loop
    {

    echo -e $lines | while read line
    do
        if [[ "$line" == "second line" ]]
        then
        foo=2; return 2;
        echo "Variable \$foo updated to $foo inside if inside while loop"
        fi

        echo -e $lines | while read line
do
    if [[ "$line" == "second line" ]]
    then
    foo=2;          
    echo "Variable \$foo updated to $foo inside if inside while loop"
    return 2;
    fi

    # Code below won't be executed since we returned from function in 'if' statement
    # We aready reported the $foo var beint set to 2 anyway
    echo "Value of \$foo in while loop body: $foo"

done
}

    my_while_loop; foo="$?"

    echo "Variable \$foo after while loop: $foo"


    Output:
    Setting  $foo 1
    Variable $foo after if statement: 1
    Value of $foo in while loop body: 1
    Variable $foo after while loop: 2

    bash --version

    GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)
    Copyright (C) 2007 Free Software Foundation, Inc.

6
어쩌면 여기 표면에 알맞은 답변이 있지만 시도하고 읽는 것이 불쾌한 지점까지 서식을 정체했습니다.
Mark Amery

원래 코드를 읽는 것이 즐겁다는 것을 의미합니까? (방금 팔로우 : p)
Marcin

0

이것은 흥미로운 질문이며 Bourne 쉘과 서브 쉘의 매우 기본적인 개념을 다루고 있습니다. 여기서는 일종의 필터링을 수행하여 이전 솔루션과 다른 솔루션을 제공합니다. 나는 실제 생활에 도움이 될만한 예를 제시 할 것입니다. 다운로드 한 파일이 알려진 체크섬을 준수하는지 확인하기위한 조각입니다. 체크섬 파일은 다음과 같습니다 (단 3 줄만 표시).

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

쉘 스크립트 :

#!/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

부모와 서브 쉘은 echo 명령을 통해 통신합니다. 부모 셸에 대해 구문 분석하기 쉬운 텍스트를 선택할 수 있습니다. 이 방법은 일반적인 사고 방식을 깨뜨리지 않으며 사후 처리를 수행해야합니다. grep, sed, awk 등을 사용하여 수행 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.