Переменная, измененная внутри цикла 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)

6 ответов

Решение
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")"

ОБНОВЛЕНИЕ #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")

Вы являетесь 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))

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

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

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

#!/bin/bash

в

#!/bin/ksh

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

Я использую 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

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

Хотя это старый вопрос, который задавали несколько раз, вот что я делаю после нескольких часов возни с hereстроки, и единственный вариант, который сработал для меня, - это сохранить значение в файле во время суб-оболочек цикла while, а затем получить его. Просто.

Использовать echo заявление о хранении и catзаявление для извлечения. И пользователь bash должен chown каталог или иметь чтение-запись chmod доступ.

#write to file
echo "1" > foo.txt

while condition; do 
    if (condition); then
        #write again to file
        echo "2" > foo.txt      
    fi
done

#read from file
echo "Value of \$foo in while loop body: $(cat foo.txt)"

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

    +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.
Другие вопросы по тегам