英文:
Executing dynamic bash script including function declarations in one line with Go
问题
我正在使用Go编写一个Bash任务运行器,它有一个简单的概念:
- 它读取一个名为
Taskfile
的Bash脚本,其中包含任务定义(简单的Bash函数声明)。 - 它动态添加额外的内容。
- 根据传递的参数执行命令。
这是一个简化的示例:
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:
- it reads a
Taskfile
, which is a a bash script containing task definitions (simple bash function declarations) - it adds dynamically additional stuff
- 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 ?
答案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 <<EOF
、EOF
和最后的 )"
也是给你的交互式 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, "-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.
答案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 "#!/bin/bash\n\ntask:foo (){\n echo \"test\"\n}\n"
-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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论