如何将Bash命令传递给`entr`,并使用引号保护文件名中的空格?

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

How to pass a Bash command to `entr`, quoting to guard against filenames with spaces?

问题

我的目标

我正在编写一个小型Bash脚本,该脚本使用 entr,这是一个在检测到文件系统事件时重新运行任意命令的实用工具。我的直接目标是将entr传递一个命令,该命令将给定的Markdown文件转换为HTML。每当Markdown文件发生更改时,entr都会运行此命令。一个简化但有效的脚本如下:

# 脚本 1
in="$1"
out="${in%.md}.html"
echo "$in" | entr pandoc "${in}" -o "${out}"

这个脚本运行良好。要监视的文件名是通过stdin提供给entr的。在检测到文件更改时,entr会运行其参数中指定的命令。在此示例中,即pandoc以及它后面的所有参数,以将Markdown文件转换为HTML文件。

为了将来参考,set -x 显示了entr的调用方式,正如我们所期望的那样。在整个示例中,以+开头的行显示了来自set -x的输出:

+ entr pandoc 'READ ME.md' -o 'READ ME.html'

问题

我想根据给定输入文件的文件类型查找提供给entr的命令。因此,文件转换命令以变量的形式出现,并且我想将该变量用作entr的命令行参数。但我无法正确进行引用。再次简化:

# 脚本 2
in="$1"
out="${in%.md}.html"
cmd="pandoc \"${in}\" -o \"${out}\""
echo "$in" | entr "$cmd"

(shellcheck.net 在上面未检测到任何问题)

这会失败。因为脚本2中最后一行中的"$cmd"在引号内,所以整个$cmd 被视为entr的单个参数:

+ entr 'pandoc "READ ME.md" -o "READ ME.html"'

entr 尝试将整个内容解释为一个可执行文件的名称,但找不到它:

entr: exec pandoc "READ ME.md" -o "READ ME.html": No such file or directory

那么我应该如何修改脚本2,以使用$cmd的内容作为entr的参数?

我尝试过什么?

  1. 检查$cmd 是否被形成为我期望的方式?如果我在脚本2中定义它之后立即 echo "$cmd" ,它看起来正是我期望的样子:

    pandoc "READ ME.md" -o "READ ME.html"
    

    我尝试过尝试构建cmd 的其他方法,比如:

    cmd='pandoc "${in}" -o "${out}"'
    

    但类似这种的变化会产生与$cmd 相同的值,并且与脚本2相同的行为。

  2. 尝试引用$cmd 的使用?

    由于脚本2中的最后一行错误地将整个"$cmd" 视为单个参数,而我们希望它将单词拆分为单独的参数,也许去掉引号并使用裸露的$cmd 是朝着正确方向迈出的一步?

    echo "$in" | entr $cmd
    

    预料之中,这会在每个空格上分割$cmd ,甚至是在我们的双引号内的空格:

    + entr pandoc ' "READ ' ME.md"' -o ' "READ ' ME.html"'
    

    这会导致Pandoc尝试并失败,尝试打开名为"READ的文件:

    pandoc: "READ: openBinaryFile: does not exist (No such file or directory)
    
  3. 尝试使用printf 构建$cmd

    我注意到printf -v 可以将输出存储在一个变量中。那么,使用这个代替分配给cmd 如何呢?

    printf -v cmd 'pandoc "%s" -o "%s"' "$in" "$out"
    

    预料之中,这会产生与脚本2相同的结果。我尝试了一些猜测性的变化,比如在格式字符串中使用%q,或直接在格式字符串中使用$in$out,但没有发现任何有助于解决问题的东西。

  4. 尝试使用${var@Q} 参数扩展的形式。

    echo "$in" | entr ${cmd@Q}
    

    无论是否在${cmd@q} 使用双引号,都没有成功,我猜我对@Q 的用途有所误解。

    + entr ''\''pandoc' '"READ ' ME.md"' -o ' '"READ ' ME.html"'\'''
    entr: exec ''pandoc: No such file or directory
    

详细信息

我使用的是Bash v5.1.16,在基于Ubuntu 22.04(Jammy)的Pop!_OS 22.04 中使用。

Ubuntu Jammy(22.04)中当前的apt 版本的entr(v5.1)对我的需求来说太旧(例如,-z 标志不起作用)。所以我从最新的v5.3源代码发布 编译自己的版本。

我知道有很多关于Bash中引

英文:

My Goal

I'm writing a small Bash script, which uses entr, which is a utility to re-run arbitrary commands when it detects file-system events. My immediate goal is to pass entr a command which converts a given markdown file to HTML. entr will run this command every time the markdown file changes. A simplified but working script looks like:

# script 1
in="$1"
out="${in%.md}.html"
echo "$in" | entr pandoc "${in}" -o "${out}"

This works fine. The filename to be watched is supplied to entr on stdin. On detecting changes in that file, entr runs the command specified by its args. In this example that is pandoc, and all the args after it, to convert the markdown file to an HTML file.

For future reference, set -x shows that entr was invoked as we'd expect. (Throughout, lines starting with + show the output from set -x):

+ entr pandoc 'READ ME.md' -o 'READ ME.html'

The problem

I want to look-up the command given to entr depending on the file-type of the
given input file. So the file-conversion command ends up in a variable, and I want to use that variable as the command-line args to entr. But I can't get the quoting right.
Again, simplified:

# script 2
in="$1"
out="${in%.md}.html"
cmd="pandoc \"${in}\" -o \"${out}\""
echo "$in" | entr "$cmd"

(shellcheck.net detects no issues on the above)

This fails. Because "$cmd" in the final line is in quotes, the entirety of $cmd
is treated as a single arg to entr:

+ entr 'pandoc "READ ME.md" -o "READ ME.html"'

entr tries to interpret the whole thing as the name of an executable, which
it cannot find:

entr: exec pandoc "READ ME.md" -o "READ ME.html": No such file or directory

So how should I modify script 2, to use the content of $cmd as the args to
entr?

What have I tried?

  1. Check that $cmd is being formed as I expect? If I echo "$cmd" right after
    it is defined in script 2, it looks exactly how I'd hope:

    pandoc "READ ME.md" -o "READ ME.html"
    

    I tried messing around with alternate ways of constructing cmd, such as:

    cmd='pandoc "'"${in}"'" -o "'"${out}"'"'
    

    but variations like this produce identical values of $cmd, and identical
    behavior as script2.

  2. Try not quoting the use of $cmd?

    Since the final line of script 2 erroneously treats the whole of "$cmd"
    as a single arg, and we want it to split up the words into seprate args
    instead, maybe removing the quotes and using a bare $cmd is a step in the
    right direction?

    echo "$in" | entr $cmd
    

    Predictably enough though, this splits $cmd up on every space, even the
    ones inside our double-quotes:

    + entr pandoc '"READ' 'ME.md"' -o '"READ' 'ME.html"'
    

    This makes Pandoc try, and fail, to open a file called "READ:

    pandoc: "READ: openBinaryFile: does not exist (No such file or directory)
    
  3. Try constructing $cmd using printf?

    I notice printf -v can store output in a variable. How about using that
    instead of assiging to cmd?

    printf -v cmd 'pandoc "%s" -o "%s"' "$in" "$out"
    

    Predictably enough, this produces the same results as script2. I tried some
    speculative variations, such as %q in the format string, or using $in
    and $out directly in the format string, but didn't stumble on anything
    that seemed to help.

  4. Try using the ${var@Q} form of parameter expansion.

    echo "$in" | entr ${cmd@Q}
    

    Tried with and without double quotes around the use of ${cmd@q}. No joy,
    I guess I'm misunderstanding what @Q is for.

    + entr ''\''pandoc' '"READ' 'ME.md"' -o '"READ' 'ME.html"'\'''
    entr: exec 'pandoc: No such file or directory
    

Details

I'm using Bash v5.1.16, in Pop!_OS 22.04, derived from Ubuntu 22.04 (Jammy).

The current 'apt' version of entr (v5.1) in Ubuntu Jammy (22.04) is too old
for my needs (e.g. the -z flag doesn't work.) so I'm compiling my own from
the latest v5.3 source release.

I know there are a lot of questions about quoting in Bash, but I don't see any that seem to match this. Apologies if I'm wrong.

答案1

得分: 1

将命令组装成一个数组,而不是一个字符串。

我在某处读到,也许$@可以做我需要的事情,所以我将$cmd的各个部分放入一个数组中:

in="$1"
out="${in%.md}.html"
cmd=(pandoc "$in" -o "$out")
echo "$in" | entr "${cmd[@]}"

这正确引用了${cmd[@]}中需要引用的项(例如,其中有空格的项)。

+ entr pandoc 'READ ME.md' -o 'READ ME.html'

因此,‘entr’成功调用‘pandoc’,并成功转换文档。它有效!我承认我没有预料到这一点。

这种方法似乎适用于其他类似的情况,不仅仅是在调用entr时。

所以我有一个解决方案。对于我的未来计划来说,这似乎不是完全理想的。我曾幻想过这些‘文件转换命令’可以配置,并因此在某个文本文件中定义,以便用户(也就是我,很可能)可以覆盖它们并定义自己的命令,但当命令被定义为数组而不是字符串时,我对如何做这个并不够流利。

我不禁感到我可能忽略了更简单的方法。

英文:

Assemble the command as an array, instead of a string.

I read somewhere that maybe $@ might do what I need, so I put the parts of $cmd into an array:

in="$1"
out="${in%.md}.html"
cmd=(pandoc "$in" -o "$out")
echo "$in" | entr "${cmd[@]}"

This correctly quotes the items in ${cmd[@]} which require it (e.g. have spaces in.)

+ entr pandoc 'READ ME.md' -o 'READ ME.html'

So ‘entr’ successfully calls ‘pandoc’, which successfully converts the documents. It works! I confess I did not expect that.

This approach seems viable for other similar situations, not just when invoking entr.

So I have a solution. It doesn't seem completely ideal for my future plans. I had visions of these 'file conversion commands' being configurable, and hence defined in a text file somewhere, so that users (==me, probably) could override them and define their own, and I'm not fluent enough with Bash to be sure how to go about that when commands are defined as arrays instead of strings.

I can't help but feel I've overlooked something simpler.

答案2

得分: 0

使用shell来解释"$cmd"的值,即:

# 将这个:
echo "$in" | entr "$cmd"
# 替换为这个:
echo "$in" | entr sh -c "$cmd"

同样,entr有一个-s选项,它为您调用一个shell(使用$SHELL中的第一个单词选择):

echo "$in" | entr -s "$cmd"

使用'-s'同样有效,而'entr'每次执行$cmd时都会有用的将"Bash退出值: x"打印到标准输出,这可能是您想要的,也可能不是。

英文:

Use a shell to interpret the value of "$cmd", ie:

# replace this:
echo "$in" | entr "$cmd"
# with this:
echo "$in" | entr sh -c "$cmd"

Similarly, entr has a -s option which invokes a shell for you (chosen using the first word in $SHELL):

echo "$in" | entr -s "$cmd"

Using '-s' works equally well, and 'entr' helpfully prints "Bash exit value: x" to stdout each time it executes $cmd, which may or may not be what you want.

huangapple
  • 本文由 发表于 2023年2月16日 11:34:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/75467601.html
匿名

发表评论

匿名网友

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

确定