ตัวแปรที่ถูกแก้ไขภายในลูป while ไม่ได้ถูกจดจำ


187

ในโปรแกรมต่อไปนี้ถ้าฉันตั้งค่าตัวแปร$fooเป็นค่า 1 ภายในifคำสั่งแรกมันจะทำงานในแง่ที่ว่าค่านั้นถูกจดจำหลังจากคำสั่ง if อย่างไรก็ตามเมื่อฉันตั้งค่าตัวแปรเดียวกันเป็นค่า 2 ภายในคำสั่งifที่อยู่ใน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ออกความคิดเห็นนี้สำหรับ Line 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

คำตอบ:


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

การwhileวนซ้ำถูกดำเนินการใน subshell ดังนั้นการเปลี่ยนแปลงใด ๆ ที่คุณทำกับตัวแปรจะไม่สามารถใช้ได้เมื่อ subshell ออก

แต่คุณสามารถใช้สตริงที่นี่เพื่อเขียนลูป while อีกครั้งเพื่อให้อยู่ในกระบวนการเชลล์หลัก เพียง แต่echo -e $linesจะทำงานใน 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")"

คุณจะได้รับการกำจัดของน่าเกลียดมากกว่าในที่นี่สตริงข้างต้นโดยขยายลำดับทับขวาทันทีเมื่อมีการระบุ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ลูปจริงๆ ค่อนข้างใช้ subshell คือในcmd1 | cmd2, cmd2อยู่ใน subshell ดังนั้นหากมีforการดำเนินการวนรอบใน subshell พฤติกรรมที่ไม่คาดคิด / ปัญหาจะถูกแสดง
PP

48

UPDATED # 2

คำอธิบายอยู่ในคำตอบของ Blue Moons

ทางเลือกอื่น ๆ :

กำจัด echo

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

เพิ่ม echo ภายใน here-is-the-document

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 (Internal Field Separator) เพื่อแยกสตริงออกเป็นองค์ประกอบอาร์เรย์ สามารถทำได้เช่น:

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

+1 สำหรับ here-doc เนื่องจากlinesวัตถุประสงค์เฉพาะของตัวแปรดูเหมือนว่าจะป้อนฟีดwhileลูป
chepner

@chepner: ขอบคุณ! ฉันเพิ่มอีกหนึ่งอุทิศให้กับคุณ!
TrueY

ยังมีวิธีแก้ปัญหาอื่นที่นี่ :for line in $(echo -e $lines); do ... done
dma_k

@dma_k ขอบคุณสำหรับความคิดเห็นของคุณ! การแก้ปัญหานี้จะส่งผลให้ 6 บรรทัดที่มีคำเดียว คำขอของ OP ที่แตกต่างกัน ...
TrueY

upvoted ใช้เสียงสะท้อนใน subshell ข้างในนี่ - คือเป็นหนึ่งในไม่กี่คำตอบที่ทำงานในเถ้า
Hamy

9

คุณเป็นผู้ใช้ 742342 คนที่จะถามคำถามที่พบบ่อยเกี่ยวกับbash นี้ คำตอบยังอธิบายถึงกรณีทั่วไปของตัวแปรที่ตั้งค่าใน subshells ที่สร้างโดยไพพ์:

E4) ถ้าฉันไพพ์เอาท์พุทของคำสั่งลงไปread variableทำไมเอาท์พุตไม่ปรากฏขึ้น$variableเมื่อคำสั่ง read เสร็จสิ้น?

สิ่งนี้เกี่ยวข้องกับความสัมพันธ์พาเรนต์ - ลูกระหว่างกระบวนการ Unix 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

อืม ... ฉันเกือบจะสาบานได้ว่าสิ่งนี้ใช้ได้กับเชลล์เป้าหมายเดิม แต่ไม่สามารถเข้าถึงสำเนาที่กำลังรันอยู่เพื่อตรวจสอบได้ในตอนนี้

อย่างไรก็ตามมีวิธีแก้ปัญหาเล็กน้อยสำหรับปัญหา

เปลี่ยนบรรทัดแรกของสคริปต์จาก:

#!/bin/bash

ถึง

#!/bin/ksh

และ voila! การอ่านที่ส่วนท้ายของไพพ์ไลน์ใช้งานได้ดีสมมติว่าคุณติดตั้ง Korn เชลล์แล้ว


1

ฉันใช้ stderr เพื่อจัดเก็บภายในลูปและอ่านจากข้างนอก ที่นี่ var i ถูกตั้งค่าและอ่านภายในลูปเป็น 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

หรือใช้วิธีการ heredoc เพื่อลดจำนวนของรหัสใน subshell หมายเหตุค่า iterative i สามารถอ่านได้ภายนอก 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

วิธีการเกี่ยวกับวิธีการที่ง่ายมาก

    +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 shell และ subshell ที่นี่ฉันมีวิธีแก้ไขปัญหาที่แตกต่างจากโซลูชันก่อนหน้าโดยทำการกรองบางประเภท ฉันจะยกตัวอย่างที่อาจเป็นประโยชน์ในชีวิตจริง นี่เป็นส่วนสำหรับการตรวจสอบว่าไฟล์ที่ดาวน์โหลดมานั้นสอดคล้องกับ checksum ที่รู้จักหรือไม่ ไฟล์เช็คซัมมีลักษณะดังต่อไปนี้ (แสดงเพียง 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.