根据文件和目录名称的第一个字母对文件和目录进行分组,并将它们移动到该字母的目录中

根据文件和目录名称的第一个字母对文件和目录进行分组,并将它们移动到该字母的目录中

我有一个包含以下内容的目录:

$ mkdir dir && cd "$_"
~/dir $ mkdir a1 a2 a3 a4 b1 c1 c2 1a 2a 2b 2c 2d 3a _1 _2
~/dir $ touch a_1 a_2 a_3 a_4 b_1 c_1 c_2 1_a 2_a 2_b 2_c 2_d 3_a __1 __2
~/dir $ ls
__1  1_a  __2  2_a  2_b  2_c  2_d  3_a  a_1  a_2  a_3  a_4  b_1  c_1  c_2
_1   1a   _2   2a   2b   2c   2d   3a   a1   a2   a3   a4   b1   c1   c2

现在我想对所有这些文件和目录进行分组根据他们的第一封信并将它们移至相同字母的目录。所以输出将是:

~/dir $ ls
_  1  2  3  a  b  c

并使用埃克萨,树看起来像这样:

~/dir $ exa --tree
.
├── 1
│  ├── 1_a
│  └── 1a
├── 2
│  ├── 2_a
│  ├── 2_b
│  ├── 2_c
│  ├── 2_d
│  ├── 2a
│  ├── 2b
│  ├── 2c
│  └── 2d
├── 3
│  ├── 3_a
│  └── 3a
├── _
│  ├── _1
│  ├── _2
│  ├── __1
│  └── __2
├── a
│  ├── a1
│  ├── a2
│  ├── a3
│  ├── a4
│  ├── a_1
│  ├── a_2
│  ├── a_3
│  └── a_4
├── b
│  ├── b1
│  └── b_1
└── c
   ├── c1
   ├── c2
   ├── c_1
   └── c_2

我知道我可以使用通配符移动:

~/dir $ mkdir a && mv a* a

这会引发错误:

mkdir: cannot create directory ‘a’: File exists

但它完成了工作。我可以这样做来避免错误:

~/dir $ mkdir temp && mv a* temp && mv temp a

然后我可以在 for 循环中使用它来处理我所知道的每个字母。但问题是我不知道那些第一个字母可能是什么,我们有很多字母。有没有一种方法可以在不需要知道这些字母的情况下实现这一目标?

答案1

只需迭代所有的事情:

#!/bin/bash
for f in *; do 
    firstChar="${f:0:1}"; 
    mkdir -p -- "$firstChar"; 
    mv -- "$f" "$firstChar"; 
done

如果目录已经存在并且是 Ksh/Bash/Zsh 语法“获取第一个字符”,-p则告诉不要抱怨。mkdir${f:0:1}

它正在发挥作用:

$ mkdir a1 a2 a3 a4 b1 c1 c2 1a 2a 2b 2c 2d 3a _1 _2
$ touch a_1 a_2 a_3 a_4 b_1 c_1 c_2 1_a 2_a 2_b 2_c 2_d 3_a __1 __2
$ ls -F
__1  _1/  1_a  1a/  __2  _2/  2_a  2a/  2_b  2b/  2_c  2c/  2_d  2d/  3_a  3a/  a_1  a1/  a_2  a2/  a_3  a3/  a_4  a4/  b_1  b1/  c_1  c1/  c_2  c2/

$ for f in *; do 
    firstChar="${f:0:1}"; 
    mkdir -p "$firstChar"; 
    mv -- "$f" "$firstChar"; 
done

$ tree
.
├── _
│   ├── __1
│   ├── _1
│   ├── __2
│   └── _2
├── 1
│   ├── 1_a
│   └── 1a
├── 2
│   ├── 2_a
│   ├── 2a
│   ├── 2_b
│   ├── 2b
│   ├── 2_c
│   ├── 2c
│   ├── 2_d
│   └── 2d
├── 3
│   ├── 3_a
│   └── 3a
├── a
│   ├── a_1
│   ├── a1
│   ├── a_2
│   ├── a2
│   ├── a_3
│   ├── a3
│   ├── a_4
│   └── a4
├── b
│   ├── b_1
│   └── b1
└── c
    ├── c_1
    ├── c1
    ├── c_2
    └── c2

22 directories, 15 files

请注意,如果现有文件或目录名只有一个字符,则会导致错误。例如,如果您创建一个名为的文件a并尝试上述方法,您将得到:

mkdir: cannot create directory ‘a’: File exists
mv: 'a' and 'a' are the same file

如果这是一个问题,您可以执行一些复杂的操作,例如重命名为临时文件、创建目录并将文件移入其中:

for f in *; do 
    firstChar="${f:0:1}";
    ## if the first character exists as a dir, move the file there
    if [[ -d "$firstChar" ]]; then
       mv -- "$f" "$firstChar"; 
    ## if it exists but is not a dir, move to a temp location,
    ## re-create it as a dir, and move back from temp
    elif [[ -e "$firstChar" ]]; then
        tmp=$(mktemp ./tmp.XXXXX)
        mv -- "$firstChar" "$tmp"
        mkdir -p "$firstChar"
        mv -- "$tmp" "$firstChar"/"$f"
    else    
        mkdir -p "$firstChar"; 
        mv -- "$f" "$firstChar"; 
    fi
done

答案2

POSIXly:

find . -path './*' -prune -exec sh -c '
    j="${1%${1#???}}"; mkdir "$j" && mv -- "$1" "$j/"
' _ {} \; 2>/dev/null

答案3

另一种没有提到的方法是找到文件和目录的所有唯一首字母,将所有内容移动到临时目录,创建首字母目录,然后移回所有内容。

letters=($(ls -1 | sort -n | cut -c 1 | uniq))
temp=$(mktemp -dp .)
for letter in "${letters[@]}"; do
    mv "$letter"* "$temp";
    mkdir "$letter";
    mv "$temp/"* "$letter";
done
rm -r "$temp"

通过这种方法,单字母目录和文件也受到照顾。行动中:

$ mkdir a1 a2 a3 a4 b1 c1 c2 1a 2a 2b 2c 2d 3a _1 _2
$ touch a_1 a_2 a_3 a_4 b_1 c_1 c_2 1_a 2_a 2_b 2_c 2_d 3_a __1 __2
$ mkdir a b c
$ touch 1 2 3 _
$ ls -F
_    1    __2  2_a  2b/  2_d  3_a  a_1  a2/  a_4  b_1  c_1  c2/
__1  1_a  _2/  2a/  2_c  2d/  3a/  a1/  a_3  a4/  b1/  c1/
_1/  1a/  2    2_b  2c/  3    a/   a_2  a3/  b/   c/   c_2

在该脚本之后,结果变为:

$ ls -F
_/  1/  2/  3/  a/  b/  c/
$ exa --tree
.
├── 1
│  ├── 1
│  ├── 1_a
│  └── 1a
├── 2
│  ├── 2
│  ├── 2_a
│  ├── 2_b
│  ├── 2_c
│  ├── 2_d
│  ├── 2a
│  ├── 2b
│  ├── 2c
│  └── 2d
├── 3
│  ├── 3
│  ├── 3_a
│  └── 3a
├── _
│  ├── _
│  ├── _1
│  ├── _2
│  ├── __1
│  └── __2
├── a
│  ├── a
│  ├── a1
│  ├── a2
│  ├── a3
│  ├── a4
│  ├── a_1
│  ├── a_2
│  ├── a_3
│  └── a_4
├── b
│  ├── b
│  ├── b1
│  └── b_1
└── c
   ├── c
   ├── c1
   ├── c2
   ├── c_1
   └── c_2

通过这种方法,您只需创建必要的目录。因此mkdir命令运行的次数尽可能少。

编辑:作为@terdon 提到,解析 的输出ls将导致命令newline在其名称中的文件上失败。为了解决这个问题,我们可以使用这个:

temp=$(mktemp -dp .)
for f in *; do
    letter="${f:0:1}";
    mv -- "$letter"* "$temp";
    mkdir "$letter";
    mv "$temp/"* "$letter";
done
rm -r "$temp"

答案4

zsh

autoload -Uz zmv # best in ~/.zshrc
() { mkdir ${(uM)@#?}; } ??* && zmv '(?)?*' '$1/$f'

我们有:

  • () { body; } args:一个匿名函数,这里传递扩展??*(由至少 2 个字符组成的文件名)作为参数。在身体里:
  • ${@#?}将是 args 被剥夺第一个字符,但使用M(用于匹配的)参数扩展标志,情况会相反,只保留第一个字符。u(对于唯一)删除重复项。
  • 然后zmv '(?)?*' '$1/$f'将至少包含 2 个字符的文件移动到各自的文件夹中。

相关内容