Variabel yang dimodifikasi di dalam loop sementara tidak diingat

187

Dalam program berikut, jika saya mengatur variabel $fooke nilai 1 di dalam ifpernyataan pertama , itu bekerja dalam arti bahwa nilainya diingat setelah pernyataan if. Namun, ketika saya mengatur variabel yang sama ke nilai 2 di dalam ifyang ada di dalam whilepernyataan, itu dilupakan setelah whileloop. Berperilaku seperti saya menggunakan semacam salinan variabel $foodi dalam whileloop dan saya hanya memodifikasi salinan tertentu. Inilah program pengujian lengkap:

#!/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)
Eric Lilja
sumber
Utilitas shellcheck menangkap ini (lihat github.com/koalaman/shellcheck/wiki/SC2030 ); Potong dan tempel kode di atas dalam shellcheck.net mengeluarkan umpan balik ini untuk Baris 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Jawaban:

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

The whileloop dieksekusi dalam subkulit. Jadi setiap perubahan yang Anda lakukan pada variabel tidak akan tersedia setelah subkulit keluar.

Sebagai gantinya, Anda dapat menggunakan string di sini untuk menulis ulang loop sementara agar berada dalam proses shell utama; hanya echo -e $linesakan berjalan dalam subkulit:

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

Anda dapat menyingkirkan yang agak jelek echodi sini-string di atas dengan memperluas urutan backslash segera saat menetapkan lines. The $'...'bentuk mengutip dapat digunakan di sana:

lines=$'first line\nsecond line\nthird line'
while read line; do
    ...
done <<< "$lines"
PP
sumber
20
lebih baik ubah <<< "$(echo -e "$lines")"ke sederhana<<< "$lines"
beliy
bagaimana jika sumbernya tail -fbukan teks tetap?
mt eee
2
@mteee Anda dapat menggunakan while read -r line; do echo "LINE: $line"; done < <(tail -f file)(jelas loop tidak akan berakhir karena terus menunggu input dari tail).
PP
Saya terbatas pada / bin / sh - apakah ada cara alternatif yang bekerja di shell lama?
user9645
1
@AvinashYadav Masalahnya tidak benar-benar terkait dengan whileloop atau forloop; alih-alih penggunaan subshell yaitu, di cmd1 | cmd2, cmd2dalam subshell. Jadi jika forloop dieksekusi dalam subkulit, perilaku tak terduga / bermasalah akan ditampilkan.
PP
48

DIPERBARUI # 2

Penjelasan ada dalam jawaban Blue Moons.

Solusi alternatif:

Menghapuskan echo

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

Tambahkan gema di dalam dokumen-di sini

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

Jalankan echodi latar:

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

Redirect ke penanganan file secara eksplisit (Pikirkan ruang di < <!):

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

Atau cukup arahkan ke stdin:

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

Dan satu untuk chepner(menghilangkan echo):

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

Variabel $linesdapat dikonversi ke array tanpa memulai sub-shell baru. Karakter \dan nharus dikonversi ke beberapa karakter (misalnya karakter baris baru nyata) dan menggunakan variabel IFS (Pemisah Bidang Internal) untuk membagi string menjadi elemen array. Ini dapat dilakukan seperti:

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

Hasilnya adalah

first line\nsecond line\nthird line
first line second line third line, Length: 3
arr=([0]="first line" [1]="second line" [2]="third line")
Benar
sumber
+1 untuk di sini-doc, karena satu lines-satunya tujuan variabel tampaknya adalah untuk memberi makan whileloop.
chepner
@chepner: Thx! Saya menambahkan satu lagi, didedikasikan untuk Anda!
Benar
Ada solusi lain yang diberikan di sini :for line in $(echo -e $lines); do ... done
dma_k
@dma_k Terima kasih atas komentar Anda! Solusi ini akan menghasilkan 6 baris yang mengandung satu kata. Permintaan OP berbeda ...
Benar
terbalik. menjalankan gema dalam sebuah subkulit di dalam sini-adalah, adalah salah satu dari beberapa solusi yang bekerja di abu
Hamy
9

Anda adalah pengguna ke-742342 untuk menanyakan FAQ bash ini . Jawabannya juga menjelaskan kasus umum variabel yang diatur dalam subkulit yang dibuat oleh pipa:

E4) Jika saya menyalurkan output dari suatu perintah read variable, mengapa output tidak muncul $variableketika perintah baca selesai?

Ini ada hubungannya dengan hubungan orangtua-anak antara proses Unix. Ini mempengaruhi semua perintah yang dijalankan dalam saluran pipa, bukan hanya panggilan sederhana read. Misalnya, memipatkan output perintah ke dalam whileloop yang berulang kali memanggil readakan menghasilkan perilaku yang sama.

Setiap elemen dari sebuah pipa, bahkan fungsi builtin atau shell, berjalan dalam proses terpisah, anak dari shell yang menjalankan pipa. Subproses tidak dapat memengaruhi lingkungan induknya. Ketika readperintah mengatur variabel ke input, variabel itu hanya diatur dalam subkulit, bukan shell induk. Ketika subkulit keluar, nilai variabel hilang.

Banyak pipa yang diakhiri dengan read variabledapat dikonversi menjadi pergantian perintah, yang akan menangkap output dari perintah yang ditentukan. Output kemudian dapat ditugaskan ke variabel:

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

dapat dikonversi menjadi

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

Sayangnya, ini tidak berfungsi untuk membagi teks di antara beberapa variabel, seperti yang dibaca ketika diberikan beberapa argumen variabel. Jika Anda perlu melakukan ini, Anda bisa menggunakan substitusi perintah di atas untuk membaca output menjadi variabel dan memotong variabel menggunakan operator ekspansi penghapusan pola bash atau menggunakan beberapa varian dari pendekatan berikut.

Say / usr / local / bin / ipaddr adalah skrip shell berikut:

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

Alih-alih menggunakan

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

untuk memecah alamat IP mesin lokal menjadi oktet terpisah, gunakan

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

Hati-hati, bagaimanapun, bahwa ini akan mengubah parameter posisi shell. Jika Anda membutuhkannya, Anda harus menyimpannya sebelum melakukan ini.

Ini adalah pendekatan umum - dalam kebanyakan kasus Anda tidak perlu mengatur $ IFS ke nilai yang berbeda.

Beberapa alternatif yang disediakan pengguna termasuk:

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

dan, di mana proses substitusi tersedia,

read A B C D < <(IFS=.; echo $(/usr/local/bin/ipaddr))
Jens
sumber
7
Anda lupa masalah Perselisihan Keluarga . Terkadang sangat sulit untuk menemukan kombinasi kata yang sama dengan siapa pun yang menulis jawaban, sehingga Anda tidak kebanjiran hasil yang salah, atau memfilter jawaban kata itu.
Evi1M4chine
3

Hmmm ... Saya hampir bersumpah bahwa ini bekerja untuk shell Bourne asli, tetapi tidak memiliki akses ke salinan yang sedang berjalan sekarang untuk memeriksa.

Namun, ada solusi yang sangat sepele untuk masalah ini.

Ubah baris pertama skrip dari:

#!/bin/bash

untuk

#!/bin/ksh

Et voila! Pembacaan pada akhir pipa berfungsi dengan baik, dengan asumsi Anda memiliki shell Korn yang diinstal

Jonathan Quist
sumber
1

Saya menggunakan stderr untuk menyimpan dalam satu lingkaran, dan membaca dari luar. Di sini var i pada awalnya diatur dan dibaca di dalam loop sebagai 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

Atau gunakan metode heredoc untuk mengurangi jumlah kode dalam subkulit. Perhatikan bahwa nilai i berulang dapat dibaca di luar loop sementara.

i=1
while read -r s;
do
  echo $s > /dev/null
  let i++
done <<EOT
$(cat $f $g)
EOT
let i--
echo $i
bugdot
sumber
0

Bagaimana dengan metode yang sangat sederhana

    +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.
Marcin
sumber
6
Mungkin ada jawaban yang layak di bawah permukaan di sini, tetapi Anda telah memotong format sampai-sampai tidak menyenangkan untuk mencoba dan membaca.
Mark Amery
Maksudmu kode asli itu enak dibaca? (Saya baru saja mengikuti: p)
Marcin
0

Ini adalah pertanyaan yang menarik dan menyentuh konsep yang sangat mendasar dalam Bourne shell and subshell. Di sini saya memberikan solusi yang berbeda dari solusi sebelumnya dengan melakukan semacam penyaringan. Saya akan memberikan contoh yang mungkin berguna dalam kehidupan nyata. Ini adalah fragmen untuk memeriksa apakah file yang diunduh sesuai dengan checksum yang dikenal. File checksum terlihat seperti berikut (Menampilkan hanya 3 baris):

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

Script 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

Induk dan subkulit berkomunikasi melalui perintah gema. Anda dapat memilih beberapa teks yang mudah diurai untuk shell induk. Metode ini tidak merusak cara berpikir normal Anda, hanya saja Anda harus melakukan beberapa pemrosesan pos. Anda dapat menggunakan grep, sed, awk, dan lainnya untuk melakukannya.

Kemin Zhou
sumber