1

I'd like to interact with another console application from my own go application. I don't want to write another FTP client, but I'll take the ftp application in this example as it should be available everywhere.

Take the following example:

func main() {
  cmd := exec.Command("/bin/bash", "-c", "ftp")
  cmd.Stdin = os.Stdin
  cmd.Stdout = os.Stdout
  cmd.Stderr = os.Stderr

  cmd.Start()
  time.Sleep(2 * time.Second)
  io.WriteString(cmd.Stdout, "help\r\n")
  time.Sleep(2 * time.Second)
  io.WriteString(cmd.Stdout, "exit\r\n")
  cmd.Wait()
}

After the "cmd.Start()" I can see "ftp>" in the console. Then I tried to write "help" into the ftp application. I can see "help" as text, but I do not get the expected output back. When I write "help" with the keyboard, I receive the expected text.

The expected workflow for this example would be the following:

ftp> help
Commands may be abbreviated.  Commands are:

!       dir     mdelete     qc      site
$       disconnect  mdir        sendport    size
account     exit        mget        put     status
append      form        mkdir       pwd     struct
ascii       get     mls     quit        system
bell        glob        mode        quote       sunique
binary      hash        modtime     recv        tenex
bye     help        mput        reget       tick
case        idle        newer       rstatus     trace
cd      image       nmap        rhelp       type
cdup        ipany       nlist       rename      user
chmod       ipv4        ntrans      reset       umask
close       ipv6        open        restart     verbose
cr      lcd     prompt      rmdir       ?
delete      ls      passive     runique
debug       macdef      proxy       send
ftp> exit

Has anyone an idea how I can realize this? Is it possible?

Thanks in advance for your feedback.

bmm
  • 21
  • 2
  • 2
    Your code is not interacting at all with the application. You start the application and then all you do is to write to stdout - which is not connected to the application. `cmd.Stdout = os.Stdout` just makes the application write to the system stdout but in no way will make other writes to stdout magically be send to the application. To feed the application with data you need instead to write to the commands stdin, like [in this example](https://pkg.go.dev/os/exec#example-Cmd.StdinPipe) – Steffen Ullrich Jan 16 '22 at 07:21
  • FTP is a terrible protocol. Avoid it, preferring SFTP or other modern transfer protocol. Assuming you must use FTP, I really do think you'll save yourself oodles of time and energy using something like https://pkg.go.dev/github.com/jlaffaye/ftp instead of shelling out to `ftp`. "it should be available everywhere" maybe on your systems, but `ftp` is not always installed by default of commonly in my experience. – erik258 Jan 16 '22 at 19:50
  • Hi Daniel, as written in the initial question, I don't want to write another ftp client or so, but I like to interact with another application. It is like start the other application, read the output, send a command, read the output again, send another command and so on and in some point of time send an exit command to finish the other application. I continued to find the solution and thinking about channels. Guess I'll need them as well. – bmm Jan 17 '22 at 04:45

2 Answers2

1

Thanks for your answers. I tried to improve it with your recommendations with piping.

Now it looks like this:

func main() {
    cmd := exec.Command("/bin/bash", "-c", "ftp")

    stdin, err := cmd.StdinPipe()
    if err != nil {
        log.Fatal(err)
    }
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }
    stderr, err := cmd.StderrPipe()
    if err != nil {
        log.Fatal(err)
    }

    go func() {
        defer stdin.Close()

        stdinCommand := "status\n"
        fmt.Print(time.Now().UTC().Format("15:04:05") + " STDIN: " + stdinCommand)
        io.WriteString(stdin, stdinCommand)

        time.Sleep(1 * time.Second)

        stdinCommand = "help\n"
        fmt.Print(time.Now().UTC().Format("15:04:05") + " STDIN: " + stdinCommand)
        io.WriteString(stdin, stdinCommand)
        time.Sleep(2 * time.Second)
    }()

    scanner := bufio.NewScanner(stdout)
    go func() {
        for scanner.Scan() {
            fmt.Println(time.Now().UTC().Format("15:04:05") + " STDOUT: " + scanner.Text())
        }
    }()

    scannerError := bufio.NewScanner(stderr)
    go func() {
        for scannerError.Scan() {
            fmt.Println(time.Now().UTC().Format("15:04:05") + " STDERR: " + scanner.Text())
        }
    }()

    if err := cmd.Run(); err != nil {
        log.Fatal(err)
    }
    fmt.Print(time.Now().UTC().Format("15:04:05") + " done")
}

The output now looks like this:

15:24:26 STDIN: status
15:24:27 STDIN: help
15:24:29 STDOUT: Not connected.
15:24:29 STDOUT: No proxy connection.
15:24:29 STDOUT: Connecting using address family: any.
15:24:29 STDOUT: Mode: ; Type: ; Form: ; Structure: 
15:24:29 STDOUT: Verbose: off; Bell: off; Prompting: on; Globbing: on
15:24:29 STDOUT: Store unique: off; Receive unique: off
15:24:29 STDOUT: Case: off; CR stripping: on
15:24:29 STDOUT: Quote control characters: off
15:24:29 STDOUT: Ntrans: off
15:24:29 STDOUT: Nmap: off
15:24:29 STDOUT: Hash mark printing: off; Use of PORT cmds: on
15:24:29 STDOUT: Tick counter printing: off
15:24:29 STDOUT: Commands may be abbreviated.  Commands are:
15:24:29 STDOUT: 
15:24:29 STDOUT: !              dir             mdelete         qc              site
15:24:29 STDOUT: $              disconnect      mdir            sendport        size
15:24:29 STDOUT: account                exit            mget            put             status
15:24:29 STDOUT: append         form            mkdir           pwd             struct
15:24:29 STDOUT: ascii          get             mls             quit            system
15:24:29 STDOUT: bell           glob            mode            quote           sunique
15:24:29 STDOUT: binary         hash            modtime         recv            tenex
15:24:29 STDOUT: bye            help            mput            reget           tick
15:24:29 STDOUT: case           idle            newer           rstatus         trace
15:24:29 done

Do you have a hint, what I need to change to get the response for "status" directly behind the input and afterwards send "help" to get the help response. In the end I'd like to send "exit" to exit the program. With the current version it automatically exits as long as I have "defer stdin.Close()" included. When I remove it, I do not get the output.

bmm
  • 21
  • 2
0

You want to have piping in your application. That doesn't happen in the way you are trying. The way you write commands in your terminal is different from the way Bash and then OS treats them.

This example might help you, copied from another SO question (view question):

package main

import (
    "os"
    "os/exec"
)

func main() {
    c1 := exec.Command("ls")
    c2 := exec.Command("wc", "-l")
    c2.Stdin, _ = c1.StdoutPipe()
    c2.Stdout = os.Stdout
    _ = c2.Start()
    _ = c1.Run()
    _ = c2.Wait()
}
Mostafa Talebi
  • 8,825
  • 16
  • 61
  • 105