1

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 ?

bambamboole
  • 577
  • 10
  • 30
  • I'm not expert on bash, but in my experience it's tricky to expect full shell functionality from cli invokations like `exec.Command`. Often, the feature set is limited - check the [this](https://pkg.go.dev/os/exec#pkg-overview) part of the according Go documentation. My guess is that the `cat <<` won't work. – NotX Dec 22 '22 at 20:58
  • What value do you get from doing this over putting the text of your function in an environment variable and `eval`ing that variable? It looks like a lot of extra overhead (heredocs are implemented by creating temporary files, so even some I/O overhead!) for no obvious benefit. – Charles Duffy Dec 22 '22 at 21:11
  • Also, note that `task:foo` is not, as a function name, accepted by all versions of bash out there. Some old releases only allow non-POSIX-compliant names when the (legacy ksh / non-POSIX) `function` keyword is use; newer releases are more lenient, but sticking to standard-compliant names is the better practice. – Charles Duffy Dec 22 '22 at 21:13
  • @CharlesDuffy, I will try that and come back – bambamboole Dec 22 '22 at 21:14
  • @NotX, one needs to explicitly invoke a shell to get full shell functionality, but that's exactly what `bash` `-c` _does_. (The `system()` equivalents in other languages discussed by the docs you link use `sh` `-c`, but `bash` `-c` as an alternative offers extensions to the language that `/bin/sh` isn't guaranteed to have). – Charles Duffy Dec 22 '22 at 21:16
  • That said -- stepping back and taking a closer look, I'm in a place to write up an answer describing what went wrong here. – Charles Duffy Dec 22 '22 at 21:20
  • (there's not much value to `eval` either, except maybe to move content off the command line -- which is more often world-readable by all users on the system -- to the environment, which on operating systems following recent best practices is readable only by the same account or by root) – Charles Duffy Dec 22 '22 at 21:41

2 Answers2

2

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.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • I just prepared an answer with a simple example using the from you mentioned env var approach, which also worked, but this here is the real solution. simpler than before and just working. the a lot for the fast help! – bambamboole Dec 22 '22 at 21:43
  • 1
    BTW, if you do end up using the eval form, make sure you're running `eval "$var"`, not `eval $var`. The bugs caused by leaving out the quotes are subtle, but that doesn't mean they can't sting. – Charles Duffy Dec 22 '22 at 21:45
0

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

Oliver Gaida
  • 1,722
  • 7
  • 14
  • It's already turned off by default in noninteractive shells, though; if an interactive shell is being used to run a script, something is very wrong. – Charles Duffy Dec 22 '22 at 21:09
  • Which is to say: This error message is created when you test the code in an interactive shell, but the OP's Go program creates a noninteractive shell, so it can't hit this bug. – Charles Duffy Dec 22 '22 at 21:11
  • @Charles i did not know that, thanks for clarrclarification – Oliver Gaida Dec 22 '22 at 21:22