JavaScript が無効になっているよ!

process substitution を待つスクリプト作り

 ·  ☕ 3 分(読了時間)  ·  🐨 Puliyo

目的

Process substitution は stdout と stderr の出力にオリジナルメッセージを付加する時に便利。

しかし問題が。

Bash は process substitution の子プロセスが完了するまで待ってくれない。

簡単な例:

1
2
3
4
5
6
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

上記、cat が 1 ~ 5 をすべてプリントする前にプロンプトが戻ってきちゃっている。

この現象の対応策が下のリンクにいろいろあったが、

https://qiita.com/takei-yuya@github/items/7afcb92cfe7e678b7f6d

個人的にここで紹介されている回避策のほうが好みだったのでちろっと紹介。

https://unix.stackexchange.com/questions/388519/bash-wait-for-process-in-process-substitution-even-if-command-is-invalid

Process substitution 出力加工例

下記スクリプトを用意した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
# sbst.sh

echo_info() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ INFO: $@";
}

echo_error() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ ERROR: $@" 1>&2
}

prepend_info() { while IFS='' read -r line; do echo_info "$line"; done; };
prepend_err() { while IFS='' read -r line; do echo_error "$line"; done; };

exec > >(prepend_info)
exec 2> >(prepend_err)

exec > >(prepend_info) で stdout の出力を prepend_info に流すようにしている。
prepend_info では echo_info を呼び出していて、出力を自分の好みのメッセージを加えて echo している。

同様に、exec 2> >(prepend_err) で stderr の出力加工している。

実際に動作するか、上記コマンドのコマンドを追加

1
2
echo Hello World
2> echo Redirecting this msg to stderr

これを実行すると

1
2
3
4
5
[root@host bin]# bash sbst.sh
[root@host bin]# 2020-10-05T06:32:41+0000 117310 ERROR: Redirecting this msg to stderr
2020-10-05T06:32:41+0000 117310 INFO: Hello World

[root@host bin]#

こんな感じで、時刻とPIDをメッセージに加えて出力している。

2020-10-05T06:32:41+0000 117310 INFO: Hello World
2020-10-05T06:32:41+0000 117310 ERROR: Redirecting this msg to stderr

ただこのままだと2つの問題が:

  1. 全部出力する前にプロンプトが返ってしまう
  2. 順序がおかしくなっている

(1) については、題名のとおり、bash が process substitution の子プロセスを待たないのが原因
(2) については、マルチプロセッシングのせいで、ようは二番目の echo が左記に完了してしまっているため。この順序の問題は今回は無視する。

回避策:子プロセス完了するまで待つ

回避策は意外とシンプル。

上のコマンドを { var_name=$( {} 3>&1 >&4 4>&-); } 4>&1 で包んでしまえばいい。

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash

{ proc_sbst=$( {

echo_info() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ INFO: $@";
}

echo_error() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ ERROR: $@" 1>&2
}

prepend_info() { while IFS='' read -r line; do echo_info "$line"; done; };
prepend_err() { while IFS='' read -r line; do echo_error "$line"; done; };

exec > >(prepend_info)
exec 2> >(prepend_err)

echo Hello World
>&2 echo Redirecting this msg to stderr

} 3>&1 >&4 4>&-); } 4>&1

上記実行すると親は子を待つのでプロンプトが全部終了したときに返ってきている。

1
2
3
4
[root@host bin]# bash sbst.sh
2020-10-05T07:13:02+0000 117640 INFO: Hello World
2020-10-05T07:13:02+0000 117640 ERROR: Redirecting this msg to stderr
[root@host bin]#

回避策実施しつつ、stdout と stderr を加工したくないとき

exec で stdout と stderr を関数に流しているが、これは fd 1 (デフォのstdout) と fd 2 (デフォのstderr)だけが対象である。

つまり別の fd に流してしまえばメッセージが加工されない。

fd は下の例①のようにファイルでもいいし、例②のように fd 1 と fd 2 のコピーでもいい。

例①:ファイルに出力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
echo Hello World > /tmp/stdout.log
bad_command 2> /tmp/stderr.log

...

[root@host bin]# bash sbst.sh
[root@host bin]# cat /tmp/std*
sbst.sh: line 45: bad_command: command not found
Hello World
[root@host bin]#

例②:stdout と stderr のコピーに出力

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash

{ proc_sbst=$( {

exec 8>&1
exec 9>&2

echo_info() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ INFO: $@";
}

echo_error() {
    echo $(date '+%Y-%m-%dT%H:%M:%S%z') "$$ ERROR: $@" 1>&2
}

prepend_info() { while IFS='' read -r line; do echo_info "$line"; done; };
prepend_err() { while IFS='' read -r line; do echo_error "$line"; done; };

exec > >(prepend_info)
exec 2> >(prepend_err)

echo Hello World >&8
bad_command 2>&9

} 3>&1 >&4 4>&-); } 4>&1

...

[root@host bin]# bash sbst.sh
Hello World
sbst.sh: line 45: bad_command: command not found
[root@host bin]#

スクリプト上部の exec 8>&1exec 9>&2 で fd 1 と fd 2 のコピー作成している。
fd 1 は fd 8 に、fd 2 は fd 9 に。
ということで、出力を加工なしstdoutしたければ fd 8 へリダイレクト、出力を加工なしstderrしたければ fd 9 へリダイレクトすればよい。

シェア
支援