在Go中执行包含函数声明的动态bash脚本的一行代码。

huangapple go评论73阅读模式
英文:

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

问题

我正在使用Go编写一个Bash任务运行器,它有一个简单的概念:

  1. 它读取一个名为Taskfile的Bash脚本,其中包含任务定义(简单的Bash函数声明)。
  2. 它动态添加额外的内容。
  3. 根据传递的参数执行命令。

这是一个简化的示例:

package main

import (
	"fmt"
	"os/exec"
)

func main() {
	// 简化为动态构建的脚本
	taskFileContent := "#!/bin/bash\n\ntask:foo (){\n  echo \"test\"\n}\n"
	// 简化为传递的参数
	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))
}

我的问题是,如果通过Go执行,这样做不起作用,我会得到以下错误:

task:foo: No such file or directory

但是,如果我直接在shell中执行生成的脚本,它确实可以工作,像这样:

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

task:foo (){
  echo "test"
}

task:foo
EOF
)"

test   <-- 从上面的`task:foo`打印出来

我在这里做错了什么?

英文:

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 (
	&quot;fmt&quot;
	&quot;os/exec&quot;
)

func main() {
	//simplified for a dynamically built script
	taskFileContent := &quot;#!/bin/bash\n\ntask:foo (){\n  echo \&quot;test\&quot;\n}\n&quot;
	// simplified for passed arguments
	task := &quot;\ntask:foo&quot;
	bash, _ := exec.LookPath(&quot;bash&quot;)
	cmd := exec.Command(bash, &quot;-c&quot;, &quot;\&quot;$(cat &lt;&lt; EOF\n&quot;+taskFileContent+task+&quot;\nEOF\n)\&quot;&quot;)
	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 &quot;$(cat &lt;&lt; EOF
#!/bin/bash

task:foo (){
  echo &quot;test&quot;
}

task:foo
EOF
)&quot;

test   &lt;-- printed out from the `task:foo` above

What am I doing wrong here ?

答案1

得分: 2

第一点:这里没有必要使用 Here Document(文档内嵌)。

你可以通过以下代码获得与此处相同的结果:

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

如果将其省略,你的代码会更简洁。


第二点:解释为什么要这样做

当你在 shell 中运行以下命令时:

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

task:foo (){
  echo "test"
}

task:foo
EOF
)"

...$() 周围的 " 是告诉解析你的命令的 bash 副本,命令替换的结果应作为一个字符串传递,不会进行字符串分割或通配符扩展。

类似地,$(cat <<EOFEOF 和最后的 )" 也是给你的交互式 shell 的指令,而不是它调用的非交互式 shell。它是交互式 shell 运行 cat(将一个包含文档内嵌内容的临时文件连接到其标准输入),读取该 cat 副本的标准输出,然后将该数据替换为一个参数,传递给 bash -c

在你的 Go 程序中,没有交互式 shell,所以你应该使用 Go 语法来完成所有这些步骤,而不是使用 shell 语法。而且,考虑到这些步骤在 Go 中本来就没有必要(没有必要将数据写入临时文件,没有必要让 /bin/cat 读取该文件的内容,没有必要运行一个命令替换来生成一个字符串(由这些内容组成),然后将其放在最终 shell 的命令行中),最好的做法是将所有这些步骤都省略掉。

英文:

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

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

cmd := exec.Command(bash, &quot;-c&quot;, taskFileContent+&quot;\n&quot;+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 &quot;$(cat &lt;&lt; EOF
#!/bin/bash

task:foo (){
  echo &quot;test&quot;
}

task:foo
EOF
)&quot;

...the &quot;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 &lt;&lt;EOF, the EOF, and the final )&quot; 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.

答案2

得分: 0

测试使用printf给了我原因

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

这种行为在这里解释了https://stackoverflow.com/questions/11816122/echo-fails-event-not-found

!字符用于csh风格的历史扩展。您需要关闭此行为...

所以您应该在您的.bash_profile中添加set +o histexpand

英文:

test with printf gave me the reason

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

this behaviour is explained here https://stackoverflow.com/questions/11816122/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

huangapple
  • 本文由 发表于 2022年12月23日 02:14:18
  • 转载请务必保留本文链接:https://go.coder-hub.com/74892412.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定