Home > Net >  Executing dynamic bash script including function declarations in one line with Go
Executing dynamic bash script including function declarations in one line with Go

Time:12-23

I am writing a bash task runner in Go which has a simple concept:

  1. it reads a Taskfile , which is a a bash script containing task definitions (simple bash function declarations)
  2. it adds dynamically additional stuff
  3. Executes a command based on passed arguments

Here is a simplified example:

package main

import (
    "fmt"
    "os/exec"
)

func main() {
    //simplified for a dynamically built script
    taskFileContent := "#!/bin/bash\n\ntask:foo (){\n  echo \"test\"\n}\n"
    // simplified for passed arguments
    task := "\ntask:foo"
    bash, _ := exec.LookPath("bash")
    cmd := exec.Command(bash, "-c", "\"$(cat << EOF\n" taskFileContent task "\nEOF\n)\"")
    fmt.Println(cmd.String())
    out, _ := cmd.CombinedOutput()
    fmt.Println(string(out))
}

My problem now is, that this does not work if it gets executed via Go and I get this error

task:foo: No such file or directory

But it does work if I just execute the generated script directly in the shell like this:

$ /opt/opt/homebrew/bin/bash -c "$(cat << EOF
#!/bin/bash

task:foo (){
  echo "test"
}

task:foo
EOF
)"

test   <-- printed out from the `task:foo` above

What am I doing wrong here ?

CodePudding user response:

First: There's no point to a heredoc here.

You're getting nothing that you wouldn't have from:

cmd := exec.Command(bash, "-c", taskFileContent "\n" task)

Your code is simpler if you leave it out.


Second: An explanation of why

When, at a shell, you run:

/opt/opt/homebrew/bin/bash -c "$(cat << EOF
#!/bin/bash

task:foo (){
  echo "test"
}

task:foo
EOF
)"

...the "s surrounding the $() are syntax not to the copy of bash that's being started, but to the copy of bash that's parsing your command. They tell that copy of bash that the results of the command substitution are to be passed as exactly one string, not subject to string-splitting or globbing.

Similarly, the $(cat <<EOF, the EOF, and the final )" are likewise instructions to your interactive shell, not the noninteractive shell it invokes. It's the interactive shell that runs cat (with a temporary file containing the heredoc's content connected to its stdin), reading the stdout of that copy of cat, and then substituting that data into a single argument that it passes to bash -c.

In your Go program, you have no interactive shell, so you should be using Go syntax -- not shell syntax -- for all these steps. And insofar as those steps are things there's no reason to do in Go to the first place (no point to writing your data file to a temporary file, no point to having /bin/cat read that file's contents, no point to having a subprocess running a command substitution to generate a string -- consisting of those contents -- to put on the command line of your final shell), it's much more sensible to just leave all those steps out.

CodePudding user response:

test with printf gave me the reason

pintf "#!/bin/bash\n\ntask:foo (){\n  echo \"test\"\n}\n"
-bash: !/bin/bash\n\ntask: event not found

this behaviour is explained here echo "#!" fails -- "event not found"

The ! character is used for csh-style history expansion. You need to turn off this behaviour...

so you should add set o histexpand to your .bash_profile

  • Related