22

find command can output names of files as a null-delimited strings (if -print0 is provided), and xargs can consume them with -0 option turned on. But in between, it's hard to manipulate that collection of files - sort command has -z switch, that makes it possible to sort those files, but head and tail don't have them.

How can I do head and tail on those null-delimited inputs in a convenient way? (I can always create a short & slow ruby script, but I hope that there could be a better way)

2 Answers 2

25

GNU head and tail since coreutils version 8.25 have a -z option for that.

With older versions or for non-GNU systems, you can try and swap \0 and \n:

find ... -print0 |
  tr '\0\n' '\n\0' |
  head |
  tr '\0\n' '\n\0'

Note that some head implementations can't cope with NUL characters (and they're not required to by POSIX), but where find supports -print0, head and text utilities generally support NUL characters.

You can also use a function to wrap any command between the two trs:

nul_terminated() {
  tr '\0\n' '\n\0' | "$@" | tr '\0\n' '\n\0'
}

find ... -print0 | nul_terminated tail -n 12 | xargs -r0 ...

Keep in mind that under nul_terminated, a \0 means a newline character. So for instance, to replace \n with _:

find . -depth -name $'*\n*' -print0 | nul_terminated sed '
  p;h;s,.*/,,;s/\x0/_/g;H;g;s,[^/]*\n,,' | xargs -r0n2 mv

(\x0 being also a GNU extension).

If you need to run more than one filtering command, you can do:

find ... -print0 |
  nul_terminated cmd1 |
  nul_terminated cmd2 | xargs -r0 ...

But that means running a few redundant tr commands. Alternatively, you can run:

find ... -print0 | nul_terminated eval 'cmd1 | cmd2' | xargs -r0 ...
2
  • 3
    Doesn't this defeat the primary reason¹ for using \x0 instead of \n to delimit the values? (¹ so you can cope with values that might contain \n)
    – Thedward
    Commented Nov 12, 2014 at 22:02
  • @Thedward, no, on the contrary -print0 | tr '\n\0' '\0\n' has lines representing the file paths where newline characters in them have been converted to \0. So, if you take the first line with head -n 1 and convert the \0s back to newlines with tr '\0\n' '\n\0' again, you've got the first file path NUL-delimited with its embedded newline characters. Commented Nov 12, 2014 at 22:08
0

On an older system of mine, grep supported null delimited input, so I did this as a replacement for head --zero-terminated -n 3 to obtain the 3 newest files:

find . -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zrn | grep -zm 3 ""

A combination of sort and grep will then allow a replacement for tail to obtain the 3rd newest file:

find . -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zrn | grep -zm 3 "" | sort -zn | grep -zm 1 ""

Filename only:

find . -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zrn | grep -zm 3 "" | sort -zn | grep -zPom 1 "(?s)/.*"

Sadly -o (only-matching) disables the zero delimited output (bug!?), which was enabled through -z and returns the filename with an ending new line, which disallows further piping

find . -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zrn | grep -zm 3 "" | sort -zn | grep -zPom 1 "(?s)/.*" | xargs -0 ls -l
ls: cannot access /te
st
: No such file or directory

This can be solved by using read:

find . -mindepth 1 -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zrn | grep -zm 3 "" | sort -zn | { IFS=" " read -rd '' mtime file && echo -en "$file\0"; } | xargs -0 -n1 ls -l
-rw-r--r-- 1 root root 0 Apr 24 17:46 ./te?st

(ls -t returns a question mark for the new line character in the filename as expected)

Or another example how to obtain the 100 oldest files in the current dir:

find /tmp/test -mindepth 1 -maxdepth 1 -type f -printf "%T@ %p\0" | sort -zn | grep -zm 10 "" | { while IFS=" " read -rd '' mtime file; do echo -en "$file\0"; done; } | xargs -0 -n1 ls -bl
-rw-r--r-- 1 root root 0 Mar 23 14:32 /tmp/test/foo\ baz.txt
-rw-r--r-- 1 root root 0 Apr 26 10:43 /tmp/test/foo\nbar\ baz.txt
...
4
  • 1
    -printf '%T@/%p\0' | sort -zrn | IFS=/ read -rd '' mtime file would make more sense. By using echo -e, you're defeating the purpose of using NUL-delimited records. Use printf '%s\0' "$file". Using a shell loop is going to be less efficient than using the couple of trs that can be used to transform from/to a format that older GNU cut/sed/head... can handle. Also note that GNU sed has been supporting -z for a very long time. Commented Apr 26, 2023 at 10:04
  • @StéphaneChazelas I tried find /tmp -type f -printf "%T@/%p\0" | sort -zrn | IFS=/ read -rd '' mtime file && printf '%s\0' "$file", but it returns nothing.
    – mgutt
    Commented Apr 26, 2023 at 10:34
  • It works with find /tmp/test -type f -printf "%T@/%p\0" | sort -zrn | { IFS=/ read -rd '' mtime file && printf '%s\0' "$file"; }, but finally I don't really see the problem by using echo -en "$file\0"?!
    – mgutt
    Commented Apr 26, 2023 at 10:45
  • 1
    echo -e expands the \x sequences, so would break on files with backslashes in their paths. Commented Apr 26, 2023 at 11:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .