1

I am trying to make a small utility for myself on the command line - for a given current work directory I would like to find the most recently added file and mv it to a name given by argument

I have been trying to achieve this via: ls -tr | tail -n 1 | xargs -0 -J % mv % SQL_warning_2.png

...however bash reports:

mv: rename Screenshot 2020-06-22 at 17.53.23.png
 to SQL_warning_2.png: No such file or directory

I would guess that this means that xargs is making separate arguments via spaces in the filename: Screenshot 2020-06-22 at 17.53.23.png, but there is no -print0 flag for ls that I am aware of.

If some of my assumptions are incorrect please excuse me - I am very unfamiliar with xargs

3 Answers3

4

Since you seem to assume that your filenames don't contain newlines (you pick one out using tail), you could use just

ls -tr | tail -n 1 | xargs -I % mv -- % SQL_warning_2.png

The -I % will cause xargs to call the utility once per line read, with % replaced by the text of the line. In contrast, -J % splits the line on spaces:

$ echo 1 2 3 | xargs -J {} printf '"%s"\n' {}
"1"
"2"
"3"
$ echo 1 2 3 | xargs -I {} printf '"%s"\n' {}
"1 2 3"

You seem to want to rename the most recently modified file in the current directory. This could be done using with the zsh shell as follows:

mv -- *(.om[1]) SQL_warning_2.png

This would call mv with the most recently modified regular file's name as the first argument. It is the om[1] in the glob qualifier that sorts the list of names by the mtime timestamp (newest to oldest) and picks out the first one. The preceding dot selects only regular files (not directories etc.)

From bash:

zsh -c 'mv -- *(.om[1]) SQL_warning_2.png'
Kusalananda
  • 333,661
  • The other answers provided me a solution, but you provided me an understanding, thankyou – Scott Anderson Jun 24 '20 at 20:05
  • With -I %, leading blanks, ", ', backslash and with some implementations, bytes not forming part of valid characters would still be a problem. Empty input would also be a (minor) problem. xargs is really unusable unless you use the -0, -r, -d GNU extensions. – Stéphane Chazelas Jun 25 '20 at 06:08
2

I would do something like this:

mv -- "$(ls -t|head -n1)" new_filename

(here assuming the name of the newest file in the current directory doesn't contain newline characters).

0

Yes, with -0, xargs expects a NUL-delimited list on stdin. Here, you're feeding it the part of the name of the newest file after the last newline characters in it, followed by a newline character (that newline character is added by ls and not part of file name).

There is no NUL in that input, so xargs takes the whole input as one argument to pass to mv which does contain that newline character, so it would only work correctly if the name of newest file did contain one and only one newline character and it was the last character in the file name.

Here, you'd need to make sure ls outputs a NUL-delimited list instead of a newline-delimited one, but I'm not aware of any ls implementation that can.

Or you'd need to revert to the default xargs input format (without -0) where arguments are delimited by blanks (the list of which depends on the xargs implementation and possibly the locale) or newlines and where "..." and '...' and \ are used for escaping those and each other (in a different way from the same shell quoting operators). As some xargs implementations try to interpret their input as text but filenames can contain any byte values (other than NUL and /), you'll also need to do that processing in the C locale.

export LC_ALL=C
ls -td ./* | awk -v q="'" '
  {gsub(/"/, "\"\\\"\"")} # escape "
  NR == 1 {printf "\"%s", $0; next}
  /\// {exit} # a / means the start of the second file
  {printf "\"\\\n\"%s", $0} # escape newlines
  END {if (NR) print "\""}' |
  xargs -J % mv % newname

As you can see, using xargs reliably is a total pain. Some xargs implementations also have a very low limit on the size of argument or input lines. Note that -J is a non-portable BSD extension.

Dealing with arbitrary file names using line-based utilities like ls is also very hard.

Best would be to use zsh which can sort the list of files by modification times by itself as @Kusalananda has shown:

mv -- *(.om[1]) newname

In bash, you could also do:

IFS= read -rd / newest < <(ls -td ./*) && newest=${newest%.}
newest=${newest%?} # strip newline
[ -n "$newest" ] && mv "$newest" newname

(would also work in ksh93 or zsh). Like in the previous xargs approach, we're using ./* to be able to tell on which line the second file in the list starts.