次のプログラムでは、最初のif
ステートメント内で変数$foo
を値1に設定すると、その値はifステートメントの後に記憶されるという意味で機能します。しかし、if
ステートメントの中にあるwhile
の中で同じ変数を値2に設定すると、while
ループの後で忘れられます。 while
ループ内で変数$foo
のある種のコピーを使用しているように動作し、その特定のコピーのみを変更しています。これが完全なテストプログラムです。
#!/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)
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
を代入するときにバックスラッシュシーケンスをすぐに展開することによって、上記のhere-string内のかなり醜いlines
を取り除くことができます。 $'...'
形式の引用符をここで使用できます。
lines=$'first line\nsecond line\nthird line'
while read line; do
...
done <<< "$lines"
更新#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")
あなたはこれを尋ねるのは742342人目のユーザーです bash FAQ その答えはまた、パイプによって作成されたサブシェルに設定された変数の一般的なケースについて説明しています。
E4)コマンドの出力を
read variable
にパイプすると、readコマンドが終了したときに出力が$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)
これは、残念ながら、複数の変数の引数が与えられた場合のreadのように、テキストを複数の変数に分割するのには役立ちません。これを行う必要がある場合は、上記のコマンド置換を使用して出力を変数に読み込み、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 Shellでもうまくいったことを誓いますが、実行するためのコピーにアクセスすることはできません。
ただし、この問題には非常に簡単な回避策があります。
スクリプトの最初の行を次のように変更します。
#!/bin/bash
に
#!/bin/ksh
Et voila! Kornシェルがインストールされていると仮定すると、パイプラインの最後の読み取りは問題なく動作します。
とても簡単な方法はどうですか
+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.
これは興味深い質問で、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
親とサブシェルはechoコマンドを介して通信します。あなたは親シェルのためにいくつかの解析しやすいテキストを選ぶことができます。この方法は通常の考え方を壊すことはありません。後処理をする必要があるということだけです。 grep、sed、awkなどを使うことができます。