How to Copy Files by Last Modified Date in Linux

bashcommand linelinuxsed

I'm trying to move a bunch of files from a directory to my phone in a specific order so it wouldn't mess up the order in my phone.

The furthest I've gotten is by using this command:

find . -printf "%T@ %p\n" | sort -n | sed 's@^.*/@@' | xargs -I{} cp "{}" '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RF8NA3063PJ/Internal storage/Music/music'

or this one

stat -c "%Y/%n" *.mp3 | sort -t/ -k1,1n |sed 's@^.*/@@' | xargs -I{} cp "{}" '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RF8NA3063PJ/Internal storage/Music/music'

The problem is that once the filename includes an apostrophe, I get the following error:
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option

I understand that when I'm using "find" command I need to add a -print0, but doing this

find . -print0 "%T@ %p\n" | sort -n | sed 's@^.*/@@' | xargs -I{} cp "{}" '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RF8NA3063PJ/Internal storage/Music/music'

Gives me an error: find: paths must precede expression: `%T@ %p\n'

Turning the command into this works but messes up the order

find * -print0 -printf "%T@ %p\n" | sort -n | sed 's@^.*/@@'

Any help?

Best Answer

In this answer I'm considering the following command of yours as our starting point:

find . -printf "%T@ %p\n" | sort -n | sed 's@^.*/@@' | xargs -I{} cp …

Then you write:

I understand that when I'm using "find" command I need to add a -print0

and then you try:

find . -print0 "%T@ %p\n" | …

or:

find * -print0 -printf "%T@ %p\n" | …

I think the main misunderstanding is in "I need to add a -print0". No, you don't add -print0. In cases (examples) where -print0 appears and seems to be added, it really replaces an implicit -print. The point is -print is the default action and in many cases you don't have to type it. Then, if you want to replace it with -print0, you append -print0. The existence of -print0 suppresses the default -print. It looks as if -print0 was added, but logically it replaced -print.

In your case the -printf … already suppresses the default -print. This means there is no -print in your command. If you add -print0 (like you did), you will have -printf … and -print0 and each one will print something.

You don't want to simply replace your -printf … with -print0, because for you the point of having -printf … instead of (explicit or implicit) -print is this %T you used. You want to modify your -printf …, so the new -printf … with respect to the old -printf … is exactly like -print0 with respect to -print.

What is the difference between -print0 and -print? The former uses one trailing null byte as the terminator, where the latter uses one trailing newline character. -print0 is equivalent to -printf '%p\0' and -print is equivalent to -printf '%p\n'

Your -printf "%T@ %p\n" exactly uses one trailing newline character, it's -print-like. To make it -print0-like and retain the %T, you just need to change \n into \0:

find . -printf '%T@ %p\0'

Then you need to make your sort, sed and xargs work with null-terminated lines. Hopefully if your find is rich enough to support -print0 and -printf then your other tools are also capable of handling null-terminated data. And I assume your cp supports -t. Effectively I assume GNU tools.

This is the command:

find . -iname '*.mp3' -type f -printf '%T@ %p\0' \
| sort -zn | sed -z 's@^[^ ]* @@' \
| xargs -r0 cp -t '/run/user/1000/gvfs/mtp:host=SAMSUNG_SAMSUNG_Android_RF8NA3063PJ/Internal storage/Music/music'

Note I changed the sed code. Your code removes everything up to the last / (.* is greedy!), so only the last component survives, the filename (except for . where there is no /, so everything survives!). If there are files matching -iname '*.mp3' -type f in subdirectories then passing only the last component will make cp fail. My code removes everything up to and including the first space, i.e. the fragment resulting from %T@ . Everything resulting from %p survives. This means that:

  • files in subdirectories can be copied,
  • but a name collision (if any) will make the later cp overwrite, so eventually some files may be missing in the target location;
  • pathname components starting with - are safe (i.e. we don't need double dash) because all pathnames start with . anyway.

I used -iname '*.mp3' because elsewhere you used *.mp3. I used -type f because its a sane thing to do (you certainly don't want to process paths to directories and pass them to cp).

I believe if there is a pathname that forms an invalid text in your locale then you need LC_ALL=C, for -iname '*.mp3' to work as expected. I'm not sure about sed. If you want to work with such pathnames, consider export LC_ALL=C beforehand.

Related Question