サブシェルが閉じた後でも、サブシェル内のリダイレクトは依然として存在します。

サブシェルが閉じた後でも、サブシェル内のリダイレクトは依然として存在します。

空の状態となしの状態で実行すると、次のスクリプトはそれぞれAとBで期待どおりに実行されます。 Aのドライランに問題があります。1>/dev/nullドライラン後も出力がまだ存在するかのように出力が抑制されます。誰かが問題を説明して解決できますか?

function baz() {
    local file="$1"; shift

    # dry run
    local err=$(source "$file" "$@" 2>&1 1>/dev/null)
    [[ -z $err ]] || { echo "Exiting"; return 1; }

    # live run
    local output=$(source "$file" "$@")
    [[ -z $output ]] || echo "$output"
}

baz <(cat <<EOF
echo "I was given $# argument(s):"  # A
# eecho "I was given $# argument(s):" # B
printf "%s " "$@"
EOF
) 'foo' 'bar'

その他:

$ uname  -a
6.7.3-arch1-2 #1 SMP PREEMPT_DYNAMIC Fri, 02 Feb 2024 17:03:55 +0000 x86_64 GNU/Linux
$ bash --version
GNU bash, version 5.2.26(1)-release (x86_64-pc-linux-gnu)

答え1

コードには2つの別々の問題があります。

  1. 引用符を忘れたので、/をEOF解析すると$#hereドキュメントで拡張されます。$@
  2. ファイルが終了するまで、名前付きパイプ(FIFOとも呼ばれます)から2回読み取ろうとします。

1 の場合は、以下を比較します。

$ bash --norc -s {1..42}
bash-5.3$ cat << EOF
> echo "$#"
> EOF
echo "42"
bash-5.3$ cat << 'EOF'
> echo "$#"
> EOF
echo "$#"

heredocの区切り文字を引用すると(ここには含まれていますが、'EOF'一部のみ引用しても同じ効果があります\EOF)、'E'OF内部拡張は実行されません。

2の場合、より単純な再現は次のようになります。

myfunction() {
  local file="$1"
  stat -Lc '$file (%n) is a %F.' -- "$file"
  echo First round:
  cat -- "$file"
  echo Second round:
  cat -- "$file"
  echo Done.
}
myfunction <(echo some text)

(これはGNUまたはGNUと同様の実装を想定していますstat)。

これは作る:

bash-5.3$ myfunction <(echo some text)
$file (/dev/fd/63) is a fifo.
First round:
some text
Second round:
Done.

<(cmd)名前付きパイプまたはhasに展開され、/dev/fdパイプの内容は一度だけ読むことができます。

Incmd1 <(cmd2)とinのように同時に実行されるcmd1違いは、inの出力はfd 0から簡単に取得できるのに対して、inはfdが出力を読み取るために最初の引数(上記の引数に拡張)を開く必要があることです。 。cmd2cmd2 | cmd1cmd2 | cmd1cmd2cmd1cmd1 <(cmd2)cmd1/dev/fd/63<(cmd2)

どちらの場合も、cmd2出力を一度読み込むと再読み込みできません。

以前のバージョンのbash(5.0以下)では、次のようにエスケープできます。

bash-5.0$ myfunction /dev/fd/3 3<< 'EOF'
> echo "$#"
> EOF
$file (/dev/fd/3) is a regular file.
First round:
echo "$#"
Second round:
echo "$#"
Done.

その時点で、ここの文書はまだ削除された状態で実行されます。 一般ファイル元の Bourne シェル実装と同じか、まだ zsh などの他のシェルにあるのと同じです。最新バージョンでは、bashはhere-docがデッドロックなしでパイプを使用できるほど小さいときにパイプを使用するように切り替えました。

bash-5.3$ myfunction /dev/fd/3 3<< 'EOF'
> echo "$#"
> EOF
$file (/dev/fd/3) is a fifo.
First round:
echo "$#"
Second round:
Done.
bash-5.3$ myfunction /dev/fd/3 3<< EOF | grep -e 9999 -e '[[:alpha:]]'
> $(seq 40000)
> EOF
$file (/dev/fd/3) is a regular file.
First round:
9999
19999
29999
39999
Second round:
9999
19999
29999
39999
Done.

ファイルを複数回読み取るには、次のものが必要です。定期的なファイルなので、オプションは次のとおりです。

  • zshたとえば、次の/dev/fd/3 3<< 'EOF'方法に切り替えます。

  • fifosの代わりに一時ファイルを使用し、独自のクリーンアップを処理する(そして順次実行される)プロセス置換形式に切り替えてzsh使用します。=(...)cmd2cmd1cmd1 =(cmd2)

    zsh% myfunction =(<<'EOF'
    cmdsubst¹ heredoc> echo "$#"
    cmdsubst¹ heredoc> EOF
    cmdsubst¹> )
    $file (/tmp/zsh7xSwLQ) is a regular file.
    First round:
    echo "$#"
    Second round:
    echo "$#"
    Done.
    
  • mktempまたは、bashを使用する必要がある場合は、たとえばbashがあるシステム(最近のほとんど)で手動一時ファイル処理を使用します。ここで元の文書のようにあらかじめ削除しておくと、これを行うことができるので、クリーンアップについて心配する必要はありません。

    file=$(mktemp) || exit
    <<'EOF' cat > "$file"
    echo "$#"
    EOF
    {
      rm -f -- "$file" && myfunction /dev/fd/3
    } 3< "$file"
    

    この方法はLinuxまたはCygwinでのみ動作しますが、/dev/fd/nどこにありますか?魔法のシンボリックリンク元のファイルに。他のシステムでは、オープンはこのように/dev/fd/n動作し、dup(n)ファイルを開くたびにファイルの先頭に戻りません。

あるいは、あなたの場合は、ファイル/fifosの代わりにメモリで実行されるようにコードをeval代わりに使用して渡すこともできます。source

myfunction() {
  local code="$1"; shift
  echo First round:
  eval -- "$code"
  echo Second round:
  eval -- "$code"
}

myfunction "$(cat <<'EOF'
echo "I got $# argument${2+s}${1+: $@}"
EOF
)" more args

/プロセス置換ではなく、$(...)コマンド置換(split + glob(zshでのみ分割)を防ぐために引用符が必要です)に注意してください。<(...)=(...)

これは作る:

First round:
I got 2 arguments: more args
Second round:
I got 2 arguments: more args

気づくcmdsubstこれが何を意味しているにもかかわらず、それプロセスの交換ここではないコマンドの置き換え

答え2

次のセクションでは、ファイル記述子の代わりにソケットパイプストリーム記述子を作成します。

cat <<EOF
...
EOF

(該当するセクションで)ストリームを最初に読み込むと、記述子の# dry run読み取りポインタはファイルの終わりに到達します。

この# live runセクションではEOF(つまり、文字列ではなくファイルの終わりEOF)のみが表示されるため、このセクションでは実際には何も実行されません# live run

この状況で私が一般的に使用する解決策は、mktemp次のようなものを使用することです。

TS="$(mktemp /tmp/subscript-XXXXXX.sh)"

cat >$TS <__EOF
...
__EOF

bash -x $TS fee fi fo fum

rm -f $TS

編集:OPが指摘したように、使用法はmktemp非常に柔軟であり、上記の正確な例に従う必要はありません。この例は私のユースケースの1つから得られ、OPのシナリオに合わせて調整されていません。

答え3

関数はファイルを期待していますが、例では<(cat << EOF ...)プロセスの置き換え時にドキュメントが渡されます。後者について述べたように、「ストリームを初めて読み込むと、記述子の読み取りポインタがファイルの末尾に到達します。」したがって、これらのファイルオブジェクトに対応するには、その内容を一時ファイルにコピーして使用することをお勧めします。

script.sh:

function baz() {
    local file="$1"; shift

    temp=$(mktemp)
    trap '{ rm -f "$temp"; }' EXIT ERR
    cat "$file" > "$temp"

    # dry run
    local err=$(source "$temp" "$@" 2>&1 1>/dev/null)
    [[ -z $err ]] || { echo "Exiting"; return 1; }

    # live run
    local output=$(source "$temp" "$@")
    [[ -z $output ]] || echo "$output"
}

baz <(cat << 'EOF'
# echo "I was given $# argument(s):"  # A
eecho "I was given $# argument(s):" # B
printf "%s " "$@"
EOF
) 'foo' 'bar'

Aの下:

$ source script.sh 'foo' 'bar'
I was given 2 argument(s):
foo bar 

次のB:

$ source script.sh 'foo' 'bar´
Exiting

追加:

  • 前に述べたように、 "[先行] EOFの周りに引用符がない場合、ここで文書を解析すると、位置パラメータがここで拡張されます。"

関連情報