bashmacosawkstatbsd

How to generate a NUL-delimited stream of timestamped filenames with BSD `stat` command


Let's suppose that you need to generate a NUL-delimited stream of timestamped filenames.

On Linux & Solaris I can do it with:

stat --printf '%.9Y %n\0' -- *

On BSD, I can get the same info, but delimited by newlines, with:

stat -f '%.9Fm %N' -- *

The man talks about a few escape sequences but the NUL byte doesn't seem supported:

If the % is immediately followed by one of n, t, %, or @, then a newline character, a tab character, a percent character, or the current file number is printed.

Is there a way to work around that? edit: (accurately and efficiently?)


Update:


Solution

  • 1. What you can do (accurate but slow):

    Fork a stat command for each input path:

    for p in "$@"
    do
        stat -nf '%.9Fm' -- "$p" &&
        printf '\t%s\0' "$p"
    done
    

    2. What you can do (accurate but twisted):

    In the input paths, replace each occurrence of (possibly overlapping) /././ with a single /./, make stat output /././\n at the end of each record, and use awk to substitute each /././\n by a NUL byte:

    #!/bin/bash
    shopt -s extglob
    
    stat -nf '%.9Fm%t%N/././%n' -- "${@//\/.\/+(.\/)//./}" |
    
    awk -F '/\\./\\./' '{
        if ( NF == 2 ) {
            printf "%s%c", record $1, 0
            record = ""
        } else
            record = record $1 "\n"
    }'
    

    N.B. If you wonder why I chose /././\n as record separator then take a look at Is it "safe" to replace each occurrence of (possibly overlapped) /./ with / in a path?


    3. What you should do (accurate & fast):

    You can use the following perl one‑liner on almost every UNIX/Linux:

    LANG=C perl -MTime::HiRes=stat -e '
        foreach (@ARGV) {
            my @st = stat($_);
            if ( @st > 0 ) {
                printf "%.9f\t%s\0", $st[9], $_;
            } else {
                printf STDERR "stat: %s: %s\n", $_, $!;
            }
        }
    ' -- "$@"
    

    note: for perl < 5.8.9, remove the -MTime::HiRes=stat from the command line.


    ASIDE: There's a bug in BSD's stat:

    When %N is at the end of the format string and the filename ends with a newline character, then its trailing newline might get stripped:

    For example:

    stat -f '%N' -- $'file1\n' file2
    
    file1
    file2
    

    For getting the output that one would expect from stat -f '%N' you can use the -n switch and add an explicit %n at the end of the format string:

    stat -nf '%N%n' -- $'file1\n' file2
    
    file1
    
    file2