Một biến được sửa đổi trong vòng lặp while không được ghi nhớ


185

Trong chương trình sau, nếu tôi đặt biến $foothành giá trị 1 bên trong ifcâu lệnh đầu tiên , nó hoạt động theo nghĩa là giá trị của nó được ghi nhớ sau câu lệnh if. Tuy nhiên, khi tôi đặt cùng một biến thành giá trị 2 bên trong một giá trị bên iftrong một whilecâu lệnh, nó sẽ bị quên sau whilevòng lặp. Nó hoạt động giống như tôi đang sử dụng một số loại bản sao của biến $footrong whilevòng lặp và tôi chỉ sửa đổi bản sao cụ thể đó. Đây là một chương trình thử nghiệm hoàn chỉnh:

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


Tiện ích shellcheck nắm bắt điều này (xem github.com/koalaman/shellcheck/wiki/SC2030 ); Cắt và dán mã trên trong shellcheck.net đưa ra phản hồi này cho Dòng 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Câu trả lời:


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

Các whilevòng lặp được thực hiện trong một subshell. Vì vậy, bất kỳ thay đổi nào bạn thực hiện đối với biến sẽ không khả dụng khi thoát khỏi khung con.

Thay vào đó, bạn có thể sử dụng một chuỗi ở đây để viết lại vòng lặp while trong quy trình shell chính; chỉ echo -e $linessẽ chạy trong một 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")"

Bạn có thể thoát khỏi cái khá xấu xí echotrong chuỗi ở đây bằng cách mở rộng các chuỗi dấu gạch chéo ngược ngay lập tức khi gán lines. Hình $'...'thức trích dẫn có thể được sử dụng ở đó:

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

20
thay đổi tốt hơn <<< "$(echo -e "$lines")"để đơn giản<<< "$lines"
beliy

Điều gì nếu nguồn là từ tail -fthay vì văn bản cố định?
mt eee

2
@mteee Bạn có thể sử dụng while read -r line; do echo "LINE: $line"; done < <(tail -f file)(rõ ràng vòng lặp sẽ không chấm dứt vì nó tiếp tục chờ đầu vào từ tail).
PP

Tôi bị giới hạn ở / bin / sh - có cách nào khác hoạt động trong trình bao cũ không?
dùng9645

1
@AvinashYadav Vấn đề không thực sự liên quan đến whilevòng lặp hoặc forvòng lặp; thay vì sử dụng subshell tức là, trong cmd1 | cmd2, cmd2là trong một subshell. Vì vậy, nếu một forvòng lặp được thực thi trong một lớp con, hành vi không mong muốn / có vấn đề sẽ được thể hiện.
PP

48

CẬP NHẬT # 2

Giải thích là trong câu trả lời của Blue Moons.

Các giải pháp thay thế:

Loại bỏ echo

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

Thêm tiếng vang bên trong tài liệu ở đây

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

Chạy echotrong nền:

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

Chuyển hướng đến một tệp xử lý rõ ràng (Lưu ý khoảng trắng trong < <!):

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

Hoặc chỉ cần chuyển hướng đến stdin:

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

Và một cho chepner(loại bỏ echo):

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

Biến $linescó thể được chuyển đổi thành một mảng mà không cần bắt đầu một vỏ con mới. Các ký tự \nphải được chuyển đổi thành một số ký tự (ví dụ: một ký tự dòng mới thực sự) và sử dụng biến IFS (Dấu tách trường nội bộ) để phân tách chuỗi thành các phần tử mảng. Điều này có thể được thực hiện như:

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

Kết quả là

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

+1 cho tài liệu ở đây, vì linesmục đích duy nhất của biến dường như là để cung cấp whilevòng lặp.
chepner

@chepner: Thx! Tôi đã thêm một cái khác, dành riêng cho Bạn!
TrueY

Vẫn còn một giải pháp khác được đưa ra ở đây :for line in $(echo -e $lines); do ... done
dma_k

@dma_k Cảm ơn bình luận của bạn! Giải pháp này sẽ dẫn đến 6 dòng chứa một từ duy nhất. Yêu cầu của OP đã khác ...
TrueY

nâng cao. chạy echo trong một subshell bên trong đây - là, một trong số ít các giải pháp hoạt động trong tro
Hamy

9

Bạn là người dùng thứ 742342 để hỏi FAQ bash này . Câu trả lời cũng mô tả trường hợp chung của các biến được đặt trong các khung con được tạo bởi các đường ống:

E4) Nếu tôi dẫn đầu ra của lệnh vào read variable, tại sao đầu ra không hiển thị $variablekhi lệnh đọc kết thúc?

Điều này có liên quan đến mối quan hệ cha-con giữa các quy trình Unix. Nó ảnh hưởng đến tất cả các lệnh chạy trong đường ống, không chỉ các cuộc gọi đơn giản read. Ví dụ: chuyển đầu ra của lệnh thành một whilevòng lặp liên tục gọi readsẽ dẫn đến hành vi tương tự.

Mỗi phần tử của một đường ống, thậm chí là hàm dựng sẵn hoặc hàm, chạy trong một quy trình riêng, một phần tử con của vỏ chạy đường ống. Một quy trình con không thể ảnh hưởng đến môi trường của cha mẹ nó. Khi readlệnh đặt biến thành đầu vào, biến đó chỉ được đặt trong lớp con, không phải vỏ cha. Khi subshell thoát, giá trị của biến bị mất.

Nhiều đường ống kết thúc bằng read variablecó thể được chuyển đổi thành các lệnh thay thế, sẽ thu được đầu ra của một lệnh được chỉ định. Đầu ra sau đó có thể được gán cho một biến:

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

có thể được chuyển đổi thành

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

Thật không may, điều này không làm việc để phân chia văn bản giữa nhiều biến, như đọc khi đưa ra nhiều đối số biến. Nếu bạn cần làm điều này, bạn có thể sử dụng thay thế lệnh ở trên để đọc đầu ra thành một biến và cắt biến bằng cách sử dụng các toán tử mở rộng loại bỏ mô hình bash hoặc sử dụng một số biến thể của phương pháp sau.

Nói / usr / local / bin / ipaddr là tập lệnh shell sau:

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

Thay vì sử dụng

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

để chia địa chỉ IP của máy cục bộ thành các octet riêng biệt, hãy sử dụng

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

Tuy nhiên, coi chừng rằng điều này sẽ thay đổi các tham số vị trí của shell. Nếu bạn cần chúng, bạn nên lưu chúng trước khi làm điều này.

Đây là cách tiếp cận chung - trong hầu hết các trường hợp, bạn sẽ không cần đặt $ IFS thành một giá trị khác.

Một số lựa chọn thay thế khác do người dùng cung cấp bao gồm:

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

và, nơi thay thế quá trình có sẵn,

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

7
Bạn đã quên vấn đề Family Feud . Đôi khi rất khó để tìm ra sự kết hợp của các từ giống như bất cứ ai đã viết câu trả lời, để bạn không bị ngập trong kết quả sai, cũng không lọc ra câu trả lời.
Evi1M4chine

3

Hmmm ... Tôi gần như sẽ thề rằng cái này hoạt động cho vỏ Bourne ban đầu, nhưng không có quyền truy cập vào một bản sao đang chạy để kiểm tra.

Tuy nhiên, có một cách giải quyết rất nhỏ cho vấn đề.

Thay đổi dòng đầu tiên của tập lệnh từ:

#!/bin/bash

đến

#!/bin/ksh

Et voila! Một lần đọc ở cuối đường ống hoạt động tốt, giả sử bạn đã cài đặt vỏ Korn.


0

Làm thế nào về một phương pháp rất đơn giản

    +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
Có thể có một câu trả lời xác đáng dưới bề mặt ở đây, nhưng bạn đã đánh lừa định dạng đến mức khó chịu khi thử và đọc.
Đánh dấu Amery

Bạn có nghĩa là mã ban đầu là dễ chịu để đọc? (Tôi vừa theo dõi: p)
Marcin

0

Đây là một câu hỏi thú vị và chạm vào một khái niệm rất cơ bản trong vỏ và vỏ con Bourne. Ở đây tôi cung cấp một giải pháp khác với các giải pháp trước đó bằng cách thực hiện một số loại lọc. Tôi sẽ đưa ra một ví dụ có thể hữu ích trong cuộc sống thực. Đây là một đoạn để kiểm tra các tệp đã tải xuống phù hợp với tổng kiểm tra đã biết. Tệp tổng kiểm tra trông giống như sau (Chỉ hiển thị 3 dòng):

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

Kịch bản shell:

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

Cha mẹ và subshell giao tiếp thông qua lệnh echo. Bạn có thể chọn một số văn bản dễ phân tích cú pháp cho shell cha. Phương pháp này không phá vỡ cách suy nghĩ thông thường của bạn, chỉ là bạn phải thực hiện một số xử lý bài. Bạn có thể sử dụng grep, sed, awk, và nhiều hơn nữa để làm như vậy.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.