user1076412
user1076412

Reputation: 317

xargs and find, rm complaining about \n (newline) in filename

I am trying to delete the oldest file in a tree with a script in Debian.

find /home/backups -type f \( -name \*.tgz -o -name \*.gz \) -print0 | xargs -0 ls -t | tail -1 | xargs -0 rm

But I am getting an error:

rm: cannot remove `/home/backups/tree/structure/file.2011-12-08_03-01-01.sql.gz\n': No such file or directory

Any ideas what I am doing wrong (or is there an easier/better way?), I have tried to RTFM, but am lost.

Upvotes: 9

Views: 15547

Answers (7)

Joe
Joe

Reputation: 1

find /home/backups -type f \( -name \*.tgz -o -name \*.gz \) -print0 | xargs -0 stat --format '%010Y:%n' | sort -n | head -n 1 | cut -d: -f2- | xargs -d '\n' rm 

from: Sort a file list by Date in Linux (Including Subdirectories)

Upvotes: 0

Jens
Jens

Reputation: 72697

The ls appends a newline and the last xargs -0 says the newline is part of the file name. Run the last xargs with -d '\n' instead of -0.

BTW, due to the way xargs works, your whole pipe is a bug waiting to happen. Consider a really long file name list produced by the find, so that the xargs -0 ls runs ls multiple times with subsets of the filenames. Only the oldest of the last ls invocation will make it past the tail -1. If the oldest file is actually, say, the very first filename output by find, you are deleting a younger file.

Upvotes: 16

Ken
Ken

Reputation: 78902

You can also use find to print out the modification time, sort, cut and xargs at will:

find /home/backups -printf "%T@ %p\n" | sort -n | head -1 | cut -d" " -f2- | xargs ls -al

Upvotes: 2

sorpigal
sorpigal

Reputation: 26096

Any solution involving ls is absolutely wrong.

The correct way to do this is to use find to fetch the set of files, sort to order them chronologically, filter out all but the first, then rm to delete. @Ken had this mostly right, missing only a few details.

find /home/backups -type f \( -name \*.tgz -o -name \*.gz \) -printf '%T@ %p\0' |\
    sort -z -n | \
    { IFS= read -d '' file ; [ -n "$file" ] && echo rm -f "$(cut -d' ' -f2- <<<"$file")" ; }

Remove the echo above to actually perform the deletion.

The above code works even for files which have spaces, newlines or other unusual values in the file names. It will also do nothing harmful when there are no results.

If you don't care about breaking on newlines in filenames this gets a bit easier

find /home/backups -type f \( -name \*.tgz -o -name \*.gz \) -printf '%T@ %p\n' |\
    sort -n |\
    head -n 1 |\
    cut -d' ' -f2- |\
    xargs echo rm

The difference is that we can rely on head and can use cut on a pipe instead of doing anything crazy.

Upvotes: 3

sehe
sehe

Reputation: 393467

Edit I missed the point of ls -t there.

Might I suggest doing it much simpler, e.g.

find /home/backups \
    -type f -iregex '.*\.t?gz$' \
    -mtime +60 -exec rm {} \;

which will delete any matching file older than a specific age (60 days, in the example)


You used tail but haven't told it to look for null-delimiters.

Regardless, here is a util that you could use to return the last 0-delimited element:

#include <string>
#include <iostream>
#include <cstdio>

int main(int argc, const char *argv[])
{
    std::cin.unsetf(std::ios::skipws);
    if (!  (freopen(NULL, "wb", stdout) && freopen(NULL, "rb", stdin) ))
    {
        perror("Cannot open stdout/in in binary mode");
        return 255;
    }

    std::string previous, element;
    while (std::getline(std::cin, element, '\0'))
    {
        previous = element; 
        // if you have c++0x support, use this _instead_ for performance:
        previous = std::move(element);
    }

    std::cout << previous << '\0' << std::flush;
}

Use it as

find /home/backups -type f \( -name \*.tgz -o -name \*.gz \) -print0 | ./mytail | xargs -0 rm 

Upvotes: 0

fge
fge

Reputation: 121780

ls -tr $(find /home/backups -name '*.gz' -o -name '*.tgz')|head -1|xargs rm -f

Upvotes: 0

thiton
thiton

Reputation: 36059

ls emits newlines as separators, so you need to replace the second xargs -0 with xargs -d '\n'. Breaks, though, if the oldest file has a newline in its name.

Upvotes: 3

Related Questions