18 of 19 added 49 characters in body

Depends what you mean by size.

size=$(wc -c < "$file")

will give you the number of bytes that can be read from the file. IOW, it's the size of the content of the file. It will however read the content of the file (except if the file is a regular file or symlink to regular file in most wc implementations as an optimisation). That may have side effects. For instance, for a named pipe, what has been read can no longer be read again and for things like /dev/zero or /dev/random which are of infinite size, it's going to take a while. That also means you need read permission to the file.

That's standard and portable, however note that some wc implementations may include leading blanks in that output. One way to get rid of them is to use:

size=$(($(wc -c < "$file")))

or to avoid an error about an empty arithmetic expression in dash or yash when wc produces no output (like when the file can't be opened):

size=$(($(wc -c < "$file") +0))

ksh93 has wc builtin (provided you enable it, you can also invoke it as command /opt/ast/bin/wc) which makes it the most efficient for regular files in that shell.

Various systems have a command called stat that's an interface to the stat() or lstat() system calls.

Those report information found in the inode. One of that information is the st_size attribute. For regular files, that's the size of the content (how much data could be read from it in the absence of error (that's what most wc -c implementations use in their optimisation)). For symlinks, that's the size in bytes of the target path. For named pipes, depending on the system, it's either 0 or the number of bytes currently in the pipe buffer. Same for block devices where depending on the system, you get 0 or the size in bytes of the underlying storage.

You don't need read permission to the file to get that information, only search permission to the directory it is linked to.

By chronological order, there is:

  • IRIX stat (90's):

    stat -qLs -- "$file"

    returns the st_size attribute of $file (lstat()) or:

    stat -s -- "$file"

    same except when $file is a symlink in which case it's the st_size of the file after symlink resolution.

  • zsh stat builtin (now also known as zstat) in the zsh/stat module (loaded with zmodload zsh/stat) (1997):

    stat -L +size -- $file # st_size of file
    stat +size -- $file    # after symlink resolution

    or to store in a variable:

    stat -L -A size +size -- $file

    obviously, that's the most efficient in that shell.

  • GNU stat (2001); also in BusyBox stat since 2005 (copied from GNU stat):

    stat -c %s -- "$file"  # st_size of file
    stat -Lc %s -- "$file" # after symlink resolution

    (note the meaning of -L is reversed compared to IRIX or zsh stat.

  • BSDs stat (2002):

    stat -f %z -- "$file"  # st_size of file
    stat -Lf %z -- "$file" # after symlink resolution

Or you can use the stat()/lstat() function of some scripting language like perl:

perl -le 'print((lstat shift)[7])' -- "$file"

AIX also has an istat command which will dump all the stat() (not lstat(), so won't work on symlinks) information and which you could post-process with, for example:

LC_ALL=C istat "$file" | awk 'NR == 4 {print $5}'

(thanks @JeffSchaller for the help figuring out the details).

In tcsh:

@ size = -Z $file:q

(size after symlink resolution)

Long before GNU introduced its stat command, the same could be achieved with GNU find command with its -printf predicate (already in 1991):

find -- "$file" -prune -printf '%s\n'    # st_size of file
find -L -- "$file" -prune -printf '%s\n' # after symlink resolution

One issue though is that doesn't work if $file starts with - or is a find predicate (like !, (...).

The standard command to get the stat()/lstat() information is ls.

POSIXly, you can do:

LC_ALL=C ls -dn -- "$file" | awk '{print $5; exit}'

and add -L for the same after symlink resolution. That doesn't work for device files though where the 5th field is the device major number instead of the size.

For block devices, systems where stat() returns 0 for st_size, usually have other APIs to report the size of the block device. For instance, Linux has the BLKGETSIZE64 ioctl(), and most Linux distributions now ship with a blockdev command that can make use of it:

blockdev --getsize64 -- "$device_file"

However, you need read permission to the device file for that. It's usually possible to derive the size by other means. For instance (still on Linux):

lsblk -bdno size -- "$device_file"

Should work except for empty devices.

An approach that works for all seekable files (so includes regular files, most block devices and some character devices) is to open the file and seek to the end:

  • With zsh (after loading the zsh/system module):

    {sysseek -w end 0 && size=$((systell(0)))} < $file
  • With ksh93:

    < "$file" <#((size=EOF))


    { size=$(<#((EOF))); } < "$file"
  • with perl:

    perl -le 'seek STDIN, 0, 2 or die "seek: $!"; print tell STDIN' < "$file"

For named pipes, we've seen that some systems (AIX, Solaris, HP/UX at least) make the amount of data in the pipe buffer available in stat()'s st_size. Some (like Linux or FreeBSD) don't.

On Linux at least, you can use the FIONREAD ioctl() after having open the pipe (in read+write mode to avoid it hanging):

fuser -s -- "$fifo_file" && 
  perl -le 'require "sys/ioctl.ph";
            ioctl(STDIN, &FIONREAD, $n) or die$!;
            print unpack "L", $n' <> "$fifo_file"

However note that while it doesn't read the content of the pipe, the mere opening of the named pipe here can still have side effects. We're using fuser to check first that some process already has the pipe open to alleviate that but that's not foolproof as fuser may not be able to check all processes.

Now, so far we've only been considering the size of the primary data associated with the files. That doesn't take into account the size of the metadata and all the supporting infrastructure needed to store that file.

Another inode attribute returned by stat() is st_blocks. That's the number of 512 byte blocks that is used to store the file's data (and sometimes some of its metadata like the extended attributes on ext4 filesystems on Linux). That doesn't include the inode itself, or the entries in the directories the file is linked to.

Size and disk usage are not necessarily tightly related as compression, sparseness (sometimes some metadata), extra infrastructure like indirect blocks in some filesystems have an influence on the latter.

That's typically what du uses to report disk usage. Most of the commands listed above will be able to get you that information.

  • POSIXLY_CORRECT=1 ls -sd -- "$file" | awk '{print $1; exit}'
  • POSIXLY_CORRECT=1 du -s -- "$file" (not for directories where that would include the disk usage of the files within).
  • GNU find -- "$file" -printf '%b\n'
  • zstat -L +block -- $file
  • GNU stat -c %b -- "$file"
  • BSD stat -f %b -- "$file"
  • perl -le 'print((lstat shift)[12])' -- "$file"