为什么我的程序返回一个错误,说文件不存在,但实际上文件是存在的?

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

Why does my program return an error stating the file doesn't exist when it does?

问题

我正在编写一个自定义调度工具的基础部分,它将读取一个配置文件中的“作业”,并将它们添加到定期运行的调度中。目前它非常基础,只是一个概念验证,在重构和添加一些更高级功能之前。

当我尝试运行这个程序时,它报告找不到脚本。脚本“test1.sh”确实存在于我尝试运行它的路径中,并且具有执行权限。

我得到了以下错误,但无法解释或解决它,因为脚本确实存在于我运行它的路径中:

-> ./scheduler-example
2021/08/16 12:48:54 fork/exec /Users/me/scheduler-example/scripts/test1.sh: no such file or directory

调度器代码:

package main

import (
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/go-co-op/gocron"
	"gopkg.in/yaml.v3"
)

type Config struct {
	GlobalLog string `yaml:"GlobalLog"`
	Jobs      []Job  `yaml:"Jobs"`
}

type Job struct {
	Name      string `yaml:"Name"`
	Command   string `yaml:"Command"`
	Frequency string `yaml:"Frequency"`
	Tag       string `yaml:"Tag,omitempty"`
	Log       string `yaml:"Log,omitempty"`
}

const ROOT string = "/Users/me/scheduler-example"

func main() {
	// 步骤1:解析配置文件
	configFile := filepath.Join(ROOT, "config", "scheduler-example.yaml")
	f, err := os.Open(configFile)
	if err != nil {
		log.Fatalln(err)
	}

	configData, err := io.ReadAll(f)
	if err != nil {
		log.Fatalln(err)
	}

	c := Config{}
	err = yaml.Unmarshal(configData, &c)
	if err != nil {
		log.Fatalln(err)
	}

	// 步骤2:验证配置
	if c.GlobalLog == "" {
		log.Fatalln("未定义全局日志")
	}
	if c.Jobs == nil {
		log.Fatalln("未定义作业")
	}
	for _, j := range c.Jobs {
		if j.Name == "" {
			log.Fatalln("未定义作业名称")
		}
		if j.Command == "" {
			log.Fatalln("未定义作业命令")
		}
		if j.Frequency == "" {
			log.Fatalln("未定义作业频率")
		}
	}

	// 步骤3:创建调度器并添加作业
	s := gocron.NewScheduler(time.UTC)

	for _, j := range c.Jobs {
		script := filepath.Join(ROOT, "scripts", j.Command)
		cmd := exec.Command(script)
		cmdLog := filepath.Join(ROOT, "logs", j.Log)
		l, err := os.OpenFile(cmdLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
		if err != nil {
			log.Fatalln(err)
		}
		cmd.Stdout = l
		cmd.Stderr = l
		freq := j.Frequency
		tag := j.Tag
		s.Every(freq).Tag(tag).Do(func() {
			err = cmd.Run()
			if err != nil {
				log.Fatalln(err)
			}
		})
	}

	// 步骤4:运行调度器
	s.StartBlocking()
}

配置文件:

GlobalLog: /tmp/scheduler-example.log
Jobs:
  - Name: test1
    Command: test1.sh
    Frequency: 5s
    Tag: test1
    Log: test1.log
  - Name: test2
    Command: test2.sh
    Frequency: 5s
    Tag: test2
    Log: test2.log

目录结构:

-> tree .
.
├── config
│   └── scheduler-example.yaml
├── go.mod
├── go.sum
├── logs
│   ├── test1.log
│   └── test2.log
├── scheduler-example.go
└── scripts
    ├── test1.sh
    └── test2.sh

3 directories, 8 files

test1.sh 脚本:

#!/bin/env bash

for i in {1..100}; do
    echo i
    sleep 10
done

感谢任何和所有的帮助!

英文:

I'm writing the basics of a custom scheduling tool, which will read a config file for "jobs" and add them to the schedule to run the periodically. It's very basic for now, like a proof of concept before refactoring and some additional more advanced features.

When I try to run this program, it reports that the script is not found. The script "test1.sh" does exist in the path where I'm trying to run it from, and it has execute permissions.

I'm getting the following error, but can't explain it or work it out as the script does exist at the path I'm running it:

-> ./scheduler-example
2021/08/16 12:48:54 fork/exec /Users/me/scheduler-example/scripts/test1.sh: no such file or directory

The scheduler code:

package main

import (
	"io"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/go-co-op/gocron"
	"gopkg.in/yaml.v3"
)

type Config struct {
	GlobalLog string `yaml:"GlobalLog"`
	Jobs      []Job  `yaml:"Jobs"`
}

type Job struct {
	Name      string `yaml:"Name"`
	Command   string `yaml:"Command"`
	Frequency string `yaml:"Frequency"`
	Tag       string `yaml:"Tag,omitempty"`
	Log       string `yaml:"Log,omitempty"`
}

const ROOT string = "/Users/me/scheduler-example"

func main() {
	// STEP1: Parse the config file
	configFile := filepath.Join(ROOT, "config", "scheduler-example.yaml")
	f, err := os.Open(configFile)
	if err != nil {
		log.Fatalln(err)
	}

	configData, err := io.ReadAll(f)
	if err != nil {
		log.Fatalln(err)
	}

	c := Config{}
	err = yaml.Unmarshal(configData, &c)
	if err != nil {
		log.Fatalln(err)
	}

	// STEP2: Validate the config
	if c.GlobalLog == "" {
		log.Fatalln("Global log not defined")
	}
	if c.Jobs == nil {
		log.Fatalln("No jobs defined")
	}
	for _, j := range c.Jobs {
		if j.Name == "" {
			log.Fatalln("Job name not defined")
		}
		if j.Command == "" {
			log.Fatalln("Job command not defined")
		}
		if j.Frequency == "" {
			log.Fatalln("Job frequency not defined")
		}
	}

	// STEP3: Create the scheduler and add jobs
	s := gocron.NewScheduler(time.UTC)

	for _, j := range c.Jobs {
		script := filepath.Join(ROOT, "scripts", j.Command)
		cmd := exec.Command(script)
		cmdLog := filepath.Join(ROOT, "logs", j.Log)
		l, err := os.OpenFile(cmdLog, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
		if err != nil {
			log.Fatalln(err)
		}
		cmd.Stdout = l
		cmd.Stderr = l
		freq := j.Frequency
		tag := j.Tag
		s.Every(freq).Tag(tag).Do(func() {
			err = cmd.Run()
			if err != nil {
				log.Fatalln(err)
			}
		})
	}

	// STEP4: Run the scheduler
	s.StartBlocking()
}

Config file:

GlobalLog: /tmp/scheduler-example.log
Jobs:
  - Name: test1
    Command: test1.sh
    Frequency: 5s
    Tag: test1
    Log: test1.log
  - Name: test2
    Command: test2.sh
    Frequency: 5s
    Tag: test2
    Log: test2.log

Directory structure:

-> tree .
.
├── config
│   └── scheduler-example.yaml
├── go.mod
├── go.sum
├── logs
│   ├── test1.log
│   └── test2.log
├── scheduler-example.go
└── scripts
    ├── test1.sh
    └── test2.sh

3 directories, 8 files

The test1.sh script:

#!/bin/env bash

for i in {1..100}; do
    echo i
    sleep 10
done

Thanks for any and all help!

答案1

得分: 3

根据Go文档的说明:

包exec用于运行外部命令。它包装了os.StartProcess,使得重新映射标准输入和标准输出、连接I/O管道以及进行其他调整更加容易。

它启动了一个隔离的操作系统进程。你需要一个终端会话或直接调用bash。

在Linux中,当你调用./script.sh时,实际上是调用了/bin/sh script.sh./script.sh之所以能够工作,是因为终端(或者对于Windows上的.bat文件是CMD)将文件视为纯粹的bash命令,逐行执行。因此,要运行用户定义的bash文件,我们可以使用以下方式:

cmd := exec.Command("/bin/sh", script)

或者

cmd := exec.Command("bash", script)

更新
如果这种方法对你有效,请阅读torek的答案,了解为什么这样做能够起作用。

实际上,我之前关于"./script.py是bash ./script.sh的说法不完全正确。

英文:

according to Go documents

> Package exec runs external commands. It wraps os.StartProcess to make
> it easier to remap stdin and stdout, connect I/O with pipes, and do
> other adjustments.

It starts an isolated OS process. what you need is a terminal session or a direct bash call.

In Linux when you call ./script.sh it actually means /bin/sh script.sh. ./script.sh works because the terminal (or CMD for .bat files on windows) treats the file as pure bash commands, line by line. so to run user-defined bash files we may use

cmd := exec.Command("/bin/sh",script)

or

cmd := exec.Command("bash",script)

UPDATE:
if this approach worked for you, read torek's answare to learn why exactly this works.

Actually what I said about "./script.py beeing bash ./script.sh is" is not quiet right

答案2

得分: 3

这是对OZahed的回答的一个小的技术修正,与Go语言无关,但如果你在Linux和Unix系统上编写代码,这是有用的。

在Linux中,当你调用./script.sh时,实际上是调用/bin/sh script.sh

这并不完全正确。

在类Unix系统上,当你在终端提示符($>%或你设置的任何提示符)下输入命令时,你会输入:

$ cmd arg1 arg2

例如。你使用的shell/bin/sh/bin/bash/usr/local/bin/bashdashtcshfish等)负责分解输入的命令行并尝试运行一个或多个进程。一般来说,它们会将这个命令分解为cmdarg1等等。如果其中一些单词包含shell的元字符*$等),它们将使用这些元字符执行自己的特殊操作。这些操作可能会变得非常复杂,但它们都取决于特定的shell:例如,bash中使用的语法与tcsh中使用的语法在许多重要方面有所不同。

无论如何,一旦这个shell完成了参数的分解并准备好调用与Go中的exec.Cmd相同的execve系统调用,shell通常会在使用$path$PATH变量搜索第一个或最佳可执行文件以进行execve之前调用execve(再次以与shell相关的方式)。这个系统调用:

  • 要求提供的路径指定一个文件;
  • 要求指定的文件被标记为可执行的;
  • 要求指定的文件包含操作系统自身认为可执行的数据

如果这三个测试中的任何一个失败,execve系统调用本身将失败。

在这一点上,Go的exec.Cmd和shell往往会有很大的不同。如果文件存在且被标记为可执行,但execve失败,shell通常会执行以下操作之一或两者:

  • 打开并读取(可能只是部分)文件,以尝试猜测哪个shell(如果有的话)可能运行这个文件,然后
  • 在该文件上运行该shell或某个默认shell。

这就是为什么会出现/bin/sh ./script.sh的情况。例如,如果脚本似乎是一个/bin/sh脚本,你的shell(即使是tcsh)应该使用/bin/sh ./script。如果脚本似乎是一个bash脚本,你的shell应该找到bash程序并使用./script.sh作为参数运行该程序。

关于可执行脚本的关键技巧:#!

请注意,上述要求我们从操作系统级别的execve系统调用中获得一个错误。在任何现代Unix系统上,我们可以通过在脚本开头加上一行特殊的内容避免这个错误。这个特殊的行由两个字符#!组成,后面是可选的空白,然后是一个解释器程序的路径名,后面是更多的可选空白和—根据特定的操作系统—一个或多个参数。

也就是说,如果我们将我们的shell脚本写成:

#! /bin/sh
echo this was run by /bin/sh

并将其设置为可执行,应用于这个脚本的execve系统调用的行为就好像它是一个调用/bin/sh,后面跟着这个脚本的路径名的execve系统调用。因此,如果我们使用的路径名是./script,我们就得到了运行/bin/sh ./script效果

/bin/sh部分来自脚本,所以我们可以控制解释器的确切路径。例如,如果我们编写了一个awk脚本,并且awk解释器在/usr/bin/awk中,那么:

#! /usr/bin/awk

就是正确的第一行。

有趣的是,这使我们能够编写一个自删除的脚本:

#! /bin/rm

当运行时,这个脚本会删除自身。(如果有的话,文件的其余部分是无关紧要的:/bin/rm命令只是删除了指定的文件。)使用/bin/mv作为解释器会产生一个自重命名的脚本。

由于像python这样的程序在某些系统上位于/usr/bin,而在其他系统上位于/usr/local/bin,因此这些天常见的一个技巧是使用POSIX的env命令来定位解释器的二进制文件:

#! /usr/bin/env python

调用/usr/bin/env python ./script.pyenv命令使用$PATH来定位python命令,然后调用execlexecve系统调用的C库包装器)以/usr/local/bin/python ./script.py或适当的路径名调用。如果系统同时安装了python2和python3,并且python2python3分别调用特定的变体,#! /usr/bin/env python3确保我们找到一个python3解释器,而不是python2解释器。

如果你在你的脚本中使用了适当的#!行,你就不会问导致这些答案的问题。 为什么我的程序返回一个错误,说文件不存在,但实际上文件是存在的?

英文:

Here's a minor technical correction to OZahed's answer, not really relevant to the Go language, but useful to know if you are writing code on Linux and Unix systems.

> In Linux when you call ./script.sh it actually means /bin/sh script.sh.

This is not quite right.

On a Unix-like system, when you're sitting at a terminal prompt ($ or > or % or whatever you have your prompt set to—lots of people use magic prompt-setters so that they get their current working directory plus perhaps some Git repository information or other useful items), you'll enter a command as:

$ cmd arg1 arg2

for instance. The shell you are using—/bin/sh, /bin/bash, /usr/local/bin/bash, dash, tcsh, fish, etc.—is responsible for breaking up the entered line(s) and attempting to run one or more processes. In general, they'll break this into <kbd>cmd</kbd>, <kbd>arg1</kbd>, and so on. If some of these words contain shell metacharacters (*, $, and so on), they will do their own special actions with those metacharacters. These can get quite complicated, but they are all up to that particular shell: the syntax used in bash differs from that in tcsh, for instance, in a number of important ways.

In any case, once this shell has gotten past the point of splitting up arguments and getting things ready to invoke the same execve system call that exec.Cmd will use in Go, the shell generally does invoke execve, often after using a $path or $PATH variable to search for the first or best executable to execve (again in a shell-dependent manner). This system call:

  • requires that the supplied path name a file;
  • requires that the named file be marked with execute permissions; and
  • requires that the named file contain data that the OS itself considers executable.

If any of these three tests fail, the execve system call itself will fail.

It's at this point that Go's exec.Cmd and the shells tend to diverge rather sharply. If the file exists and is marked executable, but execve fails, a shell will usually do one or both of the following:

  • open and read (perhaps just part of) of the file to try to guess which shell, if any, might run this file, then
  • run that shell, or some default shell, on that file.

It's that last step that results in /bin/sh ./script.sh, for instance. If the script appears to be a /bin/sh script, your shell—even if it's tcsh—should use /bin/sh ./script. If the script appears to be a bash script, your shell should locate the bash program and run that program with ./script.sh as an argument.

The key trick to know about executable scripts: #!

Note that the above requires that we get an error from the OS-level execve system call. We can avoid this error, on any modern Unix system, by starting our script with a special line. This special line takes the form of two characters, # and !, followed by optional white space, followed by the path name of an interpreter program, followed by more optional white space and—depending on the particular OS—one or more arguments.

That is, if we write our shell script as:

#! /bin/sh
echo this was run by /bin/sh

and make it executable, the execve system call, applied to this script, acts as if it were an execve system call invoking /bin/sh followed by the path name of this script. So if the path name we used was ./script, we get the effect of running /bin/sh ./script.

The /bin/sh part came out of the script, so we get to control the exact path of the interpreter. If we write an awk script, for instance, and if the awk interpreter is in /usr/bin/awk, then:

#! /usr/bin/awk

is the correct first line.

Amusingly, this gives us the ability to write a self-removing script:

#! /bin/rm

When run, this script removes itself. (The remainder of the file, if any, is irrelevant: the /bin/rm command simply removes the named file.) Using /bin/mv as the interpreter produces a self-renaming script.

A common trick these days, due to programs like python being in /usr/bin on some systems and /usr/local/bin on others, is to use the POSIX env command to locate the binary for the interpreter:

#! /usr/bin/env python

invokes /usr/bin/env python ./script.py. The env command uses $PATH to locate the python command and then calls execl (a C library wrapper for the execve system call) with /usr/local/bin/python ./script.py or whatever is appropriate. If a system has both python2 and python3 installed, with python2 and python3 invoking the specific variant, #! /usr/bin/env python3 ensures that we find a python3 interpreter, rather than a python2 interpreter.

Had you written your script with the appropriate #! line, you would never have asked the question that led to these answers. 为什么我的程序返回一个错误,说文件不存在,但实际上文件是存在的?

huangapple
  • 本文由 发表于 2021年8月16日 20:11:58
  • 转载请务必保留本文链接:https://go.coder-hub.com/68802590.html
匿名

发表评论

匿名网友

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

确定