在 Bash 中动态地将文本附加到文件名

在 Bash 中动态地将文本附加到文件名

我有以下for循环来单独处理sort文件夹内的所有文本文件(即为每个文件生成一个排序的输出文件)。

for file in *.txt; 
do
   printf 'Processing %s\n' "$file"
   LC_ALL=C sort -u "$file" > "./${file}_sorted"  
done

这几乎是完美的,除了它目前输出的文件格式为:

originalfile.txt_sorted

...而我希望它以以下格式输出文件:

originalfile_sorted.txt 

这是因为${file}变量包含文件名(包括扩展名)。我在 Windows 上运行 Cygwin。我不确定这在真正的 Linux 环境中会如何表现,但在 Windows 中,扩展名的这种变化会导致 Windows 资源管理器无法访问该文件。

我如何将文件名与扩展名分开,以便可以_sorted在两者之间添加后缀,从而让我能够轻松区分文件的原始版本和排序版本,同时仍保持 Windows 的文件扩展名不变?

我一直在寻找可能可能的解决方案,但在我看来,这些解决方案更适合处理更复杂的问题。更重要的是,以我目前的bash知识,它们远远超出了我的理解范围,所以我希望有一个更简单的解决方案适用于我的这个简单的for循环,或者有人可以解释如何将这些解决方案应用于我的情况。

答案1

你链接的这些解决方案其实都挺好的。有些答案可能缺乏解释,所以让我们整理一下,也许再添加一些。

你的这句话

for file in *.txt

表示扩展名是预先知道的(注意:POSIX 兼容环境区分大小写,*.txt不会匹配FOO.TXT)。在这种情况下

basename -s .txt "$file"

应返回不带扩展名的名称(basename还会删除目录路径:/directory/path/filenamefilename;在您的情况下,这并不重要,因为$file不包含这样的路径)。要在代码中使用该工具,您需要命令替换,通常如下所示:$(some_command)。命令替换获取 的输出some_command,将其视为字符串并将其放置在 的位置$(…)。您的特定重定向将是

… > "./$(basename -s .txt "$file")_sorted.txt"
#      ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the output of basename will replace this

这里嵌套引号是可以的,因为 Bash 足够智能,知道里面的引号$(…)是配对的。

这可以改进。注意basename是一个单独的可执行文件,而不是 shell 内置的(在 Bash 中运行type basename,与 相比type cd)。生成任何额外的进程都是昂贵的,需要资源和时间。在循环中生成它通常性能不佳。因此,您应该使用 shell 为您提供的任何功能来避免额外的进程。在这种情况下,解决方案是:

… > "./${file%.txt}_sorted.txt"

下面针对更一般的情况解释了语法。


如果您不知道扩展名:

… > "./${file%.*}_sorted.${file##*.}"

语法解释:

  • ${file#*.}$file,但匹配的最短字符串*.从前面移除;
  • ${file##*.}– ,但从前面删除$file最长的匹配字符串;使用它只得到一个扩展名;*.
  • ${file%.*}– ,但从末尾移除$file最短的字符串匹配;使用它来获取除扩展名之外的所有内容;.*
  • ${file%%.*}$file,但与最长字符串匹配的部分.*从末尾删除;

模式匹配类似于 glob,而不是正则表达式。这意味着*是零个或多个字符的通配符,?是恰好一个字符的通配符(不过,在您的情况下我们不需要?)。当您调用ls *.txtfor file in *.txt;时,您使用相同的模式匹配机制。允许使用没有通配符的模式。我们已经使用了模式的${file%.txt}位置。.txt

例子:

$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name

但请注意:

$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless

因此,以下装置可能有用(但事实并非如此,解释如下):

${file#${file%.*}}

它的工作原理是识别除扩展名(${file%.*})之外的所有内容,然后将其从整个字符串中删除。结果如下:

$ file=name.name2.name3.ext
$ echo "${file#${file%.*}}"
.ext
$ file=extensionless
$ echo "${file#${file%.*}}"

$   # empty output above

注意.这次包含了。如果$file包含文字*或,您可能会得到意外的结果?;但 Windows(扩展很重要)不允许文件名中无论如何都会有这些字符,所以您可能不在乎。但是,如果存在[…]{…},可能会触发它们自己的模式匹配方案并破坏解决方案!

您的“改进”重定向将是:

… > "./${file%.*}_sorted${file#${file%.*}}"

它应该支持带或不带扩展名的文件名,但不幸的是,不支持带方括号或花括号的文件名。真可惜。要修复它,您需要用双引号括住内部变量。

真正改进了重定向:

… > "./${file%.*}_sorted${file#"${file%.*}"}"

双引号${file%.*}不充当模式!Bash 足够聪明,可以区分内引号和外引号,因为内引号嵌入在外引号${…}语法中。我认为这是正确的方法

另一个(不完美的)解决方案,让我们出于教育原因来分析它:

${file/./_sorted.}

.它用替换第一个_sorted.。如果 中最多只有一个点,它将正常工作$file。有一个类似的语法${file//./_sorted.}可以替换所有点。据我所知,没有变体可以替换最后的仅限点。

相关内容