Переменная, измененная внутри цикла 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 выдает этот отзыв для строки 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"
белий

Что делать, если источник был 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циклом; скорее использование subshell, т. е. in cmd1 | cmd2, cmd2находится в subshell. Таким образом, если forцикл выполняется в подоболочке, будет показано непредвиденное / проблемное поведение.
PP

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 (Внутренний разделитель полей), чтобы разбить строку на элементы массива. Это можно сделать так:

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: спасибо! Я добавил еще один, посвященный Вам!
Правда

Существует еще одно решение , данное здесь :for line in $(echo -e $lines); do ... done
dma_k

@dma_k Спасибо за ваш комментарий! Это решение привело бы к 6 строкам, содержащим одно слово. Запрос OP был другим ...
TrueY

upvoted. запуск эха в подоболочке внутри вот-вот, был одним из немногих решений, которые работали в пепле
Хэми

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

Хммм ... Я бы почти поклялся, что это сработало для оригинальной оболочки Bourne, но сейчас у меня нет доступа к работающей копии, чтобы проверить.

Однако существует очень тривиальное решение этой проблемы.

Измените первую строку скрипта с:

#!/bin/bash

в

#!/bin/ksh

И вуаля! Чтение в конце конвейера работает нормально, если у вас установлена ​​оболочка 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, чтобы уменьшить количество кода в подоболочке. Обратите внимание, что итеративное значение i можно прочитать вне цикла while.

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
Возможно, здесь есть неплохой ответ, но вы разбили форматирование до такой степени, что читать и читать неприятно.
Марк Амери

Вы имеете в виду, что оригинальный код приятно читать? (Я только что следил: р)
Марцин

0

Это интересный вопрос, затрагивающий очень простую концепцию оболочки Bourne и subshell. Здесь я предлагаю решение, которое отличается от предыдущих решений тем, что выполняет какую-то фильтрацию. Я приведу пример, который может быть полезен в реальной жизни. Это фрагмент для проверки того, что загруженные файлы соответствуют известной контрольной сумме. Файл контрольной суммы выглядит следующим образом (показывает только 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

Родитель и subshell взаимодействуют с помощью команды echo. Вы можете выбрать несколько простых для анализа текста для родительской оболочки. Этот метод не нарушает ваш обычный образ мышления, просто вы должны выполнить некоторую постобработку. Для этого вы можете использовать grep, sed, awk и другие.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.