いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-683 (/dev/ttyで遊ぶ)(プロセスが所属している制御端末を指す特殊エイリアス)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ · GitHub

概要

以下、自分用のメモです。

知ってるとちょっと便利なファイルで /dev/tty があります。

これは、「プロセスが所属している制御端末」を指す特殊なエイリアスです。知ってる方も多いと思います。

これを使うと、例えば標準出力や標準エラー出力をどこかのファイルにリダイレクトさせている場合でも、ターミナルに出力出来たりします。

実際は、どれかの擬似端末、つまり、ターミナルエミュレータへのエイリアスになってます。/dev/pts/0 とか。

結構便利ですので、知っておくと便利です。

サンプル

main.go

package main

import (
    "bufio"
    "fmt"
    "log"
    "os"

    "golang.org/x/term"
)

func main() {
    log.SetFlags(0)

    //
    // 以下、サンプルであるためエラー処理は割愛
    //

    // 通常の標準出力・標準エラー出力が生きているかの確認用
    fmt.Fprintln(os.Stdout, "stdout: hello")
    fmt.Fprintln(os.Stderr, "stderr: hello")

    // /dev/tty を読み書きモードで開く
    ///
    // /dev/tty は「このプロセスの制御端末」へのエイリアスであり、
    // stdin/stdout/stderr のリダイレクト状態に関わらず
    // 常にユーザーのターミナルへ直接アクセスできる
    var (
        tty *os.File
    )
    tty, _ = os.OpenFile("/dev/tty", os.O_RDWR, 0)
    defer tty.Close()

    // /dev/tty 経由でターミナルに直接書き込む
    //
    // stdout が /dev/null に向いていても、これはターミナルに表示される
    fmt.Fprintln(tty, "tty: hello")

    // /dev/tty からの入力を行単位で読み込み、読み込んだ入力を各出力先(tty, stdout, stderr)に書き込む
    //
    // tty    : リダイレクトに関わらずターミナルに表示される
    // stdout : stdout が /dev/null の場合は捨てられる
    // stderr : stderr が /dev/null の場合は捨てられる
    var (
        reader *bufio.Reader
        line   string
    )
    fmt.Fprint(tty, "input> ")

    reader = bufio.NewReader(tty)
    line, _ = reader.ReadString('\n') // 末尾の改行ごと取得 (Scannerの場合は改行なしで取得)

    fmt.Fprintf(tty, "tty-echo   : %s", line)
    fmt.Fprintf(os.Stdout, "stdout-echo: %s", line)
    fmt.Fprintf(os.Stderr, "stderr-echo: %s", line)

    // エコーなしでパスワードを読み込む
    //
    // term.ReadPassword はシステムコールレベルで fd を直接操作するため
    // *os.File ではなく int の fd が必要。なお、Fd() を呼ぶとファイルがブロッキングモードに切り替わる。
    //
    // 内部で termios の ECHO フラグを一時的に無効化し、
    // 読み込み完了後に元の termios 設定が復元される
    var (
        fd = int(tty.Fd())
        pw []byte
    )
    fmt.Fprint(tty, "password> ")

    pw, _ = term.ReadPassword(fd)

    // term.ReadPassword はエコーを抑制するので改行も表示されない。
    // なので明示的に改行を出力して表示を調整。
    fmt.Fprintln(tty)

    fmt.Fprintf(tty, "tty-echo   : %s\n", pw)
    fmt.Fprintf(os.Stdout, "stdout-echo: %s\n", pw)
    fmt.Fprintf(os.Stderr, "stderr-echo: %s\n", pw)
}

Taskfile.yml

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: '3'

tasks:
  default:
    cmds:
      - go run .
      - go run . </dev/null >/dev/null 2>&1
    silent: false

最初の実行は stdin/stdout/stderr をそのままにして実行、次の実行は stdin/stdout/stderr を全部潰して実行します。

実行

入力する値は world という文字列を打ち込んでいるとします。

$ task
task: [default] go run .
stdout: hello
stderr: hello
tty: hello
input> world
tty-echo   : world
stdout-echo: world
stderr-echo: world
password> 
tty-echo   : world
stdout-echo: world
stderr-echo: world

task: [default] go run . </dev/null >/dev/null 2>&1
tty: hello
input> world
tty-echo   : world
password> 
tty-echo   : world

2回目の実行は全部潰しているのに、入出力出来ていますね。

参考情報

blog.ingage.jp

www.linusakesson.net

tweeeety.hateblo.jp

個人的Goのおすすめ書籍

個人的に読んでとても勉強になった書籍さんたちです。


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。