一行中的 #/bin/sh

3
我正在使用FFmpeg进行一些Haskell项目的开发。我需要批量从一个包含MP4文件的媒体文件夹中创建截图。我已经获取了代码并在Unix终端上使用它。它可以工作,但是如何使其成为一个可在Haskell中通过system "xxxx"执行的单行代码呢?
如果不使用多个system“xx”...
#/bin/sh
for i in $(ls *.mp4)
do
    ffmpeg -i $i -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  $i%1d.jpg
done

我尝试了:
import System.Cmd
function = do{system "#/bin/sh";
system "for i in $(ls *.mp4)";
system "do";
system "ffmpeg -i $i -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  $i%1d.jpg";
system "done";}

但是它会报错:
-vframes: No such file or directory
/bin/sh: Syntax error: "done" unexpected

1
第二个片段创建了四个独立的子shell。你可以尝试将它们全部压缩到一行中。或者像第一个片段那样使用一个包装脚本,但是要用#!/bin/sh作为shebang,正如另一个回答者指出的那样。 - wildplasser
4
请注意,for i in *.mp4for i in $(ls *.mp4) 更有效率。并且在那些 /bin/sh 不是指向 /bin/bash 的系统中也可以运行($(command) 是 Bash 特有的)。 - Keith Thompson
1
@wildplasser:两个版本都需要分号或换行符。 - Keith Thompson
1
@GoodGuyGreg,顺便说一下,你选择的答案是有问题的,请看看我上面的评论,了解原因。 - SiegeX
1
@KeithThompson 可能是对的,但事实也是它不是特定于Bash的。 - SiegeX
显示剩余2条评论
4个回答

8
问题在于您试图将脚本的每一行作为单独的、独立的 shell 调用来执行。您只需要使用一个 system 调用来完成所有操作,并使用 \n 分隔脚本的每一行即可。
system "for i in $(ls *.mp4)\ndo\n..."

但是你可以将Shell命令写在一行中:

system "for i in $(ls *.mp4); do ...; done"

第一行(顺便说一下,应该是#!/bin/sh)在使用system时不是必要的。
但是我不确定为什么你想要使用Haskell来执行单个shell脚本。你应该在Haskell中编写目录内容的循环,并只调用系统来执行单个转换。至少,你应该将此脚本放入自己的文件中,并使用system "sh convert.sh"或类似的方式调用它。
(如果你想要在Haskell中拥有更方便的多行字符串语法,可以尝试interpolatedstring-perl6string-qq包。)

我不仅使用Haskell来“创建截图”,还用它来创建一些带有音乐和视频信息的PDF文件,需要将截图插入到PDF中。有了这段代码,这项任务变得更加容易了。 - MrFabio

4

首先,它是 #!/bin/sh。注意感叹号。

其次,你正在尝试依次执行一系列命令,因此它们之间没有状态保持。尝试将其作为单个命令执行:

function = system "for i in $(ls *.mp4); do ffmpeg -i $i -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  $i%1d.jpg; done"

另一种选择是将整个脚本保存为 .sh 文件,并使其可执行,同时更正 #!,然后运行该文件:
function = system "./myscript.sh"

1
请注意,它需要是./myscript.sh(除非将其放入$PATH中)。 - ehird

2
您不应该像这样回显shell脚本,而是创建一个像这样的shell命令:
system "for i in $(ls *.mp4); do ffmpeg -i $i -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  $i%1d.jpg; done"

2

Bash 4.X解决方案

system "/bin/bash -c 'shopt -s globstar; for i in **.mp4; do ffmpeg -i \"$i\" -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  \"$i\"%1d.jpg; done'"
  1. 在使用system时不需要写#!/bin/bash(不要忘了bang !)
  2. 变量需要加引号,否则文件名中带有空格的文件将无法正常工作
  3. 不要像这样使用ls,当遇到文件名中带有空格的文件时会出错

Posix解决方案

system "find /some/path -type f -name \"*.mp4\" -exec sh -c 'for f; do ffmpeg -i \"$f\" -vframes 7 -y -ss 10 -s 150x150 -an -sameq -f image2 -r 1/5  \"$f%1d.jpg\"; done' _ {} +"

1
嗯...我想如果您有一个名为foo.mp4的目录,那么ls解决方案的行为可能会有所不同,但这似乎不太可能 :) - ehird
1
如果您不需要递归进入子目录并仅需要当前目录,那么*.mp4就足够了。 ls 的解决方案会在文件名中包含空格时出现问题,无论是否使用引号。 - SiegeX
1
是的,引用问题很棘手。 不过 ls 版本本身不会递归进入子目录。 - ehird
1
@GoodGuyGreg 啊,对了。system() 硬编码调用 /bin/sh,所以我们需要明确告诉它调用 bash。请参见更新后的答案。如果您想要可移植性,可以选择使用 Posix 版本。 - SiegeX
1
@GoodGuyGreg 好的,经过一番搜索,显然-print0不是posix标准,-d标志也不是用于读取的。因此,posix方法是调用find一次,然后执行/bin/sh并将找到的文件列表传递给该shell。然后该shell循环遍历这些文件以运行ffmpeg。有点笨重,但它确实是可移植的 =)。请参见更新的答案。 - SiegeX
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接