我有以下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/filename
→ filename
;在您的情况下,这并不重要,因为$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 *.txt
或for 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.}
可以替换所有点。据我所知,没有变体可以替换最后的仅限点。