「docker exec -it」の「-t」を完全に理解する

```bash
docker exec -it container bash
```
お馴染み，Dockerコンテナー`container`の中に入るコマンドですね。

`docker` `exec` `bash`の意味は知ってる人が多いと思います。`-i`も検索すればすぐ分かります。しかし，`-t`については情報があまりありません。
そこで，この記事では`-t`がどういう意味を持つのか解説していきます。
# 対象読者
- 何も考えず`docker exec -it container bash`と打っている人
- Linuxの基礎コマンド（`ls`など）を触ったことがある人
- リダイレクト・パイプなど，簡単な入出力操作ができる人
# 公式ヘルプによる`-t`の説明
とりあえず，淡い期待を抱いて公式のヘルプを見てみましょう。
`docker exec --help`によると`-t`は

> -t, --tty
> Allocate a pseudo-TTY

ということらしいです。

……何を言っているのかサッパリ分かりませんね。「Allocate a pseudo-TTY」は和訳すると「疑似TTYを割り当てる」ですが，和訳したからといって分かるものでもありません。

この記事では，このヘルプの意味するところの理解を目指して解説を進めます。
# 実行例の前提環境
以降では`container`という名前のDockerコンテナーを使ってコマンドの実行例を載せていくので，その基となるDockerイメージのソースコードを載せておきます。`docker exec`は実行中のコンテナーにしか実行できないのでcontainerの中ではシェルスクリプトが無限ループするようになっています。
```Dockerfile:Dockerfile
FROM ubuntu
COPY wait.sh .
RUN chmod u+x wait.sh
CMD ./wait.sh
```
```bash:wait.sh
#!/bin/bash
while true
do
    :
done
```
# `-t`以外のところを押さえる
まずは，`docker` `exec` `-i` `container` `bash`について説明しておきます。本丸の`-t`に取り掛かる前に，外堀をきちんと埋めるということです。

:::note info
「もうそんなことは知ってる」という人は次の章まで読み飛ばしてください。
:::
`docker exec`はDockerコンテナーの中でコマンドを実行するコマンドです。冒頭の例`docker exec -it container bash`でいうと，「コンテナー`container`の中で`bash`を実行する」ということになります。

コンテナー内で実行されたコマンドの標準出力・標準エラー出力は，dockerコマンドの標準出力・標準エラー出力として見ることができます。`-it`は要りません。
```shell-session
takechan@virtual-machine:~$ sudo docker exec container echo 'Hello, world!!'
Hello, world!!
takechan@virtual-machine:~$ sudo docker exec container cat bin/
cat: bin: Is a directory
takechan@virtual-machine:~$ 
```

`-i`は`--interactive`の略で，dockerコマンドへの標準入力をDockerコンテナー内のコマンドに伝えます。
下の例だと，コンテナー内のgrepコマンドに
- sh
- terminal
- bash
- console
- zsh
- teletype

の６文字列を標準入力で渡し，その中から「sh」を含む
- sh
- bash
- zsh

を抽出してもらっています。`-i`を付けないと上手く動作しないのが分かりますね。
```shell-session
takechan@virtual-machine:~$ sudo docker exec container grep -e sh << END
> sh
> terminal
> bash
> console
> zsh
> teletype
> END
takechan@virtual-machine:~$ sudo docker exec -i container grep -e sh << END
> sh
> terminal
> bash
> console
> zsh
> teletype
> END
sh
bash
zsh
takechan@virtual-machine:~$ 
```
## `-t`は無くても良いのではないか
ここまでの説明で`-t`以外のところは分かってもらえたかと思います。いよいよ本丸，と言いたいのですが，`-t`のことを一旦忘れて
```bash
docker exec -i container bash
```
としたらどうでしょうか。実行するとコンテナー内でbashが起動されて，
- 標準入力は`-i`によってコンテナー外部に接続される
- 標準出力・標準エラー出力はオプション無しでもコンテナー外部に接続されている

となります。

標準入力・標準出力・標準エラー出力の３つがコンテナー外と繋がっているため，コンテナー外のbashを操作するようにコンテナー内のbashを操作 ~~できます~~ **できそうですができません** 。
「百聞は一見に如かず」ということでやってみましょう。比較のために`-t`を付けた版（記事冒頭のコマンドと同じ）でも同じ操作をしてみます。
```shell-session
takechan@virtual-machine:~$ sudo docker exec -i container bash
echo 'Hello, world!!'
Hello, world!!
exit 0
takechan@virtual-machine:~$ sudo docker exec -it container bash
root@0345c75af34e:/# echo 'Hello, world!!'
Hello, world!!
root@0345c75af34e:/# exit 0
exit
takechan@virtual-machine:~$ 
```
なんと，プロンプトが出てこなくなってしまいました。しかし，ダメ元で`echo 'Hello, world!!'`とコマンドを打つと「Hello, world!!」と出力があるのでbashは動いているようです。
`exit 0`と打てばログアウトはできますが，「exit」という出力は見られません。

:::note info
`-t`が無い版はプロンプトが出ていないので，どれが入力したコマンドでどれが出力なのか分かりにくくなっています
:::

原理はともかく，Dockerコンテナーに入るとき`-t`が無いと困ることは確認できました。それでは，いよいよその原理の解説に入っていきます。
# TTYとは[^TTY・ttyコマンド][^TTY]
公式ヘルプによる`-t`の説明では，「TTY」という用語が出てきます。
[^TTY・ttyコマンド]: 参考：宮崎悟. “第32回　端末について知ろう”. CTC教育サービス. 2021-08. [https://www.school.ctc-g.co.jp/columns/miyazaki/miyazaki32.html](https://www.school.ctc-g.co.jp/columns/miyazaki/miyazaki32.html), (参照2024-06-03).
[^TTY]: 参考：Wikipedia. “テレタイプ端末”. Wikipedia. 2023-10-30. [https://ja.wikipedia.org/wiki/テレタイプ端末](https://ja.wikipedia.org/wiki/テレタイプ端末), (参照2024-06-03).

> -t, --tty
> Allocate a pseudo-TTY

この用語を深掘りしてみましょう。

TTYは「TeleTYpe」（「テレタイプ」と読む）の略で，文字を送信するキーボードと受信した文字を印刷する印刷機が一体になったハードウェアです。コンピューターとの入出力に使われていたらしいです。
キーボードでコンピューターに入力するスタイルは今も使われていますが，コンピューターからTTYに出力があったときは機械がガションガションと動いて紙に文字が印刷されるわけです。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/1cd332e4-e125-0570-d2f2-93abe12ac1fc.png)[Wikipediaの記事](https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%BF%E3%82%A4%E3%83%97%E7%AB%AF%E6%9C%AB)では機械感あふれるTTYの画像が見られますが，ディスプレイなどという物が存在しなかった当時はそれぐらいしか出力の手段が無かったんですね。

https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%BF%E3%82%A4%E3%83%97%E7%AB%AF%E6%9C%AB

ディスプレイが登場すると，ディスプレイの上で仮想的にTTYを再現するソフトウェアが開発されました。皆さんお馴染み，
- Windows Terminal
- Tera Term
- ターミナル（macOS標準のアプリケーション）

などです。これらのソフトウェアが実現する仮想的なTTYでは，紙に文字を印刷する代わりにその文字を画面に表示します。また，タブ・ウィンドウ切り替えなどの操作があるので，仮想的なTTYに対応する数だけキーボード・ディスプレイを用意する必要もありません。キーボードからの入力は，タブ・ウィンドウ切り替えで選択されているTTYからの入力として扱われます。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/f2f39d54-5b7d-3262-d6d2-d02f599bf48d.png)
また，Telnet・SSHも仮想的なTTYを実現する手段と言えます。

物理的なTTYはほぼ絶滅しましたが，仮想的なTTYはコンピューターの中で残り続け今日に至ります。今では「TTY」＝「仮想的なTTY」と言っても差し支えありません。

TTYはコンピューターで動くプロセスの標準入力にキーボードからの入力を伝えたりプロセスからの標準出力・標準エラー出力を受け取って表示したりするわけですが，標準入力・標準出力・標準エラー出力はTTYとのやり取りに使われるだけではありません。リダイレクトによってファイル[^file]とやり取りさせられたり，パイプによって別のプロセスとやり取りさせられたりすることも多々あります。
仮想化によってソフトウェアなどが多少挟まるようにはなりましたが， **CUIプロセスから見て人間に最も近い末端（ターミナル）に位置するのがTTY** だということは登場してからずっと変わりません。![人間とやり取りするときは必ず，人間に最も近いところにTTYがいます](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/0ad9e4fb-abc7-39d8-67c8-ebc0e520c1a7.png)[^flow-without-tty]
[^flow-without-tty]: あくまで「人間とやり取りするときに」TTYを通るだけで，TTYを一切介さずに完結するものは多々あります。
# TTYの検出
Linuxコマンドの中には， **標準入力・標準出力・標準エラー出力がTTYに繋がってるかを検知し，それによって挙動を変える** ものが存在します。本章ではそのことについて詳しく解説します。

## 予備知識
ファイルを ~~結合して~~ 表示するコマンドとして有名なcatですが，標準入力との結合もできます。標準入力を受け付けるのは
- ファイル名の代わりに`-`が指定されたとき
- ファイル名が１つも指定されていないとき

の２つです。ファイル名が１つも指定されていないときは標準入力と結合するファイルが他に無いので，標準入力をそのまま標準出力に流します。

この「そのまま流す」特性がこの記事での実験にすごく便利なので覚えておいてください。 ~~相変わらず結合以外の用途にしか使われないcatコマンドが可哀想~~
## TTYの検出を実験してみる
`cat`は標準入力をそのまま標準出力に渡すだけなので，パイプで他のコマンドと接続しても全く意味の無いように見えます。
```shell-session
takechan@virtual-machine:~$ echo 'Hello, world!!'
Hello, world!!
takechan@virtual-machine:~$ echo 'Hello, world!!' | cat
Hello, world!!
takechan@virtual-machine:~$ echo 'Hello, world!!' | cat | cat | cat
Hello, world!!
takechan@virtual-machine:~$ grep -e sh << END
> sh
> terminal
> bash
> console
> zsh
> teletype
> END
sh
bash
zsh
takechan@virtual-machine:~$ cat << END | grep -e sh
> sh
> terminal
> bash
> console
> zsh
> teletype
> END
sh
bash
zsh
takechan@virtual-machine:~$ cat << END | cat | cat | grep -e sh
> sh
> terminal
> bash
> console
> zsh
> teletype
> END
sh
bash
zsh
takechan@virtual-machine:~$ 
```

しかし，前に述べた「標準入力・標準出力・標準エラー出力がTTYに繋がってるかによって挙動を変える」コマンドは`cat`を噛ませるだけで挙動を変えられます。
```shell-session
takechan@virtual-machine:~$ ls
image  snap
takechan@virtual-machine:~$ ls | cat
image
snap
takechan@virtual-machine:~$ 
```
上のコードブロックでは表現できていませんが，`ls`の方の出力には色が付いていて`ls | cat`の方ではただの白文字が出力されている，という違いもあります。
lsコマンドは「標準入力・標準出力・標準エラー出力がTTYに繋がってるかによって挙動を変える」コマンドの１つです。１つ目の実行例`ls`では，lsコマンドの入出力は全てTTYに繋がっています。一方，２つ目の実行例`ls | cat`ではlsコマンドの標準出力がcatコマンドの標準入力に渡されます。catコマンドは受け取ったデータを何も加工せずTTYに出力（画面に表示）するのでlsコマンドの標準出力は実質的にTTYと繋がっている，とも言えますが，lsコマンドは機械的に「TTYでないところに標準出力が繋がっている」と判断し，１つ目の実行例とは違う挙動を見せたのです。

「TTYに繋がっているか」が挙動を変える基準となっているのは「データをやり取りする相手が人間か，プログラムか」を判断する良い材料だからです。
前にも述べた通り，TTYはCUIプロセスにとって人間に最も近いところでありすぐ向こう側には人間がいます。人間にデータを見せるときは読みやすさを重視することが多いです。色を付けたり改行位置を調整したりするのが有効です。
一方，パイプの向こう側にはプログラムがいます。プログラムにデータを見せるときは機械的処理のしやすさを求められることが多いです。行数が増えても良いからデータ区切りは全て改行に統一する，着色は制御文字が混ざってしまうのでやらない，などの設計が求められます。
ファイル[^file]は人間が見ることもありますが，どちらかと言えばプログラムによって処理される用途が多いので後者にあたります。
ヒアドキュメントは使い捨てのファイル[^file]をその場で生成して標準入力に繋ぐようなものなので，考え方はファイル[^file]と大差ありません。ちなみに，コマンドから見るとヒアドキュメントはパイプでありTTYとは判定されません。
catコマンドのように何もしないコマンドがTTYとの間に挟まっていると不自然な挙動を見せますが，機械的な判断なので仕方ありません。
[^file]: TTYもパイプも厳密にはファイルですが，ここではそういった特殊なファイルを含めないで「ファイル」と呼んでいます。

このように，コマンドの中には「データをやり取りする相手が人間かプログラムか」を判断して挙動を変えるものが存在します。
## TTYの検出には，そのコマンドの思想が現れる
ここまで「TTYに繋がっているかによって挙動を変える」という話をしてきましたが，標準入力・標準出力・標準エラー出力のどれを見てどのように挙動を変えるか，という具体的な仕様はコマンドによってバラバラです。
例えば，lsコマンドは標準出力しか見ておらず標準入力・標準エラー出力がTTYに繋がっているかは気にしません。lsコマンドは標準入力を読まないので，標準入力は判断材料にしていないのでしょう。標準エラー出力にはエラーメッセージが出力されますが，人間向け／プログラム向けで形式に差が無いので気にしていないと考えられます。
上の例では`cat`に影響されない側として登場していたgrepコマンドも「標準入力がTTYかどうか気にしていない」というだけであって，標準出力をTTY以外に繋ぐと文字への着色が無くなります（上の例では着色をコードブロックで表現できていないですが）。「人間が読みにくいような巨大ファイルから必要な行のみ抽出して人間が読む」という用途があるため，標準入力がTTYでなくても標準出力がTTYなら人間向けの動作をすべきという思想があるのでしょう。
一方のbashは，lsコマンドやgrepコマンドと違って標準入力と標準エラー出力を見ます。bashは，そのどちらかがTTYに繋がっていないと判断するとプロンプトや終了時の「exit」[^exit]を出力しなくなります。標準出力がTTYかどうかは気にしていません。
[^exit]: exitコマンドはbashの組み込みコマンドなので，bashのプロセス上で動作しています。
```shell-session
takechan@virtual-machine:~$ bash << END
> echo 'Hello, world!!'
> exit 0
> END
Hello, world!!
takechan@virtual-machine:~$ 
takechan@virtual-machine:~$ bash 2>stderr.txt
echo 'Hello, world!!'
Hello, world!!
exit 0
takechan@virtual-machine:~$ cat stderr.txt
takechan@virtual-machine:~$ 
takechan@virtual-machine:~$ bash
takechan@virtual-machine:~$ echo 'Hello, world!!'
Hello, world!!
takechan@virtual-machine:~$ exit 0
exit
takechan@virtual-machine:~$ 
```

:::note info
`bash 2>stderr.txt`以下ではプロンプトが出ていないので，どれが入力したコマンドでどれが出力なのか分かりにくくなっています。
:::
`bash`は標準入力でコマンドを受け付け，標準エラー出力にプロンプトやエラーメッセージ，「exit」を出力します。標準出力はほぼ使いません[^echo]。
[^echo]: 数少ない例として，組み込みコマンド`echo`は標準出力を使用します。とは言え，動作が単純すぎて人間向けにもプログラム向けにも使えてしまうので判断基準には入っていないのでしょう。

:::note info
`bash 2>stderr.txt`でプロンプトや「exit」が表示されないのは標準エラー出力がstderr.txtにリダイレクトされているから，と考えてしまいますが，その後`cat stderr.txt`を実行しても何も表示されないのでそもそも出力されていないことが分かります。
:::

プロンプトはコマンドの入力要求を人間に分かりやすく伝えるためのものなので
- 人間がプロンプトを読んで（標準エラー出力がTTY）
- 人間がコマンドを入力する（標準入力がTTY）

のどちらかが欠けたらプロンプトを出す意味が無くなる，ということでしょう。加えて，標準エラー出力をプログラムが読む（TTYに繋がっていない）場合にプロンプトはノイズでしかありません。

このように，コマンドは標準入力・標準出力・標準エラー出力がTTYに繋がっているかを検出して各々の思想に基づき挙動を変えます。
# `-t`は，コンテナー内のコマンドに入出力先が人間だと伝える
前章の

> プロンプトや「exit」が表示されない

という現象，見覚えがありますよね。そう，`docker exec -i container bash`（`-t`抜き）の挙動です。

> なんと，プロンプトが出てこなくなってしまいました。しかし，ダメ元で`echo 'Hello, world!!'`とコマンドを打つと「Hello, world!!」と出力があるのでbashは動いているようです。
> `exit 0`と打てばログアウトはできますが，「exit」という出力は見られません。

つまり， **`-t`を抜いたとき，コンテナー内bashの標準入力・標準エラー出力はTTYに繋がっていない** のではないかという仮説が立てられます。
実際に検証してみましょう。今実行しているbashへの標準入力・標準出力・標準エラー出力がどこに繋がっているか見るには`ls -l /proc/${$}/fd/`，標準入力がTTYか判定するには`tty`というコマンドを使います[^TTY・ttyコマンド][^ttyコマンド]。
```shell-session
takechan@virtual-machine:~$ sudo docker exec -i container bash
ls -l /proc/${$}/fd/
total 0
lr-x------ 1 root root 64 Jun  2 04:05 0 -> pipe:[18486]
l-wx------ 1 root root 64 Jun  2 04:05 1 -> pipe:[18487]
l-wx------ 1 root root 64 Jun  2 04:05 2 -> pipe:[18488]
tty
not a tty
exit 0
takechan@virtual-machine:~$ sudo docker exec -it container bash
root@0345c75af34e:/# ls -l /proc/${$}/fd/
total 0
lrwx------ 1 root root 64 Jun  2 04:06 0 -> /dev/pts/0
lrwx------ 1 root root 64 Jun  2 04:06 1 -> /dev/pts/0
lrwx------ 1 root root 64 Jun  2 04:06 2 -> /dev/pts/0
lrwx------ 1 root root 64 Jun  2 04:06 255 -> /dev/pts/0
root@0345c75af34e:/# tty
/dev/pts/0
root@0345c75af34e:/# exit 0
exit
takechan@virtual-machine:~$ 
```
[^ttyコマンド]: 参考：Wikipedia. “tty”. Wikipedia. 2023-11-21. [https://ja.wikipedia.org/wiki/Tty](https://ja.wikipedia.org/wiki/Tty), (参照2024-06-03).

:::note info
`sudo docker exec -i container bash`以下ではプロンプトが出ていないので，どれが入力したコマンドでどれが出力なのか分かりにくくなっています。
:::

`tty`コマンドは標準入力がTTYに繋がっていると判断するとファイル名[^デバイスファイル]，繋がっていないと判断すると「not a tty」を出力します。それを踏まえて上の実験結果を見ると，`-t`を付けることでコンテナー内bashの標準入力がTTYになったことが確認できます。また，`ls -l /proc/${$}/fd/`の結果より，`-t`を付けたときは標準出力・標準エラー出力も標準入力と同じTTYに繋がっていることが読み取れます。一方，`-t`を抜いた場合はコンテナー内bashの入出力がパイプに繋がっているようです。
要するに，「`-t`を抜いたとき，コンテナー内bashの標準入力や標準エラー出力はTTYに繋がっていない」という仮説は正しかったわけです。そして，上の例で`-t`を付けたとき出てきた`/dev/pts/0`こそが **疑似TTY（pseudo-TTY）** です。疑似TTYは前に述べた「仮想的なTTY」の一種と思ってもらって良いです。
[^デバイスファイル]: 「デバイスファイル」と呼ばれる特殊なファイルです。仮想的なものも含め，コンピューターに繋がるデバイスと対応しています。

`-t`を付けた場合，付けなかった場合のそれぞれでTTY周りがどのように接続されているかを図にしてみました。![-tを抜いた場合，コンテナー内のbashはパイプに接続され「パイプに繋がっているからプログラム向けの動作をしよう」と判断してプロンプトを出さない挙動をします。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/50f74c18-bcd8-5ec0-f90b-3cb06a12d7a2.png)![-tを付けた場合，コンテナー内のbashはdockerコマンドが用意した仮想的なTTYに接続され「TTYに繋がっているから人間向けの動作をしよう」と判断してプロンプトを出す挙動をします。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/fd25331e-d4a5-8b43-cdd9-24005dd42548.png)[^sudoが用意したTTY]
この図を踏まえて公式ヘルプの説明
[^sudoが用意したTTY]: 実は，/dev/pts/1もsudoコマンドが用意した仮想的なTTYです。画像では分かりやすさを重視して省略していますが，/dev/pts/1と人間の間にはsudoコマンドとTTY`/dev/tty2`がいます。 

> -t, --tty
> Allocate a pseudo-TTY

を見てみると，ヘルプの言いたいことが大体分かるようになっているのではないでしょうか？
# `-t`を付けるか，付けないか[^「-t」は設計上不要？]
ここまでで`-t`の意味するところは分かったかと思います。それを踏まえると， **コンテナー内のコマンドに人間向けの動作をしてほしいとき`-t`を付ける** ，という運用が適切だと言えます。
[^「-t」は設計上不要？]: 「`docker exec`でコンテナー内コマンドに疑似TTYを繋ぐ方法が`-t`しか無いのはどうなのか」という話もあるのですが，複雑すぎるのでここでは取り上げません。

`docker exec -i container bash`では，コンテナー外でコンテナー内のbashとやり取りしているのはTTY（人間）なのにコンテナー内のbashは「プログラムとやり取りしている」と判断してプログラム向けの動作をしてしまいました。その結果，bashはプロンプトを出さず人間は不便な思いをしました。これは **`-t`が必要なのに付けなかった場合** の一例です。

それとは逆に， **`-t`を付けてはいけないのに付けてしまった場合** も問題になります。以下の例では，コンテナー内でlsコマンド，コンテナー外でwcコマンド（行数を数える）[^wc]を用いてディレクトリ・ファイルの数を数えようとしています。
[^wc]: 参考：西村めぐみ. “【 wc 】コマンド――テキストファイルの文字数や行数を数える”. atmarkIT. 2016-11-07. [https://atmarkit.itmedia.co.jp/ait/articles/1611/07/news026.html](https://atmarkit.itmedia.co.jp/ait/articles/1611/07/news026.html), (参照2024-06-02).
```shell-session
takechan@virtual-machine:~$ sudo docker exec container ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
wait.sh
takechan@virtual-machine:~$ sudo docker exec -t container ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var  wait.sh
takechan@virtual-machine:~$ sudo docker exec container ls | wc --lines
20
takechan@virtual-machine:~$ sudo docker exec -t container ls | wc --lines
2
takechan@virtual-machine:~$ 
```
[^TTYの幅]
`-t`を付けると正しい答えが得られていないことが分かります。コンテナーの外で出力を受け付けるのはwcコマンドなのに，lsコマンドに人間向けの出力をさせてしまったのが元凶ですね。
[^TTYの幅]: `sudo docker exec -t container ls`では出力が１行に収まっていたのに，`sudo docker exec -t container ls | wc --lines`で「2」と表示されるのは一見すると妙に思えます。しかし，`wc --lines`の代わりに`cat`を入れて実験してみるとlsコマンドの出力行数が本当に変わっていることが分かります。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/3e133d69-5f15-de99-07a2-7ccc3117d3bd.png)
推測にはなりますが，この現象にはTTYの幅が関係していると考えられます。lsコマンドは人間向けの動作においてTTYの幅を取得[^TIOCGWINSZ]し，それに基づいて人間が見やすいように改行を入れていると考えられます。また，dockerコマンドがコンテナー内に仮想的なTTY（lsコマンドから幅を参照される）を用意するときはdockerコマンドの標準出力先となっているTTYを参照して同じ幅を設定しようとすると考えられます。標準入力のTTYから高さ・幅を取得するコマンド`stty size`を用いてコンテナー内TTYの幅を取得する実験をした結果，この動作はdockerコマンドの標準出力先がTTYである場合のみ上手くいっているようです。dockerコマンドの標準出力先がcatコマンドの場合は，コンテナー内TTYの高さ・幅が０になっています。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/6b0f9025-b92e-c374-6865-cf8ee5ad6ff0.png)
dockerコマンドがTTYでない標準出力から幅を参照しようとして失敗した，ということでしょう。結果として，コンテナー内TTYの幅を参照したlsコマンドの出力行数が変わったと考えられます。TTYの幅が０になっているという異常事態でも２行で出力しているのは，適当な幅を推測しているからでしょう。
上の実験では`cat`を用いましたが，`wc --lines`を用いたときでも同様の現象が起こり「2」が出力される結果になったと考えられます。
この注釈の内容を図にまとめると次のようになります。![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/1f720229-0d9b-a1d5-16a3-f409bd11b735.png)![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339988/bb476705-f3fc-2027-fe67-ee7d02683f41.png)[^sudoが用意したTTY]

[^TIOCGWINSZ]: 参考：KusaReMKN. “ターミナルの幅と高さに関するメモ書き”. Zenn. 2020-10-10. https://zenn.dev/kusaremkn/articles/abdbd2f38c3d98b145eb, (参照2024-06-02).

:::note info
これは簡単な例なので，`wc --words`を用いて単語数を数える[^wc]という回避策があります。ただ，人間向けの出力を強引にプログラムで処理するのは良くないです。
:::

上の例で分かったように，「コンテナー内のコマンドに人間向けの動作をしてほしいとき`-t`を付ける」のが大原則です。ただ，その原則に従えない場合があります。コンテナー外からコンテナー内コマンドへの標準入力がTTYでない場合です。
実は`docker exec -it`も「標準入力・標準出力・標準エラー出力がTTYに繋がってるかによって挙動を変える」コマンドの１つで，標準入力がTTYでない場合にエラーを出します。「コンテナー外からコンテナー内のコマンドに入力しているのはプログラムなのに，コンテナー内のコマンドが人間向けの動作をしてしまう」という状況を回避するためでしょう。この動作から，「標準入力がTTYでないなら，どのようなコマンドも人間向けの動作をするはずが無い」というフールプルーフ[^フールプルーフ]的思想が見えます。
しかし，標準入力がTTYでなくても標準出力がTTYなら人間向けの動作をする，というコマンド（grepなど）もあるためこの思想での「フール」（「馬鹿」）が本当にフールとは言えません。そういったコマンドを`docker exec -it`で動かすとき，コンテナー外からの標準入力がTTYでないだけで人間向けの動作を禁止されて不便を感じるかもしれません。
[^フールプルーフ]: 株式会社FAプロダクツ. “フールプルーフとは？導入事例、メリットと意外な注意点か解説”. おしえてJSS. [https://jss1.jp/column/column_433/](https://jss1.jp/column/column_433/), (参照2024-06-08).

この記事の結論としては，こうした **`docker exec -it`の仕様も時折思い出しながら「コンテナー内のコマンドに人間向けの動作をしてほしいとき付ける」という原則に基づいて`-t`を付けたり外したりする** のが良いと思います。
# まとめ
この記事の内容をまとめると以下のようになります。
- TTYは，人間がCUIを使うためのインターフェース
- 一部のコマンドは，自身の入出力がTTYに繋がっているかに基づいて人間向け／プログラム向けの動作をする
- `docker exec`において`-t`は，コンテナー内のコマンドをTTYに繋ぐ＝コンテナー内のコマンドに人間向けの動作をさせる
- `-t`は付け得ではなく，付けない方が良いとき・付けられないときも存在する
# あとがき
僕はDocker初学者で，この記事を書く前もDockerについて学んでいました。その中で引っかかったところを調べているうちに内容が膨らみ，この記事ができました。

同じところで引っかかっている人のモヤモヤを晴らせれば幸いです。