Переменная, измененная внутри цикла while, не запоминается

В следующей программе, если я устанавливаю переменную $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)

Ответ 1

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"

Ответ 2

ОБНОВЛЕНИЕ # 2

Объяснение в ответе Blue Moons.

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

Устранить 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")

Ответ 3

Вы пользователь 742342nd, чтобы задать этот bash FAQ. Ответ также описывает общий случай переменных, заданных в подоболочках, созданных трубами

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операторов расширения или использовать некоторые варианты следующих Подход.

Say/usr/local/bin/ipaddr - это следующая оболочка script:

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

Ответ 4

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

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

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

#!/bin/bash

к

#!/bin/ksh

Эт вуаля! Чтение в конце конвейера работает отлично, если у вас установлена ​​оболочка Korn.

Ответ 5

Как насчет очень простого метода

    +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

Это интересный вопрос, затрагивающий очень базовую концепцию оболочки 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 и другие.