使用Bash脚本解析`ls -l`命令的结果

3
我需要使用Bash脚本存储目录中每个文件的名称,并对其进行某些处理:
drwxrwxr-x  5 matteorr matteorr  4096 Jan 10 17:37 Cluster
drwxr-xr-x  2 matteorr matteorr  4096 Jan 19 10:43 Desktop
drwxrwxr-x  9 matteorr matteorr  4096 Jan 20 10:01 Developer
drwxr-xr-x 11 matteorr matteorr  4096 Dec 20 13:55 Documents
drwxr-xr-x  2 matteorr matteorr 12288 Jan 20 13:44 Downloads
drwx------ 11 matteorr matteorr  4096 Jan 20 14:01 Dropbox
drwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Music
drwxr-xr-x  2 matteorr matteorr  4096 Jan 19 22:12 Pictures
drwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Public
drwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Templates
drwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Videos

使用以下命令,我能够将 ls -l 的结果在每个空格处分割,并访问包含名称的最后一个元素:

ls -l | awk '{split($0,array," ")} END{print array[9]}'

然而,它仅返回最后一行(即Videos),因此我需要迭代所有由ls -l命令返回的行。
  • 我该如何做到这一点?
  • 有没有更好的方法来解决整个问题?

添加部分

对于目录中包含的所有文件,如果它是一个文件,则不执行任何操作,如果它是一个目录,则应将该目录的名称附加到其中包含的所有文件的末尾。

所以,假设目录Videos有以下文件:

-rwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 video1.mpeg
-rwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Video2.wmv

我需要将它们重命名如下:

-rwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 video1_Videos.mpeg
-rwxr-xr-x  2 matteorr matteorr  4096 Oct 18 18:43 Video2_Videos.wmv

4
如果你关心脚本的健壮性(对奇怪的文件名有健壮性),更好的方法可能是使用find而不是ls:http://mywiki.wooledge.org/ParsingLs - Jakub Kotowski
你想做什么? - ctrl-alt-delor
你打算对每个文件做什么? - Behe
你能举个例子吗? - ctrl-alt-delor
@richard - 当然可以!但你能在你的回答中帮我弄清楚吗?谢谢。 - Matteo
显示剩余10条评论
4个回答

7
更好的方法是使用bash globbing。 只需列出所有文件。
echo *

或者对它们进行一些操作。
for file in *; do
  echo "$file" # or do something else
done

或者使用bash 4+递归执行:

shopt -s globstar
for file in **/*; do
  echo "$file" # or do something else
done 

更新以获取目录名称并将其附加到其中的所有文件

请用echo替换mv以测试其功能。此外,注意${file##*.}假定扩展名是最后一个句点之后的所有内容,因此如果您在目录on中有一个名为file.tar.gz的文件,则以下命令会将其转换为file.tar_on.gz。据我所知,目前没有简单的方法来解决这个问题,但是如果您想要,可以跳过具有多个.的文件。

#!/bin/bash
d="/some/dir/to/do/this/on"
name=${d##*/} #name=on
for file in "$d"/*; do
  extension=${file##*.} 
  filename=${file%.*}
  filename=${filename##*/}
  [[ -f $file ]] && mv "$file" "$d/${filename}_${name}.$extension"
done

e.g.

> ls /some/dir/to/do/this/on
video1.mpeg  Video2.wmv
> ./abovescript
> ls /some/dir/to/do/this/on
video1_on.mpeg  Video2_on.wmv

说明

在bash中,您可以这样做:

  • ${parameter#word} 移除最短匹配前缀
  • ${parameter##word} 移除最长匹配前缀
  • ${parameter%word} 移除最短匹配后缀
  • ${parameter%%word} 移除最长匹配后缀

要删除最后一个句点及其之前的所有内容(*),我执行了以下操作:

 extension=${file##*.} 

为了移除包括最后一个句号在内的所有内容,我进行如下操作(请从右往左思考最短匹配,例如,* 会从右往左查找任何非句号文本,然后发现句号时将其整个部分移除)。
filename=${file%.*}

为了删除最后一个斜杠(包括它本身)及其前面的所有内容,我使用了以下代码。
filename=${filename##*/}

一些其他注意事项:

  • "$d/${filename}_${name}.$extension" 变量可以带有 _,因此我在这里切换了几个变量的语法以使其正常工作。
  • "$d"/* 展开为 "$d" 目录中任何类型(普通、目录、符号链接等)的每个文件。

谢谢你的精彩回答!我现在正在尝试它。 - Matteo
@Matteo 没问题。如果您有一个带有多个点扩展名的文件,请参阅我的脚本上面的评论。处理这种情况会变得非常混乱,可能需要使用已知多点扩展名的数组并对其进行测试或类似操作。 - Reinstate Monica Please
脚本运行良好,能否请您再多注释一下?我对所有的 #,%,。/,* 字符有些困惑... - Matteo
@Matteo 更新了解释。另外,我在原始脚本中忘记了一个相当重要的部分。如果你只想移动普通文件,你应该做类似于[[ -f $file ]] && mv这样的事情,因为*会扩展到包括子目录在内的所有文件。 - Reinstate Monica Please
+1 非常酷的Bash技巧。我每天都在这里学到新东西。今天,这个答案是“新鲜事物”。谢谢!注意 - 我修正了一些拼写错误。请确保我没有意外改变你的答案的含义。 - Floris
@Floris 感谢您的修复。 - Reinstate Monica Please

4

有什么问题

ls > myfile.txt

这只会列出文件名(仅此而已),并将它们发送到myfile.txt
如果您想使用awk方法,只需执行以下操作。
ls -l | awk '{print $9}'
awk 的默认操作是按空格分割字段 - 这将打印每行的第9个字段...
如果您想对文件名进行其他操作,可以扩展您的 awk 脚本。例如,可以使用这些文件名创建一个数组。
ls -l | awk '{a[NR]=$9}'

您可以在进一步的处理中使用这个数组(称为a)。如果处理需要除awk之外的其他内容(从评论中可以看出),最好使用类似于以下内容的东西:

#!/bin/bash
for f in $1"/"*
do
if [ -d "$f" ] ; then
  ./listdir $f
else
  echo $f
fi
done

将此代码保存为 listdir,并放置在当前目录中即可使用。

./listdir .

该命令将列出整个目录,并根据需要(带有完整的相对路径)递归下降。

如果您希望此命令“随时可用”(毕竟它是非常有用的命令),则应将其放在您的路径中(并执行“rehash”命令,以便它被“识别”);然后您不需要在命令开头使用./


myfile.txt 是从哪里来的?问题中没有提到。 - ctrl-alt-delor
谢谢,但我该如何逐个迭代列表中的项? - Matteo
@richard - 问题中说“我需要存储”... 我决定将其存储在一个文件中。 - Floris
@matteo - 你想如何迭代?我给出的awk命令为每次迭代提供一个值。你可以在awk脚本中放置其他表达式。 - Floris
对不起,你是正确的。 - ctrl-alt-delor
对于目录中包含的所有文件,如果是文件,则不做任何操作;如果是目录,则应将该目录的名称附加到其包含的所有文件。 - Matteo

3
很好的问题!很高兴你能问出来。解析ls的输出通常不是正确的做法。有无数种方法可以处理文件列表。这取决于你想要对它们做什么。
以下是一些你可以做的事情的示例。我使用touch作为示例命令。用你想要执行的任何命令替换它即可。
  1. To run a command over multiple files, often you can simply pass all the files on the command-line.

    touch /var/myapp/*
    
  2. To loop over the files in the current directory:

    for file in *; do
        touch "$file"
    done
    
  3. To loop over files in another directory:

    for file in /some/dir/*; do
        touch "$file"
    done
    
  4. To rename files named *.txt to '*.bak', both here and in sub-directories:

    find . -name '*.txt' -exec mv {} {}.bak \;
    
  5. To delete JPEGs in Bob's home directory (damn you Bob and your wandering eyes):

    find ~bob/ -name '*.jpg' -delete
    
  6. To loop over files recursively and do complicated things to them:

    find /dir/to/search -print0 | while read -d $'\0' file; do
        echo "$file"
        touch "$file"
    
        if [[ -L $file ]]; then
            # $file is a symlink, do something special
        fi
    done
    

也许需要注意的是,find -print0 是一个非便携式的 GNU find 扩展。 - tripleee

1

ls -l | awk '{split($0,array," ")} {print array[9]}'

或者

ls -l | awk '{print $9}'

但是为什么不直接使用ls命令呢?


谢谢,但我该如何逐个迭代列表项? - Matteo
1
如果重定向到终端以外的地方,则“ls”将输出显示在一列中。输入ls | cat进行检查。如果你真的不相信自动终端检测会起作用,可以使用 ls -1(这是数字1,不是小写字母L)。 - abligh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接