在Shell中获取文件大小(以字节为单位)的便携式方法

145
在Linux上,我使用stat --format="%s" FILE命令来获取文件大小,但我所能访问的Solaris机器没有stat命令。那我应该使用什么命令呢?我的脚本使用Bash编写,无法在系统中安装新软件。我已经考虑过以下选项:
perl -e '@x=stat(shift);print $x[7]' FILE

甚至是:

ls -nl FILE | awk '{print $5}'

但是这两种方法都不太合理 —— 运行 Perl 来获取文件大小?或者运行两个程序来完成同样的任务?


1
一个Bash脚本就是软件,如果你可以将它放在系统上,你就可以安装软件。 - just somebody
5
从技术上讲 - 是正确的。我是指我没有root权限,不能安装新的软件包。当然,在用户目录中安装是可行的。但是,当我需要创建一个可移植的脚本,并在多台计算机上安装时,安装新的附加软件包就变得棘手了。 - user80168
16个回答

241

wc -c < filename(代表单词计数,-c打印字节计数)是一个可移植的POSIX解决方案。唯一可能不同于平台统一的是输出格式,因为某些空格可能会被添加(这是Solaris的情况)。

不要省略输入重定向符号。当文件作为参数传递时,文件名将在字节计数后被打印。

我很担心它不能用于二进制文件,但它在Linux和Solaris上都能正常工作。你可以用 wc -c < /usr/bin/wc 尝试一下。此外,POSIX实用程序可以保证处理二进制文件,除非明确地指定了其他内容。


71
如果您不想显示文件名,只需使用wc -c < file命令即可。 - caf
39
如果我没记错,管道中的 wc 命令必须要读取整个数据流才能统计字节数。而 ls/awk 等解决方案则使用了系统调用来获取文件大小,这应该是线性时间复杂度(与 O(文件大小) 相比)。 - jmtd
7
我不会使用 wc -c 命令;虽然它看起来更整洁,但使用 ls + awk 命令可以更快速/节省资源。另外,我想指出的是,你实际上还需要对 wc 命令的结果进行后处理,因为在某些系统上它会在结果前有空白字符,你可能需要在进行比较之前去掉它们。 - Haravikk
4
wc -c 命令很好用,但如果你没有文件的读取权限,它将无法工作。 - Silas
4
statls 工具只是执行 lstat 系统调用并获取文件长度,而不读取文件。因此,它们不需要读取权限,并且它们的性能不取决于文件的长度。相比之下,wc 实际上会打开并通常读取文件,在处理大文件时性能较差。但是 GNU Coreutils wc 在仅需要正常文件的字节数时进行了优化:它使用 fstatlseek 系统调用来获取计数。请参见源代码中包含 (dd ibs=99k skip=1 count=0; ./wc -c) < /etc/group 注释。 - Palec
显示剩余4条评论

49

我最终写了一个很小的程序来显示文件大小。更多信息在bfsize - 以字节打印文件大小 (仅此而已)中。

在我看来,使用常见的Linux工具有两种最干净的方法:

stat -c %s /usr/bin/stat

50000


wc -c < /usr/bin/wc

36912

但我不想输入参数或管道输出只是为了获得文件大小,因此我在使用自己的bfsize


2
问题描述的第一行指出stat不是一个选项,而wc -c是现在已经超过一年的最佳答案,所以我不确定这个答案的意义是什么。 - user80168
28
关键点在于像我这样通过谷歌搜索发现了这个问题的人,而对于他们来说,“stat” 是一个可选项。 - yo'
4
我正在开发一个嵌入式系统,使用wc -c命令在处理一个10 MB的文件时需要4090毫秒,而stat -c %s命令只需"0"毫秒。因此,我认为即使这些解决方案并不能完全回答提出的问题,拥有备选方案也是有帮助的。请您注意,我的翻译不改变原意,且尽量简洁易懂。 - Robert Calhoun
4
"stat -c" 在 MacOS 和 Linux 上不具备可移植性,且在这两个系统上所接受的参数也有所不同。对于大文件,使用 "wc -c" 会非常缓慢。 - Orwellophile
3
"stat" 也不是可移植的。 stat -c %s /usr/bin/stat stat:非法选项--c 用法:stat [-FlLnqrsx] [-f格式] [-t时间格式] [文件…] - user1985657
显示剩余3条评论

39

尽管du通常打印磁盘使用情况而不是实际数据大小,但GNU Core Utilitiesdu可以以字节形式打印文件的“表面大小”:

du -b FILE

但它无法在 BSDSolarismacOS 等系统下运行。


6
在 macOS X 上,运行 brew install coreutils 和 gdu -b 命令可以实现相同的效果。 - Jose Alban
3
我更喜欢使用 du 命令而不是 wc 命令,因为 wc 命令需要读取整个文件才能给出结果,而 du 命令则可以立即给出结果。 - CousinCocaine
4
POSIX在《du》的解释中提到了du -b,但是上下文完全不同。详见du合理性 - Palec
这里只使用了 lstat 调用,因此其性能不取决于文件大小。比 stat -c '%s' 更短,但对于文件夹的处理方式不同(打印其中每个文件的大小)。 - Palec
1
FreeBSD的du命令可以使用du -A -B1来接近实现,但它仍然以1024B块的倍数打印结果。无法使其打印字节计数。即使在环境中设置BLOCKSIZE=1也没有帮助,因为仍然使用512B块。 - Palec
它在哪里工作?只能在Linux上吗? - Peter Mortensen

13

最终我决定使用ls和Bash数组扩展:

TEMP=( $( ls -ln FILE ) )
SIZE=${TEMP[4]}

虽然不是很好,但至少它只做了一个fork+execve,并且没有依赖于第二种编程语言(Perl, Ruby, Python或其他)。


只是顺便提一下,'-ln'中的'l'是不必要的;'-n'与'-ln'完全相同。 - barryred
1
不是的。只需要比较输出即可。 - user80168
1
人们可能会猜测可移植的 ls -ln FILE | { read _ _ _ _ size _ && echo "$size"; } 不需要在管道的第二步骤中进行分叉,因为它只使用了内置命令,但是在 Linux 上,Bash 4.2.37 会分叉两次(尽管仍然只有一个 execve)。 - Palec
read _ _ _ _ size _ <<<"$(exec ls -ln /usr/bin/wc)" && echo "$size" 可以使用单个fork和单个exec,但它使用一个临时文件来进行here-string。通过将here-string替换为POSX兼容的here-document,可以使其可移植。顺便提一下,在子shell中使用exec。如果没有这个,Bash会为子shell执行一个fork,然后为内部运行的命令执行另一个fork。这也是你在这个答案中提供的代码中的情况。 - Palec
分叉不应该是一个问题;大多数人不会在只包含一个命令的子shell中编写exec。临时文件更糟,但这只是shell而已。试图如此严格地限制分叉数量绝对是过早优化,即万恶之源。可移植性、可读性和代码长度应该胜过像这样的小性能提升。如果需要优化一个工作中的shell脚本,你应该考虑重写它(或至少是其关键部分)到C语言。 - Palec
1
在存在 -n 的情况下,-l 是多余的。引用 POSIX ls manpage:*-n: 打开 -l 选项,但在写入文件所有者或组时,分别写入文件的数字 UID 或 GID 而不是用户或组名。禁用 -C-m-x 选项。* - Palec

10

Solaris根本没有stat实用程序。 - Palec
Busybox不支持该结构:stat:无法识别选项:% BusyBox v1.32.1()多调用二进制文件。 - Jason Martin

9

处理 ls -n 输出时,作为不太便携的 shell 数组的替代方案,您可以使用位置参数,它们形成了唯一的数组并且是标准 Shell 中唯一的本地变量。将位置参数的覆盖包装在函数中,以保留脚本或功能的原始参数。

getsize() { set -- $(ls -dn "$1") && echo $5; }
getsize FILE

此命令将根据当前IFS环境变量设置分割ln -dn的输出,将其分配给位置参数并回显第五个参数。 -d 确保目录被正确处理,而-n 确保不需要解析用户和组名称,与-l 不同。此外,包含空格的用户和组名称理论上可能会破坏预期的行结构。虽然它们通常被禁止,但这种可能性仍会使程序员停下来思考。请注意保留HTML标记。

8
跨平台最快解决方案(只使用单个fork()进行ls操作,不尝试计算实际字符数,也不生成不需要的awk、perl等)。在Mac OS X和Linux上测试过。可能需要对Solaris进行轻微修改:
__ln=( $( ls -Lon "$1" ) )
__size=${__ln[3]}
echo "Size is: $__size bytes"

如果需要的话,简化ls的参数,并调整${__ln[3]}中的偏移量。 注意:它将遵循符号链接。

1
或者将其放入一个shell脚本中:ls -Lon“$1”| awk '{ print $4 }' - Luciano
1
@Luciano,我认为你完全没有理解“不要分叉”的重点,而是用bash来串联许多Unix命令,这样效率低下。 - Orwellophile

5
如果您使用GNU文件实用程序中的find命令:
size=$( find . -maxdepth 1 -type f -name filename -printf '%s' )

不幸的是,其他find实现通常不支持-maxdepth-printf。例如,Solaris和macOS的find就是这种情况。


FYI maxdepth 不是必需的。它可以重写为 size=$(test -f filename && find filename -printf '%s') - Palec
@Palec:-maxdepth 旨在防止 find 递归(因为 OP 需要替换的 stat 不是)。你的 find 命令缺少 -name,而且 test 命令也不必要。 - Dennis Williamson
@DennisWilliamson find 会递归地搜索其参数以查找符合给定条件的文件。如果参数不是目录,则递归过程非常简单。因此,我首先测试 filename 是否真的是一个现有的普通文件,然后使用 find 打印其大小,因为此时 find 已经没有递归的对象了。 - Palec
1
如果文件在当前目录中,find . -maxdepth 1 -type f -name filename -printf '%s' 只能工作,并且它仍然可能检查目录中的每个文件,这可能会很慢。最好使用(甚至更短!)find filename -maxdepth 1 -type f -printf '%s' - Palec

3
你可以使用find命令获取一些文件集合(这里提取了临时文件)。然后您可以使用du命令以人类可读的形式获取每个文件的文件大小,使用-h开关。

find $HOME -type f -name "*~" -exec du -h {} \;

输出:
4.0K    /home/turing/Desktop/JavaExmp/TwoButtons.java~
4.0K    /home/turing/Desktop/JavaExmp/MyDrawPanel.java~
4.0K    /home/turing/Desktop/JavaExmp/Instream.java~
4.0K    /home/turing/Desktop/JavaExmp/RandomDemo.java~
4.0K    /home/turing/Desktop/JavaExmp/Buff.java~
4.0K    /home/turing/Desktop/JavaExmp/SimpleGui2.java~

2
你的第一个Perl示例看起来还不错。
正是因为这样的原因,我从编写Shell脚本(使用Bash、sh等)转向使用Perl编写除最简单的脚本以外的所有脚本。我发现我需要针对特定需求启动Perl,随着这种情况越来越多,我意识到使用Perl编写脚本可能是实现我想要的目标更强大(在语言和通过CPAN提供的广泛库的方面)和更高效的方法。
请注意,其他Shell脚本语言(例如PythonRuby)无疑也会有类似的功能,你可能需要评估这些语言是否适合你的目的。我只讨论Perl,因为那是我使用并熟悉的语言。

1
嗯,我自己写了很多Perl代码,但有时候工具是由别人选择的,而不是我选择的 :) - user80168

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