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

Has*_*ziz 6 windows cygwin bash filenames bash-scripting

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

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

这几乎是完美的,除了它当前以以下格式输出文件:

originalfile.txt_sorted
Run Code Online (Sandbox Code Playgroud)

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

originalfile_sorted.txt 
Run Code Online (Sandbox Code Playgroud)

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

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

我一直在看什么或许可能的解决方案,但对我来说,这些似乎更配备以处理更复杂的问题。更重要的是,以我目前的bash知识,它们超出了我的头脑,所以我希望有一个更简单的解决方案适用于我的简陋for循环,或者有人可以解释如何将这些解决方案应用于我的情况。

Kam*_*ski 20

您链接到的这些解决方案实际上非常好。有些答案可能没有解释,所以让我们整理一下,可能会添加一些。

你的这条线

for file in *.txt
Run Code Online (Sandbox Code Playgroud)

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

basename -s .txt "$file"
Run Code Online (Sandbox Code Playgroud)

应该返回没有扩展名的名称(basename也删除目录路径:/directory/path/filename? filename;在你的情况下这无关紧要,因为$file不包含这样的路径)。要在代码中使用的工具,你需要命令替换的是长相一般是这样的:$(some_command)。命令替换采用 的输出some_command,将其视为字符串并将其放置在原$(…)处。您的特定重定向将是

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

嵌套引号在这里没问题,因为 Bash 足够聪明,知道其中的引号$(…)是配对在一起的。

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

… > "./${file%.txt}_sorted.txt"
Run Code Online (Sandbox Code Playgroud)

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


如果您不知道扩展名:

… > "./${file%.*}_sorted.${file##*.}"
Run Code Online (Sandbox Code Playgroud)

语法解释:

  • ${file#*.}$file,但将匹配的最短字符串*.从前面去掉;
  • ${file##*.}$file,但最长的字符串匹配*.从前面去掉;用它来获得扩展;
  • ${file%.*}$file,但将匹配的最短字符串.*从末尾移除;用它来获得除扩展之外的一切;
  • ${file%%.*}$file,但与最长的字符串匹配.*从末尾删除;

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

例子:

$ file=name.name2.name3.ext
$ echo "${file#*.}"
name2.name3.ext
$ echo "${file##*.}"
ext
$ echo "${file%.*}"
name.name2.name3
$ echo "${file%%.*}"
name
Run Code Online (Sandbox Code Playgroud)

但要注意:

$ file=extensionless
$ echo "${file#*.}"
extensionless
$ echo "${file##*.}"
extensionless
$ echo "${file%.*}"
extensionless
$ echo "${file%%.*}"
extensionless
Run Code Online (Sandbox Code Playgroud)

出于这个原因,以下装置可能有用(但它不是,下面的解释):

${file#${file%.*}}
Run Code Online (Sandbox Code Playgroud)

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

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

$   # empty output above
Run Code Online (Sandbox Code Playgroud)

请注意,这次.包括在内。如果$file包含文字*?;你可能会得到意想不到的结果。但是 Windows(扩展很重要)无论如何都不允许在文件名中使用这些字符,因此您可能不在乎。然而[…]or {…},如果存在,可能会触发他们自己的模式匹配方案并破坏解决方案!

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

… > "./${file%.*}_sorted${file#${file%.*}}"
Run Code Online (Sandbox Code Playgroud)

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

真正改进的重定向:

… > "./${file%.*}_sorted${file#"${file%.*}"}"
Run Code Online (Sandbox Code Playgroud)

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

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

${file/./_sorted.}
Run Code Online (Sandbox Code Playgroud)

它取代了第一个._sorted.。如果您在$file. 有一个类似的语法${file//./_sorted.}可以替换所有的点。据我所知,没有仅替换最后一个点的变体。