1

I have the following go code:

package main

import "fmt"

func main() {
    if scanln_test() {
        fmt.Println("Success!")
    }
}

func scanln_test() bool {
    fmt.Print("Please type yes or no and then press enter [y/n]: ")

    var response string
    fmt.Scanln(&response)

    if response == "y" {
        return true
    } else if response == "n" {
        return false
    } else {
        return scanln_test()
    }
}

When execute the compiled binary via pipe, e.g.:

$ echo "just-showing-how-pipe-affects-the-behavior" | ./scanln

I get a infinite output of fmt.Print inside of scanln_test function. However when I don't execute it via pipe, everything works fine.

Is there a way to fix this?

UPD. fmt.Scanln returns "EOF" as an error when the binary executed with in pipe

UPD2. The example with echo above is just to make a point about the pipe, but I'm not looking to read the content of this echo in my go program. The real case looks like this: wget -qO- http://example.com/get | sh -s "param". I have exec of go program inside of this downloaded shell script and I want my program to show dialogue to user with Y/N. I'm not sure whether it's possible at all, so for now I've decided to get rid of the pipe and run it separately like wget -qO- http://example.com/get | sh && go-program "param".

chingis
  • 1,514
  • 2
  • 19
  • 38
  • Check your calls for errors. – hobbs May 31 '16 at 07:10
  • fmt.Scanln returns EOF, is this the output of the command before pipe? – chingis May 31 '16 at 07:43
  • yes, it returns EOF because there's no more input. And you keep trying to read over and over even though there will never be any more input. – hobbs May 31 '16 at 07:55
  • What would be the right way to read the input when the binary executed in pipe? – chingis May 31 '16 at 08:37
  • First call to `fmt.Scanln(&response)` reads "just-showing-how-pipe-affects-the-behavior", since it is not equal to neither "y" nor "n" it calls `scanln_test()` again in your else statement. But now Stdin is empty and closed so `fmt.Scanln(&response)` returns EOF error and response is null value string, so it didnt match again and calls `scanln_test()` again in else statement. And it loops forever in recursion. What did you expect? Does `$ echo "y" | ./scanln` runs as expected? – Darigaaz May 31 '16 at 13:52
  • Whether it's from a pipe or not *makes no difference*. The right thing to do is check your errors and handle EOF by exiting or doing something else. – hobbs May 31 '16 at 14:23
  • `$ echo "y" | ./scanln` runs well. Ok, now I understand that this caused by EOF in stdin, but what would be the right way to read user input when I use pipe to run? – chingis Jun 01 '16 at 05:15

2 Answers2

1

So you need concurrent input from Stdin (pipe) and user Stdin (Keyboard):
i think you answer is cat command see: How can I pipe initial input into process which will then be interactive?
and: https://en.wikipedia.org/wiki/Cat_(Unix)

see: How to pipe several commands in Go?
and Go Inter-Process Communication

there are 3 things to note:
first: it is good practice to check for all errors:
in your case :

n, err := fmt.Scanln(&response)  

second:
you are using recursive call (Tail Call) and it is not necessary here.
replace it with simple for loop and see: Tail Call Optimization in Go
3rd:
last but not least: in case of bad input your code loops for ever(consuming stack if compiler could not optimizes tail call)!
it is good to limit to 3.
example:

package main

import "fmt"
import "strings"

type Input int

const (
    Timeout Input = iota
    Yes
    No
    Abort
    BadInput
)

func userInput(msg string) Input {
    var input string
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        n, err := fmt.Scanln(&input)
        if n > 0 {
            switch strings.ToLower(input) {
            case "y":
                return Yes
            case "n":
                return No
            case "a":
                return Abort
            }
        }
        if err != nil {
            return BadInput // or panic(err)
        }
    }
    return Timeout
}
func main() {
    ans := userInput("Please type Yes,No or Abort and then press enter [y/n/a]: ")
    fmt.Println(ans)
    switch ans {
    case Yes:
        fmt.Println("Yes") // do some job
        //...
    }
}

Edit: with this simple "y/n" you do not need to check is it pipe or not.
even simple std Read with one byte slice is good:

os.Stdin.Read(b1)

see my piping sample: https://stackoverflow.com/a/37334984/6169399

but in case your Stdin is pipe you may use:

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

to read all piped data all at once. but be careful to handle errors. you can check if stdin is associated with a terminal or a pipe then use appropriate code.
simple way to detect it is pipe or not :

package main

import (
    "fmt"
    "os"
)

func main() {
    info, err := os.Stdin.Stat()
    if err != nil {
        fmt.Println("not a pipe")
    } else {
        fmt.Println("pipe name=", info.Name(), "pipe size=", info.Size())
    }
}

all in one sample code:

package main

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

type Input int

const (
    Timeout Input = iota
    Yes
    No
    Abort
    BadInput
)

func userInput(msg string) Input {
    var input string
    for i := 0; i < 3; i++ {
        fmt.Println(msg)
        n, err := fmt.Scanln(&input)
        if n > 0 {
            switch strings.ToLower(input) {
            case "y":
                return Yes
            case "n":
                return No
            case "a":
                return Abort
            }
        }
        if err != nil {
            return BadInput // or panic(err)
        }
    }
    return Timeout
}
func main() {
    info, err := os.Stdin.Stat()
    if err != nil {
        //fmt.Println("not a pipe")
        ans := userInput("Please type Yes,No or Abort and then press enter [y/n/a]: ")
        fmt.Println(ans)
        switch ans {
        case Yes:
            fmt.Println("Yes") // do some job
            //...
        }

    } else {
        fmt.Println("pipe name=", info.Name(), "pipe size=", info.Size())
        bytes, err := ioutil.ReadAll(os.Stdin)
        fmt.Println(string(bytes), err) //do some jobe with bytes
    }

}
Community
  • 1
  • 1
  • Thanks! All 3 recommendations are useful, however, the original question remains unanswered – how to read user input when I use pipe to run the binary? – chingis Jun 01 '16 at 05:17
  • This was discussed here below http://stackoverflow.com/a/37539014/1826109. It helps to get the data I have before the pipe, which is not the case I'm looking for. My example `echo "just-showing-how-pipe-affects-the-behavior" | ./scanln` is just to make a point, but what I really have is something like that `wget -qO- http://example.com/get | sh -s "param"`. I have a binary exec inside of this shell script and I want it to show user dialogue with y/n – chingis Jun 01 '16 at 05:45
0

You can use os.ModeCharDevice:

stat, _ := os.Stdin.Stat()

if (stat.Mode() & os.ModeCharDevice) == 0 {
    // piped
    input, _ := ioutil.ReadAll(os.Stdin)
} else {
    // not piped, do whatever, like fmt.Scanln()
}
Duru Can Celasun
  • 1,621
  • 1
  • 16
  • 28
  • The condition always returns false, but anyway I tried to replace fmt.Scanln with `ioutil.ReadAll(os.Stdin)` and it returns `[119 104 97 116 101 118 101 114 10]` which is probably the same "EOF" returned by Scanln error – chingis May 31 '16 at 07:50
  • You must be doing something wrong. Save [this](https://play.golang.org/p/YvJwt6bw0N) file as `input.go` and try with `go run input.go` and `echo foo | go run input.go`. You'll see `not piped` and `piped` respectively. – Duru Can Celasun May 31 '16 at 08:43
  • I execute the compiled binary (Linux x64) – chingis May 31 '16 at 08:45
  • Doesn't matter, it works the same. See [here](http://dpaste.com/18QYWES.txt). Did you try the **exact** file I've linked? Also, what is your go version? – Duru Can Celasun May 31 '16 at 08:50
  • Sorry, I must have mixed up something. The condition works now. I tried to use `ioutil.ReadAll(os.Stdin)` as you suggested, but it just returns the content of what I have before the pipe. My golang version is 1.6 – chingis May 31 '16 at 09:02