-1

So I would like to create a command shell in Go. My goal is to use its capabilities in portable system tools I'm thinking about writing. I think a great starting point would be something that can demonstrate the basics of running system commands and allowing the user to interact with the system shell.

lee8oi
  • 1,169
  • 8
  • 12

1 Answers1

0

I have such a solution! I actually have it posted in a Gist at gist.github.com/lee8oi/a8a90f559fe48355f800

Its a nice little demonstration that uses Go's os/exec package to directly pipe input/output/error to the command instance. When "bash" runs you'll be sitting at a very simple prompt. You can interact with it much like you would with bash (but still pretty simplified). When you run a regular command the output will be displayed then the command will exit (like with "ping"). I think this would make a great starting point for tinkering with ways to interact with the system shell in Go.

package main

import (
    "bufio"
    "fmt"
    "io"
    "log"
    "os"
    "os/exec"
    "sync"
)

func getPipes(c *exec.Cmd) (inp io.Writer, outp, errp io.Reader) {
    var err error
    if inp, err = c.StdinPipe(); err != nil {
        log.Fatal(err)
    }
    if outp, err = c.StdoutPipe(); err != nil {
        log.Fatal(err)
    }
    if errp, err = c.StderrPipe(); err != nil {
        log.Fatal(err)
    }
    return
}

func pipe(wg *sync.WaitGroup, inp io.Reader, outp io.Writer) {
    if wg != nil {
        defer wg.Done()
    }
    r := bufio.NewReader(inp)
    for {
        c, err := r.ReadByte()
        if err != nil {
            return
        }
        fmt.Fprintf(outp, "%s", string(c))
    }
}

func Command(args ...string) {
    var cmd *exec.Cmd
    var wg sync.WaitGroup
    if len(args) > 1 {
        cmd = exec.Command(args[0], args[1:]...)
    } else {
        cmd = exec.Command(args[0])
    }
    inp, outp, errp := getPipes(cmd)
    wg.Add(1)
    go pipe(&wg, errp, os.Stderr)
    wg.Add(1)
    go pipe(&wg, outp, os.Stdout)
    go pipe(nil, os.Stdin, inp)
    if err := cmd.Start(); err != nil {
        log.Fatal("start ", err)
    }
    wg.Wait()
}

func main() {
    Command("bash")
    Command("date")
    Command("echo", "some text")
    Command("ping", "-c 3", "www.google.com")
}
lee8oi
  • 1,169
  • 8
  • 12
  • I wonder if there is a bug in Stack Overflow question/answers dates or if you actually answered yourself in less than one second. Both are at 23:32:47Z. – siritinga Jan 17 '15 at 11:23
  • A code review for you: This gives a nil pointer exception if you ^D out of the shell. The use of `interface {}` is strange, just use an `io.Reader` and `io.Writer`. Errors from `StdinPipe*` should be checked as should errors from `Wait`. runPipe` is overcomplicated - you can replace it all with `io.Copy`. – Nick Craig-Wood Jan 17 '15 at 12:58
  • Thank you Nick. Good stuff. To be honest its just meant to be some hacking code for playing with shells in Go. I did have checks for the Std{out,err,in}Pipe() functions, they just simply never triggered. But I can put them back for completeness. I'll go over the code again with your suggestions in mind. Thanks again. You said something more useful than the entire G+ Go community so far. – lee8oi Jan 17 '15 at 15:25
  • Ok I updated the code. Ctrl+D won't cause a crash anymore, the extra error checking has been added, and swapped out bufio with the io.Copy. If you have any other suggestions please do continue to feel free to hit me with em :) – lee8oi Jan 17 '15 at 18:03
  • Ok, the io.Copy method was randomly giving me some bad file descriptor errors. Turns out the way I was doing it is more reliable. – lee8oi Jan 17 '15 at 23:32