テキストファイルを変更すると、変数 "line"を読み取るbash whileループは自動的に更新されますか?

テキストファイルを変更すると、変数 "line"を読み取るbash whileループは自動的に更新されますか?

たとえば、「He​​llo」を含む hello.txt というテキストファイルがあります。読み取り可能なwhileループがある場合:

while IFS= read -r line
do
    echo $line > hello.txt # whatever that inserts/ edits something in the text file
done < hello.txt

whileループの読み取りが変更されるか、無限ループが発生しますか?それとも、whileループはテキストファイルの変更を読み取ることができませんか?更新/修正されたテキストファイルを強制的に読み取ることができますか?

答え1

ループの入力とread出力は同じファイルに接続されます。はい。echo

この場合、出力リダイレクトを設定するたびに、個々の呼び出しごとにechoファイルが切り捨てられます。入力の読み出し位置は、最後の読み出しラインの直後の元の位置にとどまります。ここではまったく同じ行を印刷するので、読み取り位置は新しいEOFにあり、ループは1回の反復後に終了します。ファイルの最初の最初の行をファイルの唯一の内容として保持します。


ただし、長いテキストを出力すると、ループは以前に出力された内容の一部を読み取ることができます。

たとえば、考えてみましょう。

$ cat hello.txt
abc
def
ghi
$ cat test.sh
while IFS= read -r line
do
    echo "$line"  # to terminal, to see what is read
    echo "something something $line" > "$1"
done < "$1"

これでスクリプトを実行すると、hello.txt次の行が残ります。

something something g something abc

最初の行の後にはファイルが含まれており、something something abc読み取り位置は最初の行にありますtabcおよび改行文字を読み取るため)。読み取りthing something abcと印刷を繰り返しますsomething something thing something abc(ファイルの切り取り)。読み取り位置はまだ中間にあるため、最終的にEOFに達するまでループが繰り返されます。

read特に、読み取り位置は正確に行の末尾に保持されます。これはたとえば(標準準拠)と同じですhead -n1が、他の多くのユーティリティはブロック全体を読み込み、読み込み位置を短い入力のために元のファイルの末尾に残します。ここで結果が変わります。


一方、出力リダイレクトをに置き換えると、>> fileすべての書き込みがファイルに追加され、読み取り位置は決して終わりに達しません(ループは繰り返しごとに1行だけ読み書きするため)。無限ループが発生します。


繰り返しますが、削除するループ内の出力ファイルは、リダイレクト前にすべてが変更されます。

while IFS= read -r line
do
    rm "$1"
    echo "$line"  # to terminal, to see what is read
    echo "something something $line" > "$1"
done < "$1"

これで名前がなくなったため、出力リダイレクトから同じファイルにアクセスできなくなります。新しいファイルが作成されます。読み取りファイルハンドルはまだファイルに接続されています。もはや開いていない場合にのみ最終的に削除されます。元のバージョンでこれを実行すると、hello.txt新しいバージョンが残りますhello.txt

something something ghi

出力ファイルが入力から独立しているので、ループは入力を最後まで読み込みます。繰り返しごとに削除され、再生成されます。


ループの繰り返しごとにファイルを削除することはあまり役に立ちませんが、一度だけ削除された場合は、リダイレクトを少し移動する必要があります。これにより、ファイル全体が繰り返され、すべての行に次の名前が付いた新しいファイルが残りますsomething something

exec < "$1"  # input redirection for the whole shell script
rm "$1"
exec > "$1"  # symmetrically, output, creating a new file
             # with the same name
while IFS= read -r line
do
    echo "$line"  >&2 # to terminal via stderr, to see what is read
    echo "something something $line"
done

答え2

これを試して、いくつかのデバッグ情報を見てみましょう。私はカーネル呼び出しに慣れていませんが、経験豊富な人が間違いを修正できることを願っています。

$ cd "$(mktemp --directory)"
$ cat > test.bash <<'EOF'
> while IFS= read -r line
> do
>     echo "$line" > hello.txt
> done < hello.txt
> EOF
$ echo foo > hello.txt
$ strace bash --noprofile --norc test.bash 
[skipping the script setup for clarity, and annotating the rest]
# Open hello.txt for reading as file descriptor 3
openat(AT_FDCWD, "hello.txt", O_RDONLY) = 3
fcntl(0, F_GETFD)                       = 0
fcntl(0, F_DUPFD, 10)                   = 10
fcntl(0, F_GETFD)                       = 0
fcntl(10, F_SETFD, FD_CLOEXEC)          = 0
# Duplicate FD 3 as FD 0
dup2(3, 0)                              = 0
# Close FD 3
close(3)                                = 0
ioctl(0, TCGETS, 0x7ffc7a087960)        = -1 ENOTTY (Inappropriate ioctl for device)
# Go to start of FD 0
lseek(0, 0, SEEK_CUR)                   = 0
# Read up to max 128 bytes from FD 0. Four bytes read, which is the full file.
# `read` strips the newline, so $line ends up containing just "foo"
read(0, "foo\n", 128)                   = 4
# Open hello.txt for writing as FD 3
openat(AT_FDCWD, "hello.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fcntl(1, F_GETFD)                       = 0
fcntl(1, F_DUPFD, 10)                   = 11
fcntl(1, F_GETFD)                       = 0
fcntl(11, F_SETFD, FD_CLOEXEC)          = 0
# Duplicate FD 3 as FD 1
dup2(3, 1)                              = 1
# Close FD 3
close(3)                                = 0
fstat(1, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
# Write what we read plus a newline to FD 1
write(1, "foo\n", 4)                    = 4
dup2(11, 1)                             = 1
fcntl(11, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(11)                               = 0
ioctl(0, TCGETS, 0x7ffc7a087960)        = -1 ENOTTY (Inappropriate ioctl for device)
# Go to current position in FD 0 (index 4, as returned by the previous `read`)
lseek(0, 0, SEEK_CUR)                   = 4
# Try to read again from FD 0. Encounters end of file (return value 0).
read(0, "", 128)                        = 0
# Shut down
dup2(10, 0)                             = 0
fcntl(10, F_GETFD)                      = 0x1 (flags FD_CLOEXEC)
close(10)                               = 0
read(255, "", 129)                      = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
exit_group(0)                           = ?

デフォルトでは、EOFが発生するとループは終了readします。whileスクリプトを置き換えると、echo "$line" > hello.txt実行されるたびにファイルが終了する前により多くの内容があるecho "$line" >> hello.txtため、リポジトリがいっぱいになるまで実行されます。read

関連情報