使用所有类型的字符重命名大量文件,具有 POSIX 可移植性

使用所有类型的字符重命名大量文件,具有 POSIX 可移植性

有时我需要重命名目录中的所有文件(重命名约定稍后介绍),其中文件名始终采用“filenamename.extension”的形式(扩展名始终存在并变化)。该名称可能包含空格和 [:graph:] 类中的字符。我的第一个问题是它应该在 *NIX 系统(特别是 Linux、BSD,以及后来的其他系统,比如 AIX)之间绝对可移植。我的第二个问题是 [:graph:] 类。文件名可以是:

cat.txt
dog_and_cat.txt
Where is the cat?.png
my.cat.is.cute.txt.js.html
;;; ;;; ;;;.......321
áéúő _[a lot of whitespaces]_ óü^^^^^ö.jpg

很容易看出,这些很难处理并放入 for 循环中。例如,

for i in *; do something; done

并不总是喜欢空格和奇怪的字符,尤其是在不同的操作系统中。

重命名约定是将所有文件重命名为某种哈希值的$FOOBAR.$EXTENSION形式$FOOBAR,例如 md5sum。所以在 for 循环中我有一行类似于

mv $FILE $(md5sum $FILE | sed 's/\ \ .\+//');

它会将文件移动到其自身的 md5sum 中,但扩展名会消失。我想保留扩展名,它们几乎总是在.[a-zA-Z0-9]{1,3}表单中。有时.tar.gz也需要保留类似的扩展(当然我可以将它们添加到变量中,例如MYEXTENSIONS='tar.gz tar.bz2 foo.bar')。

我的直觉告诉我,这个问题可以通过参数化良好的默认 UNIX/shell 命令来解决,但现在对我来说非常困难。我确信我会从答案中学到很多东西。我知道我说了那个神奇的词可移植性,但如果我必须指定语言,则首选解决方案是 bash。

答案1

实际上,for i in *; do something; done正确对待每个文件名,除了以 a 开头的文件名.被排除在通配符匹配之外。要以可移植方式匹配所有文件(除了...),请匹配* .[!.]* ..?*并跳过因不匹配模式保持原样而产生的任何不存在的文件。

$i如果您遇到问题,可能是因为您后来没有正确引用。始终在变量替换和命令替换两边加上双引号:"$foo","$(cmd)"除非你打算发生字段分割和通配符。

如果您需要将文件名传递给外部命令(此处不需要),请注意并不echo "$foo"总是$foo按字面打印。一些 shell 执行反斜杠扩展,并且一些$foo以 开头的值-将被视为选项。准确打印字符串的安全且符合 POSIX 标准的方法是

printf '%s' "$foo"

printf '%s\n' "$foo"在末尾添加换行符。另一件需要注意的事情是命令替换会删除尾随换行符;如果需要保留换行符,一个可能的技巧是向数据附加一个非换行符,确保转换保留该字符,最后截断该字符。例如:

mangled_file_name="$(printf '%sa' "$file_name" | tr -sc '[:alnum:]-+_.' '[_*]')"
mangled_file_name="${mangled_file_name%a}"

要提取文件的 md5sum,请避免在输出中包含文件名md5sum,因为这将使其难以剥离。将数据传递到md5sum的标准输入上。

请注意,该md5sum命令不在 POSIX 中。一些 UNIX 变体有md5或根本没有。cksum是 POSIX 但容易发生冲突。

获取文件名中的扩展名关于如何获取文件的扩展名。

让我们把它们放在一起(未经测试)。这里的一切都可以在任何 POSIX shell 下运行;您可以从 bash 功能中获得一点,但不会太多。

for old_name in * .[!.]* ..?*; do
  if ! [ -e "$old_name" ]; then continue; fi
  hash=$(md5sum <"$old_name")
  case "$old_name" in
    *.*.gz|*.*.bz2)                   # double extension
      ext=".${old_name##*.}"
      tmp="${old_name%.*}"
      ext=".${old_name##*.}$ext";;
    ?*.*) ext=".${old_name##*.}";;    # simple extension
    *) ext=;;                         # no extension
  esac
  mv -- "$old_name" "$hash$ext"
done

请注意,我没有考虑已经存在指定名称的目标文件的情况。特别是,如果您有现有文件,其名称看起来像您采用的约定,但校验和部分与文件的内容不匹配,而是与具有相同扩展名的其他文件的内容匹配,则发生的情况将取决于文件的相对字典顺序文件名。

答案2

由于这是一个相当复杂的问题,我只提供一些指导方针:

  • 双引号文件名变量无处不在。这将避免由于分词而导致的几乎所有空白问题。
  • 内部变量$()必须像外部结构一样被引用。不需要额外的转义。
  • $()``构造尾随换行符的条带,因此您必须添加不同的字符,然后将其剥离到构造之外$()

    varx="$([command which might print a value ending in \n]; echo x)"
    var="${varx%x}"
    
  • --在命令中有必要将参数与文件名分开,因为文件名可以以 开头--,因此将作为参数处理。
    • find不支持此语法,因此使用readlink来获取根据定义以斜杠开头的绝对路径,或者确保给出的路径find已经是绝对路径或以 开头./
  • 使用进程替换来<(代替管道,以避免发送进程终止时管道损坏。
  • 使用 3 到 9 之间的文件描述符而不是标准输入进行数据传递,以避免贪婪命令(例如catssh吞食所有数据)。
  • 首先,测试!我通常使用这个文件名来测试上面提到的东西:$'--$`!*@\a\b\E\f\r\t\v\\\'"\360\240\202\211 \n'

相关内容