Читать с начального ввода в Go?

Я хотел бы зачитать от первоначального stdin программы Go. Например, если бы я сделал echo test stdin | go run test.go echo test stdin | go run test.go, я бы хотел иметь доступ к "test stdin". Я пробовал читать с os.Stdin, но если в нем ничего нет, он будет ждать ввода. Я также попытался сначала проверить размер, но os.Stdin.Stat().Size() равен 0, даже когда ввод передается.

Что я могу сделать?

Ответ 1

Я думаю, что ваш вопрос сам по себе не имеет разумного ответа, потому что просто нет такого понятия, как "начальный stdin". Unix-подобные ОС и Windows реализуют концепцию "стандартные потоки" , которая работает так (упрощена): когда процесс создается, он Automagically имеет три файловых дескриптора (дескрипторы в Windows) open — stdin, stdout и stderr. Не сомневайтесь, вы знакомы с этой концепцией, но я хотел бы подчеркнуть значение слова "поток" там:— в вашем примере, когда вы вызываете

$ echo 'test stdin' | ./stdin

оболочка создает pipe, генерирует два процесса (один для echo и один для вашего двоичного кода) и использует созданная труба: труба write FD присоединена к выходу echo, а труба, считываемая FD, прикреплена к вашему двоичному stdin. Тогда любой процесс echo, который нравится писать в его stdout, передается по каналам (sic!) В stdin вашего процесса. (На самом деле большинство современных оболочек реализуют echo как встроенный примитив, но это никоим образом не изменяет семантику: вместо этого вы могли бы попробовать /bin/echo, что является реальной программой. Также обратите внимание, что я просто использовал ./stdin, чтобы обратиться к вашей программе - это для ясности, поскольку go run stdin.go сделает именно это, в конце.)

Обратите внимание на несколько важных вещей здесь:

  • Процесс записи (echo в вашем случае) не закрывается, чтобы записать что-либо в его stdout (например, echo -n ничего не записывал бы в его stdout и не удалялся успешно).
  • Он также может делать произвольные задержки, записывая свои данные (либо потому, что он хочет сделать такие задержки, либо потому, что он был выгружен ОС, либо спит в каком-то syscall, ожидающем некоторых загруженных системных ресурсов и т.д.).
  • Буферы ОС переносятся по трубам. Это означает, что процесс записи отправляется в канал, может появляться в произвольных фрагментах на стороне чтения. 1
  • Есть только два способа узнать, что на стороне записи больше нет данных для отправки по каналу:
    • Как-то кодировать это в самих данных (это означает использование согласованного протокола передачи данных между автором и читателем).
    • Писатель может закрыть свою сторону трубы, что приведет к условию "конец файла" на стороне читателя (но только после того, как буфер будет разряжен, а один другой вызов read будет выполнен, что не удастся).

Позвольте обернуть это: поведение, которое вы наблюдаете, является правильным и нормальным. Если вы ожидаете получить какие-либо данные из stdin, вы не должны ожидать, что он будет легко доступен. Если вы также не хотите блокировать stdin, тогда создайте goroutine, который будет блокировать чтение из stdin в бесконечном цикле (но проверять условие EOF) и передавать собранные данные по каналу (возможно, после определенной обработки, если необходимо).

1 Вот почему некоторые инструменты, которые обычно встречаются между двумя трубами в конвейере, например grep, могут иметь специальные опции, чтобы заставить их сбросить их stdout после записи каждой строки — прочтите пример --line-buffered на странице grep руководства для примера. Люди, которые не знают эту семантику "полной буферизации по умолчанию", недоумевают, почему tail -f /path/to/some/file.log | grep whatever | sed ..., похоже, заглох и не отображает ничего, когда очевидно, что проверенный файл обновляется.


В качестве побочного примечания: если вы должны были запустить свой двоичный "как есть", как в

$ ./stdin

это не означало бы, что порожденный процесс не будет иметь stdin (или "начальный stdin" или whaveter), вместо этого его stdin будет подключен к тому же потоку, который ваша оболочка получает от вашего импорта с клавиатуры (так что вы можете прямо набрать что-то ваш процесс stdin).

Единственный верный способ связать процесс stdin с нигде - использовать

$ ./stdin </dev/null

в Unix-подобных ОС и

C:\> stdin <NUL

в Windows. Этот "null device" заставляет процесс видеть EOF на первом read из его stdin.

Ответ 2

Чтение из stdin с помощью os.Stdin должно работать как ожидалось:

package main

import "os"
import "log"
import "io/ioutil"

func main() {
    bytes, err := ioutil.ReadAll(os.Stdin)

    log.Println(err, string(bytes))
}

Выполнение echo test stdin | go run stdin.go должно печатать 'test stdin' просто отлично.

Это поможет, если вы присоедините код, который вы использовали для идентификации проблемы, с которой вы столкнулись.

Для чтения на основе строки вы можете использовать bufio.Scanner:

import "os"
import "log"
import "bufio"

func main() {
    s := bufio.NewScanner(os.Stdin)
    for s.Scan() {
        log.Println("line", s.Text())
    }
}

Ответ 3

Вы не можете проверить stdin для содержимого, но вы можете проверить, связан ли stdin с терминалом или каналом. IsTerminal просто принимает стандартные UNIX-номера (0,1,2). Пакет syscall имеет назначенные переменные, поэтому вы можете сделать syscall.Stdin, если вы предпочитаете их именовать.

package main

import (
    "code.google.com/p/go.crypto/ssh/terminal"
    "fmt"
    "io/ioutil"
    "os"
)

func main() {
    if ! terminal.IsTerminal(0) {
        b, _ := ioutil.ReadAll(os.Stdin)
        fmt.Print(string(b))
    } else {
        fmt.Println("no piped data")
    }
}