38

I need a command line utility to behave different if some string is piped into its STDIN. Here's some minimal example:

package main // file test.go

import (
    "fmt"
    "io/ioutil"
    "os"
)

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

    if len(bytes) > 0 {
        fmt.Println("Something on STDIN: " + string(bytes))
    } else {
        fmt.Println("Nothing on STDIN")
    }
}

This works fine if you call it like that:

echo foo | go run test.go

If test.go is called without anything on STDIN, the thing stucks at...

bytes, _ := ioutil.ReadAll(os.Stdin)

... waiting for EOF.

What do I need to do to get this going?

Thanks in advance!

sontags
  • 2,639
  • 3
  • 18
  • 18
  • did you try wrapping stdin with a bufio.reader or something like that? or maybe using peek to see if there's anything to read? – Not_a_Golfer Mar 30 '14 at 14:11
  • read the doc: ReadAll goes on until there's an error or EOF, so ask yourself: was there an error reading from stdin? EOF? (you can send EOF in a terminal, it control-D on unix, something else on windows) – loreb Mar 30 '14 at 17:03
  • @loreb I read the docs. You describe the same stuff I did, there is nothing new mentioned. – sontags Mar 30 '14 at 17:09
  • @Not_a_Golfer I'll try that, thanks so far – sontags Mar 30 '14 at 17:09
  • @sontags uh? Sorry, I must have misunderstood your question then. It doesn't see EOF because, well, obviously the keyboard is still there, so you must either send an EOF from the keyboard (control-D in unix) or read the output one piece at a time, line by line or whatever. – loreb Mar 30 '14 at 17:15
  • @loreb Sorry, probably I was not clear enough: I am aware of the fact that there needs to be a EOF or an error in order to pass that ReadAll. And yes, CTRL+D sends a EOF, but thats not an option for usability reasons. So most likely ReadAll is the wrong approach, this is just to illustrate what the result should be. I basically ask for some hints how the expected behavior can be achieved. – sontags Mar 30 '14 at 18:04
  • 3
    possible duplicate of [Determine if Stdin has data with Go](http://stackoverflow.com/questions/22563616/determine-if-stdin-has-data-with-go) – Kluyg Mar 31 '14 at 03:56

4 Answers4

55

I solved this by using os.ModeCharDevice:

stat, _ := os.Stdin.Stat()
if (stat.Mode() & os.ModeCharDevice) == 0 {
    fmt.Println("data is being piped to stdin")
} else {
    fmt.Println("stdin is from a terminal")
}
ostler.c
  • 2,953
  • 3
  • 20
  • 21
  • 1
    tested this, if you pipe stdin (ie. ./foo < /dev/null) from /dev/null you will think there is no stdin. That seems correct to me, since there is nothing to get from /dev/null anyways... – ostler.c Nov 01 '14 at 21:37
  • 5
    I like this solution better than @NickCraig-Wood because it uses standard go packages. – coffekid Dec 01 '15 at 14:46
19

Use the IsTerminal function from code.google.com/p/go.crypto/ssh/terminal (which was exp/terminal) or the Isatty function from github.com/andrew-d/go-termutil which is a much more focussed package.

If stdin is a terminal/tty then you aren't being piped stuff and you can do something different.

Here is an example

package main

import (
    "fmt"
    termutil "github.com/andrew-d/go-termutil"
    "io"
    "os"
)

func main() {
    if termutil.Isatty(os.Stdin.Fd()) {
        fmt.Println("Nothing on STDIN")
    } else {
        fmt.Println("Something on STDIN")
        io.Copy(os.Stdout, os.Stdin)
    }
}

Testing

$ ./isatty 
Nothing on STDIN
$ echo "hello" | ./isatty 
Something on STDIN
hello
$ (sleep 1 ; echo "hello") | ./isatty 
Something on STDIN
hello
miku
  • 172,072
  • 46
  • 300
  • 307
Nick Craig-Wood
  • 50,914
  • 12
  • 120
  • 124
3

If none of the above works for you, try this way:

stat, err := os.Stdin.Stat()
if err != nil {
    return nil, fmt.Errorf("you have an error in stdin:%s", err)
}
if (stat.Mode() & os.ModeNamedPipe) == 0 {
    return nil, errors.New("you should pass smth to stdin")
}

It worked for me in both darwin (Mac OS) and linux (Ubuntu).

bullgare
  • 1,523
  • 1
  • 20
  • 30
  • `$ go run sotest.go < sotest.go` results in `you should pass smth to stdin`. The answer with `ModeCharDevice` behaves as expected. – Ondrej Dec 12 '17 at 08:23
0

This does it:

package main // file test.go

import (
    "bufio"
"fmt"
    "os"
)

func main() {
    in := bufio.NewReader(os.Stdin)
    stats, err := os.Stdin.Stat()
    if err != nil {
        fmt.Println("file.Stat()", err)
    }

    if stats.Size() > 0 {
        in, _, err := in.ReadLine()
        if err != nil {
            fmt.Println("reader.ReadLine()", err)
        }
        fmt.Println("Something on STDIN: " + string(in))
    } else {
        fmt.Println("Nothing on STDIN")
    }
}

Thanks @Kluyg !

sontags
  • 2,639
  • 3
  • 18
  • 18
  • 4
    This doesn't work `( sleep 1 ; echo "hello" ) | ./test`, printing `Nothing on STDIN`. I don't think you'll get this approach to work reliably. – Nick Craig-Wood Apr 01 '14 at 18:46