Maintaining Multi-Line „stat“ Formats using Bourne Again Shell

The stat command from GNU core utilities features not only a --format FORMAT option but also a --printf FORMAT one, the difference being that the latter allows for backslash escapes such as \n.

This allows for custom per-file report formats containing newlines, for example:

stat --printf 'Name: %n\nSize: %s Bytes\n' /etc/passwd

If the format string becomes more complex, the command line soon becomes unwieldy, such as:

stat --printf 'Name: %n\nOwner ID: %u\nSize: %s Bytes\nLast accessed: %x\n' /etc/passwd

Using a multi-line string, the format string can be broken up, but continuation and indentation rules make it difficult to maintain a neat organization to the format text. An example follows (there is also a variant that uses line continuation, but it suffers from similar problems):

stat --printf \
'Name: %n
Owner ID: %u
Size: %s Bytes
Last accessed: %x
' /etc/passwd

Let’s write down the lines of the report format for stat as a Bourne Again Shell array, one element per line:

STAT_FMT=(
    "Name: %n"
    "Owner ID: %u"
    "Size: %s Bytes"
    "Last accessed: %x"
)

Now we need an expression that expands this data structure into a single argument for stat, interleaving the elements with line-breaks.

The following attempt uses the special variable IFS and the single-word array expansion:

# Problematic: Only the first character of IFS actually applies. 
IFS='\n' ; stat --printf "${STAT_FMT[*]}" /etc/passwd

This leads to errors like this:

stat: warning: unrecognized escape '\O'

IFS needs to be a single character, and the escape sequence \n is two characters long; assigning \n to IFS will result in only the first character, \ being interleaved.

This is one of the (rare) use cases for Bourne Again Shell’s ’single quote expansion‘ which takes the form $'\n' – it expands to a single-quoted string where backspace escape sequences have been expanded to their literal values – in the case of \n this would be the ASCII Line Feed (0xa), a single character.

The following works:

IFS=$'\n' ; stat --printf "${STAT_FMT[*]}" /etc/passwd

A more complete code example follows. Note that in this example the stat call with the modified IFS variable has been put into a function, with IFS being (re-)declared a local variable so that the IFS modification would not interfere with later commands.

STAT_FMT=(
    "Information about file \"%n\":"
    "- Owner ID: %u"
    "- Size: %s Bytes"
    "- Last accessed: %x"
    "- Last modified: %y"
    "- Last changed: %z"
)

stat_fmt() {
    local IFS=$'\n'
    stat --printf "${STAT_FMT[*]}\n" "$@"
}

stat_fmt "$@"