【おうどんの基礎講座】標準入出力ってなに? stdin・stdout・stderrの違いと>・2>&1・パイプの使い方

標準入出力ってなに? stdin・stdout・stderrの違いと>・2>&1・パイプの使い方

こんにちは、おうどんです🍜

うどん屋の閉店後。 今日の仕込み結果を、スクリプトに報告させます。

# udon.sh
echo "kitsune udon: ready"
echo "tempura udon: sold out" >&2

画面がうるさいので、結果はファイルにしまっておくことにしました。

bash udon.sh > out.txt

画面は静かに……

tempura udon: sold out

……ならない。

店長(おうどん)「え、ファイルに出してって言ったよね?」

おわび係「はい、配膳口の分はファイルに出ました。わたしはおわび係なので、店頭で叫びます」

店長「じゃあファイルのほうは?」

おわび係「天ぷらが売り切れたことは、記録に残りません」

……いちばん残してほしい話が、残っていない。

(会話は説明用の架空の場面です。出力は今回の検証環境の Git Bash 5.2.26 で実行した結果です)

これが、今日の主役の標準入出力です。 プログラムには最初から3つの窓口がついていて、> は、そのうちの1つしか動かしていなかったんです。

最初に、初心者向けのチェックリストです。

  • 結果もエラーも1つのファイルに残すなら > log.txt 2>&1 と書く(この順番で)
  • いらない出力を捨てるときは、捨てたい窓口だけを /dev/null に向ける(> /dev/null は結果だけ、2> /dev/null はエラーだけ)
  • パイプ | で次のコマンドに渡るのは、標準出力だけ
目次

標準入出力は、プログラムについている3つの窓口

うどん屋で言うと、注文口・配膳口・おわび係の3つです。 どのプログラムも、生まれたときからこの3つの窓口を持っています。

番号名前略称うどん屋でいうとふだんつながっている先
0標準入力stdin注文口キーボード
1標準出力stdout配膳口画面
2標準エラー出力stderrおわび係画面

POSIX(UNIX 系の OS の共通ルール)の stdin の説明では、この3つの番号はそれぞれ 0・1・2 と決められていて、標準入力はふつうの入力、標準出力はふつうの出力、標準エラー出力は診断用の出力(エラーや警告などのメッセージ)に使うもの、とされています。

左の番号はファイル記述子(プログラムが開いている入出力の通し番号)と呼ばれます。上級者向けの章でもう一度出てくるので、ここでは「窓口の番号」と覚えておけば十分です。

冒頭の udon.sh の >&2 は、「この echo は2番の窓口(おわび係)から出して」という意味です。 何もせずに動かすと、こうなります。

bash udon.sh
kitsune udon: ready
tempura udon: sold out

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

画面だけ見ると、2行とも同じに見えます。 配膳口もおわび係も、ふだんは同じ画面につながっているからです。

同じ画面に出るなら、窓口を分ける意味ある?

おわび係「あります。お客さんの丼に、おわびの言葉が入らないように分けているんです」

……丼に「売り切れです」と書いた紙が浮いていたら、たしかに困る。

窓口が分かれているのは、結果(データ)とメッセージ(人が読むもの)を混ぜないためです。結果だけを次のコマンドやファイルに渡し、エラーは人の目に届くようにできます。

店長「じゃあ全部おわび係から出せば、お客さんに一番気持ちが伝わるのでは?」

……誠意はこもっても、うどんが1杯も届かなくなります。

> と >> と <:窓口の行き先を変える

窓口の行き先を変える書き方を、リダイレクト(向け先の変更)と言います。 基本の3つは、配膳口の行き先を変える >、>> と、注文口の入口を変える < です。

GNU Bash のマニュアルの Redirections の節では、番号を書かないとき、< で始まるリダイレクトは標準入力(0番)、> で始まるリダイレクトは標準出力(1番)が対象になる、とされています。 つまり > は 1> の省略形です。冒頭で2番がそのまま画面に出たのは、このためです。

> は上書き、>> は追記

echo "kitsune" > orders.txt
echo "kake" >> orders.txt
cat orders.txt
echo "---"
echo "zaru" > orders.txt
cat orders.txt
kitsune
kake
---
zaru

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

>> は、ファイルの最後に書き足します。 > は、ファイルの中身を消してから書きます。最後の > で、きつねとかけの注文票が消えました。

店長「ログは毎日 > で書いてるから、ずっと最新で便利だよ」

……昨日のログは、今朝の > が音もなく片付けています。

< はファイルを注文口に流しこむ

printf 'kitsune\nkake\nzaru\n' > menu.txt
wc -l menu.txt
wc -l < menu.txt
tr a-z A-Z < menu.txt
3 menu.txt
3
KITSUNE
KAKE
ZARU

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

wc -l menu.txt は、wc 自身がファイルを開くので、ファイル名も出します。 wc -l < menu.txt は、シェルがファイルを開いて注文口(標準入力)に流しこむので、wc はどこから来た注文か知りません。だからファイル名が出ないんです。

tr(文字を置き換えるコマンド)は、ファイル名を受け取らず、標準入力だけを読むコマンドです。こういうコマンドには < で渡します。

wc「行数は3です。どこの注文票かは、聞かないでください」

……口の固い店員。

2> と 2>&1:おわび係の行き先を変える

> が配膳口なら、おわび係の行き先は 2> で変えます。番号を書けばいいだけです。

bash udon.sh 2> err.txt
echo "--- err.txt"
cat err.txt
kitsune udon: ready
--- err.txt
tempura udon: sold out

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

今度は逆に、エラーだけファイルに入って、結果が画面に残りました。

両方を1つのファイルに入れたいときは、2>&1 を使います。 2>&1 は「2番の窓口を、いま1番がつながっている先と同じ所につなぐ」という意味です。&1 の & は、ファイル名ではなく窓口の番号だという印です。

bash udon.sh > all.txt 2>&1
echo "--- all.txt"
cat all.txt
--- all.txt
kitsune udon: ready
tempura udon: sold out

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

画面には何も出ず、2行ともファイルに入りました。 bash では &> all.txt とも書けます。GNU Bash のマニュアルでは、&>word と >&word のうち &>word が推奨の書き方で、意味は >word 2>&1 と同じ、とされています。

2>&1 の & を付け忘れたら?

2>1 は「2番を、1 という名前のファイルに出す」になります。 フォルダに 1 というファイルが生えたら、だいたいこれです。

いらない出力は /dev/null に捨てる

/dev/null は、書きこんだものを全部捨てる特別なファイルです。

bash udon.sh > /dev/null
bash udon.sh > /dev/null 2>&1
echo "(nothing above)"
tempura udon: sold out
(nothing above)

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

1行目は結果だけ捨てたので、おわびが画面に残りました。2行目は両方捨てたので、何も出ていません。 Windows のコマンドプロンプトでは、/dev/null の代わりに nul を使います(後の章で実行例があります)。

おわび係「わたしを /dev/null に向けるのは、やめてもらえますか」

店長「でも毎回同じおわびだし……」

……おわびを聞かずに捨てた日に限って、本物のトラブルが起きます。エラーを捨てるのは、中身を確かめてからにしましょう。

パイプ | で次に渡るのは、標準出力だけ

パイプ | は、左のコマンドの配膳口を、右のコマンドの注文口につなぐものです。 おわび係は、つながれていません。

bash udon.sh | tr a-z A-Z
tempura udon: sold out
KITSUNE UDON: READY

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

tr を通ったのは配膳口の1行だけで、大文字になっています。 おわびは tr を通らずに画面へ直行したので、小文字のままです。今回の検証では、おわびのほうが先に表示されました。2つの窓口は別々に画面へ届くので、並び順は保証されません。

エラーもパイプに流したいときは、2>&1 |、bash なら短く |& と書けます。

bash udon.sh |& tr a-z A-Z
KITSUNE UDON: READY
TEMPURA UDON: SOLD OUT

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

おわび係「パイプのほうが近道なのに、わたしだけ徒歩で画面に向かっています」

店長「その徒歩のおかげで、grep に食べられずに済んでるんだよ」

……おわびが grep で絞りこまれて消えないのは、この仕組みのおかげです。

じゃあ、エラーメッセージを grep で探したいときは?

そのときこそ |& か 2>&1 | の出番です。 おわび係にも、たまにはパイプに乗ってもらいましょう。

パイプの左側が失敗したときの終了コードの扱いは、Bashで処理が失敗したのに成功扱い? pipefail・PIPESTATUSの記事にまとめています。

🔰 ここまで読めば今日から困らない

  • プログラムには、注文口(0番:標準入力)、配膳口(1番:標準出力)、おわび係(2番:標準エラー出力)の3つの窓口がある
  • > は配膳口だけ。おわびは 2>。両方なら > ファイル 2>&1
  • >> は追記、> は上書き
  • パイプ | は配膳口だけを次のコマンドに渡す。おわびも渡すなら |&(bash)か 2>&1 |

迷ったら、まずこれです。

bash udon.sh > all.txt 2>&1

これ、毎回 2>&1 を書くの、呪文っぽい。

「にー・だい・あんど・いち」。 ……声に出すと、余計に呪文でした。意味で覚えるなら「おわび係も、配膳口と同じ所へ」です。

店長「2>&1 を書き忘れても、ログを見ればエラーがあったかは分かるよね?」

……そのログに、エラーは入っていません。それが冒頭の事件です。

標準入出力とセットで覚えたいのが終了コード(プログラムが最後に返す成功・失敗の番号)です。 エラーメッセージは「何があったか」を人に伝える窓口、終了コードは「成功したか」を機械に伝える番号、と役割が違います。詳しくは【おうどんの基礎講座】終了コードってなに?でどうぞ。

ここから先は中級者向け。 2>&1 の順番、ファイルに出すと順番が入れ替わる話、Windows での書き方、完成コードです。

2>&1 は、書く順番で結果が変わる

2>&1 は「いまの1番の行き先をコピーする」ものです。合流させる約束ではありません。 だから、書く位置で結果が変わります。

bash udon.sh 2>&1 > only.txt
echo "--- only.txt"
cat only.txt
tempura udon: sold out
--- only.txt
kitsune udon: ready

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

> only.txt 2>&1 と順番を逆にしただけで、おわびがファイルに入らず、画面に出ました。

GNU Bash のマニュアルでも、リダイレクトは左から順に処理され、ls > dirlist 2>&1 は両方をファイルに、ls 2>&1 > dirlist は標準出力だけをファイルに向ける(標準エラー出力は、標準出力がファイルに向く前の行き先をコピーしたため)、と説明されています。

窓口が実際にどこにつながっているかを、のぞいてみました。 Git Bash では /proc/$$/fd/番号 で、その番号の窓口の行き先を確認できます($$ はそのシェルのプロセス番号です)。

# fds.sh
for n in 0 1 2; do
  echo "fd $n -> $(basename "$(readlink /proc/$$/fd/$n)")"
done
bash fds.sh < menu.txt > fds.txt 2> err.txt
cat fds.txt
bash fds.sh > fds2.txt 2>&1 < menu.txt
cat fds2.txt
fd 0 -> menu.txt
fd 1 -> fds.txt
fd 2 -> err.txt
fd 0 -> menu.txt
fd 1 -> fds2.txt
fd 2 -> fds2.txt

(今回の検証環境の Git Bash 5.2.26 で実行した結果です。実際の実行では2つの結果のあいだに区切りの行を出していましたが、ここでは省いています)

> fds2.txt 2>&1 の順なら、2番も fds2.txt を向いています。 逆の順 2>&1 > fds3.txt で同じことをすると、2番は fds3.txt ではなく、そのとき1番がつながっていた元の出力先(今回の検証では、実行結果を記録するための出力先)を向いていました。

おわび係「配膳口さんと同じ所へ、と言われたので、配膳口さんが今いる場所に行きました」

店長「そのあと配膳口はファイルに引っ越したんだけど」

おわび係「引っ越し先は聞いていません」

……言われたとおりにしか動かない、まじめすぎる店員。

覚え方は「行き先を決めてから、コピーする」です。> ファイル が先、2>&1 が後。

じゃあ 2>&1 を2回書けば、どっちの順番でも安心?

2>&1 > ファイル 2>&1 なら、最後に新しい行き先をコピーし直すので、両方ともファイルに入ります。 でも、最初のコピーは余分です。> ファイル 2>&1 の順番で書くほうが、ずっとすっきり。

ファイルに出すと、出力の順番が入れ替わる

画面では順番どおりに出ていたのに、> log.txt 2>&1 でファイルに入れたら順番が変わった。 これは Python などが出力をためてから出すために起きます。

# order.py
import sys
print("1: start (stdout)")
print("2: error (stderr)", file=sys.stderr)
print("3: end (stdout)")
python order.py > log.txt 2>&1
cat log.txt
2: error (stderr)
1: start (stdout)
3: end (stdout)

(今回の検証環境の Windows 10・Python 3.13.1 を Git Bash から実行した結果です)

2番目に出したはずのエラーが、先頭に来ました。

Python の sys モジュールのドキュメントでは、対話モード(画面につながっているとき)の標準出力は行単位でためる(1行ごとに出す)一方、そうでないときは通常のファイルと同じくまとめてためる(ブロック単位のバッファリング)、と書かれています。標準エラー出力は、Python 3.9 からどちらの場合も行単位です。

つまりファイルに出すと、1番は 1: と 3: を手元にためて最後にまとめて出し、2番は1行ごとにすぐ出す。だから先にエラーが届きます。

配膳口「うどんは、3杯まとめて運んだほうが効率がいいので」

おわび係「おわびは、すぐ言うのが基本なので」

……どちらも正しいのに、伝票の順番だけがぐちゃぐちゃになる。

同じドキュメントにあるとおり、-u オプションか環境変数 PYTHONUNBUFFERED で、ためずに出すようにできます。

python -u order.py > log_u.txt 2>&1
cat log_u.txt
1: start (stdout)
2: error (stderr)
3: end (stdout)

(今回の検証環境の Python 3.13.1 で実行した結果です。PYTHONUNBUFFERED=1 python order.py と、print("1: start (stdout)", flush=True) のように1行目を書き出してからエラーを出す書き方でも、同じ順番になりました)

店長「じゃあ全部のスクリプトに -u を付けておこう」

……ためずに出すぶん、書きこみの回数は増えます。まずは、順番が大事なログを出すスクリプトだけで十分です。

Windows のコマンドプロンプトと PowerShell

コマンドプロンプト(cmd.exe)は、ほぼ同じ書き方

cmd /c "(echo ok& echo ng 1>&2) > cmd_out.txt 2>&1"
Get-Content .\cmd_out.txt
cmd /c "(echo ok& echo ng 1>&2) > nul 2>&1"
'(nothing above)'
ok
ng
(nothing above)

(今回の検証環境の Windows PowerShell 5.1 から cmd.exe を呼んで実行した結果です。ng の行は、実際には末尾に空白が1つ付いていました。echo ng 1>&2 の ng と 1>&2 のあいだの空白まで echo が出力するためです)

> ファイル 2>&1 の順番の考え方も同じです。捨てる先は /dev/null ではなく nul です。

PowerShell は窓口が6つある

PowerShell は少し事情が違います。 PowerShell の about_Redirection(Windows PowerShell 5.1 版)では、リダイレクトできる出力ストリーム(出力の通り道)が 1 Success・2 Error・3 Warning・4 Verbose・5 Debug・6 Information と6つあり、Success と Error が他のシェルの stdout と stderr に近いもの、とされています。

$r = & { Write-Output "ok"; Write-Warning "warn"; Write-Error "ng" } 3>&1 2>&1
foreach ($x in $r) { $x.GetType().Name + ": " + $x }
String: ok
WarningRecord: warn
ErrorRecord: ng

(今回の検証環境の Windows PowerShell 5.1.19041 で実行した結果です)

3>&1 で警告、2>&1 でエラーを Success に合流させると、文字列ではなく WarningRecord、ErrorRecord という「記録の形」のまま届きました。PowerShell は文字ではなくオブジェクト(データのかたまり)を流すからです。

PowerShell「おわびも、書類の形式でお届けします」

……おわびに様式があるタイプのお店。

PowerShell 5.1 の > は UTF-16 で書く

ここは実務でよくハマります。

"kake" > ps_out.txt
$b = [IO.File]::ReadAllBytes("$PWD\ps_out.txt")
($b | ForEach-Object { $_.ToString('X2') }) -join ' '
FF FE 6B 00 61 00 6B 00 65 00 0D 00 0A 00

(今回の検証環境の Windows PowerShell 5.1.19041 で実行した結果です)

先頭の FF FE は UTF-16LE(1文字を2バイト以上で表す文字コード)の印で、k が 6B 00 のように2バイトになっています。 about_Redirection では、PowerShell の > は Out-File に何も指定せずに渡すのと同じ働き、とされていて、Out-File(5.1 版)の -Encoding の既定値は unicode(UTF-16 のリトルエンディアン)です。

文字のあいだに 00 が挟まっているので、今回の検証では Git Bash の grep -c kake ps_out.txt が 0(見つからない)を返しました。文字コードを決めたいときは Out-File -Encoding utf8 のように明示しましょう(5.1 の utf8 は BOM 付きです)。

店長「PowerShell で出したログを bash で grep したら、0件だった」

PowerShell「1文字ずつ、ていねいに間をあけておきました」

……そのていねいさで、検索が全部すり抜ける。

PowerShell の > は「大なり」ではない

about_Redirection には、こんな注意も書かれています。

if (36 > 42) { "true" } else { "false" }
Get-ChildItem -Name
Get-Content .\42
false
42
ps_out.txt
36

(今回の検証環境の Windows PowerShell 5.1.19041 で実行した結果です)

false と出たので一見正しいのですが、42 という名前のファイルができて、中に 36 が入っています。 PowerShell で大小を比べるときは -gt(より大きい)・-lt(より小さい)を使います。

36「42 より大きいですか、と聞かれたので、42 さんの家に住みこみました」

……比べるどころか、同居が始まっている。

完成コード:結果とエラーを窓口で分けるスクリプト

3つの窓口を、役割どおりに使い分けたひな形です。

  • 注文は標準入力から読む
  • できあがったうどん(データ)は標準出力へ
  • 問題と集計(人が読むメッセージ)は標準エラー出力へ
  • 失敗があれば終了コード 1
#!/usr/bin/env bash
# usage: make_udon.sh < orders.txt > done.txt 2> error.log
# stdin : one order per line
# stdout: finished orders (data)
# stderr: problems (messages for humans)
set -u

err() { printf 'ERROR: %s\n' "$*" >&2; }

ok=0
ng=0
while IFS= read -r order || [[ -n $order ]]; do
  case $order in
    kitsune|kake|zaru)
      printf '%s udon\n' "$order"
      ok=$((ok + 1))
      ;;
    *)
      err "unknown menu: $order"
      ng=$((ng + 1))
      ;;
  esac
done

printf 'summary: ok=%d ng=%d\n' "$ok" "$ng" >&2
[[ $ng -eq 0 ]]

3つの窓口を全部ファイルにつないで動かします。

printf 'kitsune\nkarubonara\nzaru\n' > orders.txt
bash make_udon.sh < orders.txt > done.txt 2> error.log
echo "rc=$?"
echo "--- done.txt"
cat done.txt
echo "--- error.log"
cat error.log
rc=1
--- done.txt
kitsune udon
zaru udon
--- error.log
ERROR: unknown menu: karubonara
summary: ok=2 ng=1

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

done.txt には、できたうどんだけが入っています。次のプログラムに渡すなら、この中身だけで済みます。 カルボナーラの件は、ちゃんと error.log に残りました。

パイプにつなぐと、配膳口の分だけが sort に渡ります。

bash make_udon.sh < orders.txt | sort -r
ERROR: unknown menu: karubonara
summary: ok=2 ng=1
zaru udon
kitsune udon

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

sort -r(逆順に並べる)されたのはうどんの2行だけで、おわびの2行は並べ替えに混ざっていません。

おわび係「カルボナーラは、うちでは出せませんでした」

店長「そもそも、誰が注文したの」

……注文口に何でも流しこめるのが、標準入力のいいところであり、こわいところです。

while IFS= read -r の書き方の理由は、bashのwhile readでループ後に変数が消える・最終行が読まれない原因と対処で説明しています。

集計の summary は結果だから、配膳口に出したほうがいいのでは?

設計しだいです。 ただ、done.txt を次のプログラムが1行1杯として読むなら、そこに summary: が混ざると1杯多く数えられます。データの窓口には、データだけを置いておくと平和です。

切り分けチェックリスト

症状まず疑うこと直し方
> log.txt したのにエラーが画面に出るエラーは2番の窓口> log.txt 2>&1
2>&1 を書いたのにエラーがファイルに入らない書く順番> ファイル を先、2>&1 を後
パイプの先の grep でエラーが見つからないパイプは1番だけ|& か 2>&1 |
フォルダに 1 という名前のファイルができた& の付け忘れ2>1 を 2>&1 に
昨日のログが消えた> は上書き>> で追記
ログの中でエラーの位置がずれている出力のバッファリングpython -u、flush=True
PowerShell で出したファイルを grep できない5.1 の > は UTF-16Out-File -Encoding utf8
PowerShell で 36 > 42 のあとファイルが増えた> はリダイレクト-gt・-lt で比べる

(表の中の \| は Markdown の表を崩さないための書き方で、実際のコマンドでは | です)

表、8行もある。

店長「全部の行で、窓口の番号を確かめれば終わるよ」

……結局、番号札を見れば解決するお店でした。

どれから見ればいい?

上から2行で、だいたいの事件は片付きます。 「どの窓口から出たか」と「どの順番で書いたか」です。

ここから先は上級者向け。読み飛ばしてもOKです。 窓口の正体と、つながっている先によってプログラムの振る舞いが変わる話です。

窓口の正体は、番号つきのファイル記述子

標準入出力は、特別な装置ではなく、0・1・2 番のファイル記述子に過ぎません。 3番以降も、自分で開いて使えます。

exec 3> fd3.txt
echo "to fd3" >&3
exec 3>&-
cat fd3.txt
to fd3

(今回の検証環境の Git Bash 5.2.26 で実行した結果です。exec 3> ファイル はいまのシェルで3番を開き、exec 3>&- で閉じる書き方です)

GNU Bash のマニュアルによると、bash はリダイレクトの中で /dev/fd/番号、/dev/stdin、/dev/stdout、/dev/stderr という名前を特別に扱い、それぞれ対応する番号(/dev/stderr なら2番)を複製します。

{ echo "via /dev/stderr" > /dev/stderr; } 2> dev_err.txt
cat dev_err.txt
via /dev/stderr

(今回の検証環境の Git Bash 5.2.26 で実行した結果です)

> /dev/stderr は >&2 と同じく2番に出すので、外側の 2> dev_err.txt でファイルに入りました。

4番の窓口に、天かす係を置いてもいい?

置けます。 ただ、天かす係がいることを知っているのは、店長だけです。ほかのコマンドは0〜2番しか見に来ないので、説明書きを忘れずに。

GNU Bash のマニュアルには、noclobber(上書き防止)の設定も載っています。有効にすると、すでにある通常のファイルへの > は失敗し、どうしても上書きしたいときは >| を使います。

set -o noclobber
echo "tanuki" > menu.txt
echo "rc=$?"
cat menu.txt
echo "tanuki" >| menu.txt
cat menu.txt
set +o noclobber
s1.sh: line 66: menu.txt: cannot overwrite existing file
rc=1
kitsune
kake
zaru
tanuki

(今回の検証環境の Git Bash 5.2.26 で、スクリプトとして実行した結果です。1行目はスクリプトのフルパスを、ファイル名の s1.sh だけに短くしています。対話シェルでは先頭が bash: になります)

店長「全部のシェルで noclobber をオンにすれば、ログ消失事件は撲滅できる」

……今度は、上書きしたい日に毎回 >| と打つ係が必要になります。

つながっている先によって、プログラムは振る舞いを変える

プログラムは、自分の窓口の先が画面(端末)なのか、ファイルやパイプなのかを調べられます。 画面かどうかで、ため方や文字コードを変えるプログラムがあるのは、そのためです。

POSIX の stdin の説明では、標準エラー出力は完全にはバッファリングしない(ためこまない)こと、標準入力と標準出力は対話的な装置(端末)につながっていないと判断できたときに限って完全にバッファリングすることが決められています。先ほどの Python の順番の入れ替わりは、この考え方と同じ形です。

Windows の Python では、文字コードも変わります。

# enc.py
import sys
print("stdout encoding:", sys.stdout.encoding, "isatty:", sys.stdout.isatty())
print("stderr encoding:", sys.stderr.encoding, "isatty:", sys.stderr.isatty())
print("うどん")
python enc.py > enc_file.txt
head -n 2 enc_file.txt
tail -n 1 enc_file.txt | od -An -tx1
python -X utf8 enc.py > enc_utf8.txt
head -n 1 enc_utf8.txt
tail -n 1 enc_utf8.txt | od -An -tx1
stdout encoding: cp932 isatty: False
stderr encoding: cp932 isatty: False
 82 a4 82 c7 82 f1 0d 0a
stdout encoding: utf-8 isatty: False
 e3 81 86 e3 81 a9 e3 82 93 0d 0a

(今回の検証環境の Windows 10・Python 3.13.1 で、Git Bash から実行した結果です。この検証では標準エラー出力も端末につながっていなかったため、どちらも isatty: False です)

ファイルに出すと cp932(Shift_JIS をマイクロソフトが拡張したもの)で書かれ、「うどん」が 82 a4 82 c7 82 f1 になりました。-X utf8(UTF-8 モード)を付けると UTF-8 の e3 81 86 ... です。パイプにつないだ場合も、今回の検証では同じく cp932 でした。

Python の sys のドキュメントでは、Windows ではコンソール(画面)には UTF-8 を使い、ディスク上のファイルやパイプのような文字デバイスでないものにはシステムのロケールのエンコーディング(ANSI コードページ)を使う、とされています。 画面では文字化けしないのに、> log.txt したら文字コードが変わる。理由はこれです。cp932 そのものの話は、PythonでUnicodeDecodeError: ‘cp932’が出る原因と対処にまとめています。

Python「画面のお客さんには UTF-8 で、持ち帰りのお客さんには cp932 でお包みします」

……持ち帰った先が UTF-8 の家だと、包み紙が開けられない。

もう1つ、PowerShell 5.1 から外部のプログラム(ネイティブコマンド)を呼んで 2>&1 すると、標準エラー出力の行は文字列ではなく ErrorRecord になります。

$r = cmd /c "echo kake 1>&2" 2>&1
$r.GetType().FullName
System.Management.Automation.ErrorRecord

(今回の検証環境の Windows PowerShell 5.1.19041 で実行した結果です)

中身は kake というただの文字なのに、PowerShell の中ではエラーの記録として扱われます。外部コマンドの「おわび係」の行は、PowerShell ではエラー扱いになる、と覚えておくと、$ErrorActionPreference = 'Stop' のスクリプトで驚かずに済みます(この設定での動きは、今回は実行していません)。

cmd「かけうどんを、おわびの窓口から出しただけなんですが」

PowerShell「おわびの窓口から出たものは、すべて事故報告書として受理します」

……かけうどん、事故扱い。

まとめ:おわびは、別の窓口から叫んでいた

冒頭では、> out.txt したのに、売り切れのおわびだけが画面に残っていました。

  • プログラムには 0(標準入力)・1(標準出力)・2(標準エラー出力)の3つの窓口がある
  • > は1番だけ。2番は 2>。両方なら > ファイル 2>&1(bash なら &> も可)
  • 2>&1 は「いまの1番の行き先をコピー」。だから順番が大事
  • パイプは1番だけを渡す。2番も渡すなら |& か 2>&1 |
  • ファイルに出すとバッファリングで順番が変わることがある。Python は -u や flush=True
  • PowerShell 5.1 の > は UTF-16。36 > 42 は比較ではなくファイル作成
  • Windows の Python は、ファイルやパイプに出すと通常はシステムのロケールの文字コードを使う(今回の環境では cp932。UTF-8 モードや PYTHONIOENCODING などで変更できる)

おわび係「今日から、ログにも残してもらえるんですね」

店長「2>&1 を後ろに書くって、覚えたからね」

おわび係「前に書いたら?」

店長「……きみは画面で叫ぶ」

……覚え方としては、それで合っています。

店長「よし、今日から全部のコマンドを > /dev/null 2>&1 にして、店を静かにしよう」

……それは、おわび係を毎日ゴミ箱の前に立たせる働き方です。静かになるのは、トラブルに気づくのが遅れるからです。

ファイルへの > を書いたら、2番の窓口がどこへ行くかも一緒に決める。それだけで、ログからおわびが消える事件は起きなくなります。

まずは、毎日動かしているスクリプトの > を1つ探してみてください。 後ろに 2>&1 がなかったら、おわび係の行き先を決めてあげてください。きっと今夜のログには、売り切れの天ぷらまで、ちゃんと残ります🍜

参考資料

  • POSIX:stdin, stdout, stderr — 0・1・2 の番号、標準エラー出力が診断用の出力であること、標準エラー出力は完全にはバッファリングされず、標準入出力は端末でないと判断できるときに限り完全にバッファリングされること
  • GNU Bash Reference Manual:Redirections — 番号を省いたときの < と > の対象、ls > dirlist 2>&1 と ls 2>&1 > dirlist の違い、&> と >&、noclobber と >|、/dev/stdin・/dev/stdout・/dev/stderr の扱い
  • Python ドキュメント:sys — stdout・stderr の用途、対話モードかどうかでのバッファリングの違い、-u と PYTHONUNBUFFERED、Windows でのコンソールとファイル・パイプのエンコーディング
  • PowerShell about_Redirection(5.1) — 6つの出力ストリームと番号、n>&1、> が Out-File と同じ働きであること、36 > 42 がファイルを作ること
  • Out-File(PowerShell 5.1) — -Encoding の既定値が unicode(UTF-16 リトルエンディアン)であること

確認日:2026-10-05。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次