web-dev-qa-db-ja.com

Bash:匿名のFIFOを作成する

私たちは皆、mkfifoとパイプラインを知っています。最初のものはnamedパイプを作成するため、名前を選択する必要があります。おそらくmktempを使用し、後でリンクを解除することを忘れないでください。もう1つは匿名パイプを作成し、名前と削除の手間はありませんが、パイプの両端はパイプラインのコマンドに関連付けられているため、ファイル記述子を把握して残りの部分で使用することは本当に便利ではありませんスクリプトの。コンパイルされたプログラムでは、単にret=pipe(filedes)を実行します。バッシュにはexec 5<>fileしたがって、"exec 5<> -"または"pipe <5 >6"-バッシュにそのようなものはありますか?

39
Adrian Panasiuk

名前付きパイプを現在のプロセスにアタッチした直後に、名前付きパイプのリンクを解除できます。これにより、実質的には匿名パイプになります。

# create a temporary named pipe
PIPE=$(mktemp -u)
mkfifo $PIPE
# attach it to file descriptor 3
exec 3<>$PIPE
# unlink the named pipe
rm $PIPE
...
# anything we write to fd 3 can be read back from it
echo 'Hello world!' >&3
head -n1 <&3
...
# close the file descriptor when we are finished (optional)
exec 3>&-

名前付きパイプを避けたい場合(たとえば、ファイルシステムが読み取り専用である場合)は、「ファイル記述子を把握する」という考え方も機能します。 procfsを使用しているため、これはLinux固有のものであることに注意してください。

# start a background pipeline with two processes running forever
tail -f /dev/null | tail -f /dev/null &
# save the process ids
PID2=$!
PID1=$(jobs -p %+)
# Hijack the pipe's file descriptors using procfs
exec 3>/proc/$PID1/fd/1 4</proc/$PID2/fd/0
# kill the background processes we no longer need
# (using disown suppresses the 'Terminated' message)
disown $PID2
kill $PID1 $PID2
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptors when we are finished (optional)
exec 3>&- 4<&-
45
htamas

私が知っているどのシェルもフォークせずにパイプを作成することはできませんが、一部のシェルは基本的なシェルパイプラインよりも優れています。

Bash、ksh、zshでは、システムが/dev/fdをサポートしていると仮定すると(最近はほとんどの場合)、コマンドの入力または出力をファイル名に関連付けることができます。<(command)は、次のファイル名に展開されます。 commandからの出力に接続されたパイプを指定し、>(command)commandの入力に接続されたパイプを指定するファイル名に展開されます。この機能はプロセス置換と呼ばれます。その主な目的は、複数のコマンドを別のコマンドとの間でパイプすることです。たとえば、

diff <(transform <file1) <(transform <file2)
tee >(transform1 >out1) >(transform2 >out2)

これは、基本的なシェルパイプのいくつかの欠点に対処する場合にも役立ちます。たとえば、command2 < <(command1)は、そのステータスがcommand1 | command2であることを除いて、command2と同等です。別の使用例は、exec > >(postprocessing)です。これは、残りのスクリプト全体を{ ... } | postprocessing内に配置することと同等ですが、より読みやすくなっています。

2012年10月現在、この機能はまだBashには存在しないようですが、名前のない/匿名のパイプが必要なのが子プロセスと通信することだけである場合は、coprocを使用できます。この時点でのcoprocの問題は、明らかに一度に1つしかサポートされないことです。 coprocがこの制限を受けた理由を理解できません。それらは既存のタスクのバックグラウンドコード(&op)の拡張であるはずですが、それはbashの作者にとっての問題です。

3
Radu C

@DavidAndersonの answer はすべてのベースをカバーし、いくつかの優れた保護手段を提供しますが、明らかにする最も重要なことは、匿名パイプを手に入れるのは<(:)と同じくらい簡単で、 Linuxを使い続けるとき。

だからあなたの質問への最も短くて簡単な答えは:

exec 5<> <(:)

MacOSでは機能しません。リダイレクトするまで、名前付きのfifoを格納する一時ディレクトリを作成する必要があります。他のBSDについては知りません。

2
clacke

次の関数は、GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)を使用してテストされました。オペレーティングシステムはUbuntu 18でした。この関数は、匿名FIFOに必要なファイル記述子である単一のパラメーターを取ります。

_MakeFIFO() {
    local "MakeFIFO_upper=$(ulimit -n)" 
    if [[ $# -ne 1 || ${#1} -gt ${#MakeFIFO_upper} || -n ${1%%[0-9]*} || 10#$1 -le 2
        || 10#$1 -ge MakeFIFO_upper ]] || eval ! exec "$1<> " <(:) 2>"/dev/null"; then
        echo "$FUNCNAME: $1: Could not create FIFO" >&2
        return "1"
    fi
}
_

次の関数は、GNU bash, version 3.2.57(1)-release (x86_64-Apple-darwin17)を使用してテストされました。オペレーティングシステムはmacOS High Sierraでした。この関数は、名前付きのFIFO一時ディレクトリに、それを作成したプロセスだけが知っているを作成することから始まります。次に、ファイル記述子がリダイレクトされます。最後に、FIFOは、一時ディレクトリを削除することにより、ファイル名からリンク解除されます。これにより、FIFOが匿名になります。

_MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
        MakeFIFO_file="$MakeFIFO_directory/pipe"
        mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
        ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}
_

上記の機能は、両方のオペレーティングシステムで動作する単一の機能に組み合わせることができます。以下はそのような関数の例です。ここでは、真に匿名のFIFOを作成しようとします。失敗した場合、名前付きのFIFOが作成され、匿名のFIFOに変換されます。

_MakeFIFO() {
    MakeFIFO.SetStatus() {
        return "${1:-$?}"
    }
    MakeFIFO.CleanUp() {
        local "MakeFIFO_status=$?"
        rm -rf "${MakeFIFO_directory:-}"    
        unset "MakeFIFO_directory"
        MakeFIFO.SetStatus "$MakeFIFO_status" && true
        eval eval "${MakeFIFO_handler:-:}'; true'" 
    }
    local "MakeFIFO_success=false" "MakeFIFO_upper=$(ulimit -n)" "MakeFIFO_file=" 
    MakeFIFO_handler="$(trap -p EXIT)"
    MakeFIFO_handler="${MakeFIFO_handler#trap -- }"
    MakeFIFO_handler="${MakeFIFO_handler% *}"
    trap -- 'MakeFIFO.CleanUp' EXIT
    until "$MakeFIFO_success"; do
        [[ $# -eq 1 && ${#1} -le ${#MakeFIFO_upper} && -z ${1%%[0-9]*}
        && 10#$1 -gt 2 && 10#$1 -lt MakeFIFO_upper ]] || break
        if eval ! exec "$1<> " <(:) 2>"/dev/null"; then
            MakeFIFO_directory=$(mktemp -d) 2>"/dev/null" || break
            MakeFIFO_file="$MakeFIFO_directory/pipe"
            mkfifo -m 600 $MakeFIFO_file 2>"/dev/null" || break
            ! eval ! exec "$1<> $MakeFIFO_file" 2>"/dev/null" || break
        fi
        MakeFIFO_success="true"
    done
    rm -rf "${MakeFIFO_directory:-}"
    unset  "MakeFIFO_directory"
    eval trap -- "$MakeFIFO_handler" EXIT
    unset  "MakeFIFO_handler"
    "$MakeFIFO_success" || { echo "$FUNCNAME: $1: Could not create FIFO" >&2; return "1"; }
}
_

以下は、匿名FIFOを作成し、同じFIFOにテキストを書き込む例です。

_fd="6"
MakeFIFO "$fd"
echo "Now is the" >&"$fd"
echo "time for all" >&"$fd"
echo "good men" >&"$fd"
_

以下は、匿名FIFOの内容全体を読み取る例です。

_echo "EOF" >&"$fd"
while read -u "$fd" message; do
    [[ $message != *EOF ]] || break
    echo "$message"
done
_

これにより、次の出力が生成されます。

_Now is the
time for all
good men
_

以下のコマンドは、匿名FIFOを閉じます。

_eval exec "$fd>&-"
_

参照:
後で使用するための匿名パイプの作成
公的に書き込み可能なディレクトリ内のファイルは危険です
シェルスクリプトセキュリティ

1
David Anderson

Htamasからの素晴らしく明るい答えを使用して、1つのライナーで使用するように少し変更しました。

# create a temporary named pipe
PIPE=(`(exec 0</dev/null 1</dev/null; (( read -d \  e < /proc/self/stat ; echo $e >&2 ; exec tail -f /dev/null 2> /dev/null ) | ( read -d \  e < /proc/self/stat ; echo $e  >&2 ; exec tail -f /dev/null 2> /dev/null )) &) 2>&1 | for ((i=0; i<2; i++)); do read e; printf "$e "; done`)
# attach it to file descriptors 3 and 4
exec 3>/proc/${PIPE[0]}/fd/1 4</proc/${PIPE[1]}/fd/0
...
# kill the temporary pids
kill ${PIPE[@]}
...
# anything we write to fd 3 can be read back from fd 4
echo 'Hello world!' >&3
head -n1 <&4
...
# close the file descriptor when we are finished (optional)
exec 3>&- 4<&-
0