Змінна, модифікована всередині циклу, не запам'ятовується


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 видає цей відгук для рядка 19:SC2030: Modification of foo is local (to subshell caused by pipeline).
qneill

Відповіді:


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

whileЦикл виконується в подоболочкі. Тож будь-які зміни, які ви зробите для змінної, будуть недоступні, коли закінчується підзарядка.

Натомість ви можете використовувати рядок тут, щоб повторно записати цикл 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).
ПП

Я обмежений / bin / sh - чи існує альтернативний спосіб, який працює у старій оболонці?
user9645

1
@AvinashYadav Проблема насправді не пов'язана з whileциклом чи forциклом; скоріше використання піддіаграфа, тобто, в cmd1 | cmd2, cmd2в нижній частині. Отже, якщо forцикл виконується в нижній частині корпусу, буде показана несподівана / проблемна поведінка.
ПП

48

ОНОВЛЕНО №2

Пояснення - у відповіді Блакитних Лун.

Альтернативні рішення:

Усуньте 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 (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: Thx! Я додав ще один, присвячений Тобі!
TrueY

Існує ще одне рішення , дане тут :for line in $(echo -e $lines); do ... done
dma_k

@dma_k Дякуємо за ваш коментар! Це рішення призведе до 6 рядків, що містять одне слово Прохання ОП було іншим ...
Правда,

прихильний. Запуск відлуння в підпакеті всередині тут був одним із небагатьох рішень, що працювали в золі
Хамі

9

Ви - 742342-й користувач, щоб задати цей запит щодо bash. Відповідь також описує загальний випадок змінних, встановлених в підрозділах, створених трубами:

E4) Якщо я передаю висновок команди в read variable, чому результат не з’являється, $variableколи команда читання закінчується?

Це пов'язано з відносинами батько-дитина між процесами 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

Et voila! Прочитане в кінці трубопроводу працює чудово, якщо припустити, що у вас встановлена ​​оболонка 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
Можливо, тут є гідна відповідь під поверхнею, але ви врізали форматування до того, що неприємно намагатися читати.
Марк Амері

Ви маєте на увазі, що оригінальний код приємно читати? (Я щойно стежив за: 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 тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.