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