Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Ugh! I just deleted a bunch of files that I wasn't suppose to delete. But I can't understand how. Here's my script:
#!/usr/local/bin/perl use strict; use File::Find; my $mail_to = 'perlmonk@perlmonks.org'; my $subj = 'FTP report'; my $date = scalar localtime; my @deletions; # Let's define the paths. my $one_month = '/ftp'; my $one_week = '/ftp/ftp/incoming'; # Traverse desired filesystems my $days = 30; File::Find::find({wanted => \&wanted}, $one_month); my $days = 7; File::Find::find({wanted => \&wanted}, $one_week); # Send the report. open(MAIL, "|/usr/lib/sendmail -t ") || die "Can't open sendmail: $!"; print MAIL <<MESSAGE; From: perlmonk To: $mail_to Subject: $subj Weekly FTP policy report for $date ---------------------------- MESSAGE # What did we find? if ($#deletions == 0 ) { print MAIL "Nobody violated our FTP policy. No files deleted."; } else { print MAIL "The following files have been deleted:\n\n"; for my $element (@deletions) { print MAIL "$element\n"; } } close(MAIL); sub wanted { /software|patch|files/i and $File::Find::prune = 1; return if ((-d $_) || (int(-M $_) < $days)); push @deletions, "$File::Find::name"; unlink "$File::Find::name" || die "Can't delete file: $!"; }

Shouldn't unlink only delete the files that go into @deletions?

What am I missing?

Replies are listed 'Best First'.
Re: Whoops! I deleted everything!
by tachyon (Chancellor) on Dec 10, 2002 at 22:57 UTC

    Drop everything. Shut the server down if you can. Then read on. You can rescue large chunks of data

    Shouldn't unlink only delete the files that go into @deletions?

    It does. The problem is that $days is not passed into your deletion sub. This is because it is localised to package main:: (your code) but the sub is called in package File::Find where there is no var $days. As a result $days is == 0. As a result all files are too old as none of them have -M < 0. As a result all files get deleted. Getting out the all seeing RETROSPECTOSCOPE: If you had used warnings Perl would have cried about the uninitialised $days var for every call to &wanted (as well as the two my's BTW). If you had have used print as a test first you would have saved much pain - I always do these days having been made painfully aware of the awesome power of unlink with code that does not work as expected. If you unmount your file system right now and remount it read only you will be able to rescue some data. There is a node about doing that here somewhere. You have my sympathy. Been there. Deleted that. It hurts.

    I am especially sorry for you as you have been doing the right thing and using strict. Remove strict and the my before $days and $days is global and......it works as expected. Ouch. This does not mean strict is bad but I would advise using it together with warnings as standard practice to catch these sort of issues.

    Update

    I can't find the link here I was after but http://recover.sourceforge.net/links/ is a good place to start. From your code you are on *nix so you want to unmount the partitions/disk(s) affected as read only and (perhaps) even boot from another disk. You can then recover data that has not already been overwritten. Don't expect miracles as you will get back raw inodes but if the data is vital, there are no good backups or your job is on the line...it may be worth it.
    #!/usr/local/bin/perl use strict; use File::Find; #use warnings; # <--- this would have saved the day my $one_month = 'd:/sharedata/perl'; my $one_week = '/ftp/ftp/incoming'; # Traverse desired filesystems my $days = 30; File::Find::find({wanted => \&wanted}, $one_month); my $days = 7; File::Find::find({wanted => \&wanted}, $one_week); sub wanted { /software|patch|files/i and $File::Find::prune = 1; do { print "'$days' Skipping $File::Find::name\n" ; return } if (( +-d $_) || (int(-M $_) < $days)); push @deletions, "$File::Find::name"; print "'$days' Deleting: $File::Find::name\n" || die "Can't delete + file: $!"; } # notice the lack of a value for $days..... __DATA__ '' Skipping d:/sharedata/perl '' Skipping d:/sharedata/perl/Webmail3 '' Deleting: d:/sharedata/perl/Webmail3/AddresessBookExport Fields.txt '' Deleting: d:/sharedata/perl/Webmail3/config.pl '' Deleting: d:/sharedata/perl/Webmail3/convert.pl # with warnings ............... __DATA__ "my" variable $days masks earlier declaration in same scope at test.pl + line 17. Use of uninitialized value in numeric lt (<) at test.pl line 28. "my" variable $days masks earlier declaration in same scope at test.pl + line 17. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. "my" variable $days masks earlier declaration in same scope at test.pl + line 17. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. "my" variable $days masks earlier declaration in same scope at test.pl + line 17. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28. Use of uninitialized value in numeric lt (<) at test.pl line 28.

    Bummer

    tachyon

    s&&rsenoyhcatreve&&&s&n.+t&"$'$`$\"$\&"&ee&&y&srve&&d&&print

      It does. The problem is that $days is not passed into your deletion sub. This is because it is localised to package main:: (your code) but the sub is called in package File::Find where there is no var $days. As a result $days is == 0.

      You were close. The fact that $days is a lexical doesn't actually make a difference. Notice that @deletions is also lexical and it doesn't cause any problems. The package that the sub is called from doesn't actually matter. The $days variable is indeed visible to the file in which the wanted sub is defined.

      The real gotcha here is that $days was declared twice. Basically, the second declaration causes the first to be completely ignored. It's like that whole line is commented out. So, on the first call, which takes place to the actual initialization, $days is still undefined. On the second call, it is properly initialized.

      -sauoq
      "My two cents aren't worth a dime.";
      

        Hmm, it is as you say....

        cheers

        tachyon

        s&&rsenoyhcatreve&&&s&n.+t&"$'$`$\"$\&"&ee&&y&srve&&d&&print

      What a Gotcha!

      Thanks for figuring it out. I was stumped. Thankfully the files are not that important (I think!). I might hear about it tomorrow, but I shouldn't be in too much trouble.

      It's time for me to buy a tape drive for that server!

      I will never forget this lesson.

        By the time you buy that tape drive , I might even have gotten around to re-writing the POD for Device::MagneticTape, which has undergone some pretty substantial changes of late, so if you are Talking to Magnetic Tapes try it.

        </shamelessplug>
Re: Whoops! I deleted everything!
by sauoq (Abbot) on Dec 11, 2002 at 00:21 UTC
    What am I missing?

    A bunch of data apparently... (I couldn't resist.)

    I know you don't want to hear this now, but you should never run (even as a test) code like this without neutering it first. Don't unlink anything but have it write the names of the files it would have unlinked to a file for inspection. Only after you are sure that it won't do anything harmful, run it on a test directory. Triple check it before you run it in a production environment.

    The problem is with your $days variable. You declared it with my() twice. Apparently, that results in some freakish behavior. During the first call to File::Find::find, $days is undefined. It is properly set to 7 during the second call.

    Using warnings would have helped you spot this bug early.

    By the way, if ($#deletions == 0 ) is not what you mean. That will be true if @deletions has a single element as the index of that element will indeed be zero. You mean if (@deletions == 0) which will be true if @deletions has no elements.

    -sauoq
    "My two cents aren't worth a dime.";
    
      Thanks for your insight. I was too confident about my code, so I let it run. Big mistake. I will always use warnings and do more testing in the future.

      The problem is with your $days variable. You declared it with my() twice. Apparently, that results in some freakish behavior.

      Is it really a bug? It just seems like it should work. Would it have worked properly if I hadn't used my() and just made $days global? What would be the best solution?

        Well, when you declare it twice, the first declaration overides the second and it is like the first one didn't exist at all. You could comment out the my $days = 31; and your program will behave exactly the same.

        It would have worked if $days was global but I don't think that's the best solution. I think the best solution is simply to remove the 'my' on the line where you set my $days = 7;. So just change that line to $days = 7; and that should do the trick.

        Of course, I still recommend testing thoroughly before actually allowing it to unlink files. ;-)

        -sauoq
        "My two cents aren't worth a dime.";
        
Re: Whoops! I deleted everything!
by jdporter (Paladin) on Dec 10, 2002 at 22:30 UTC
    What am I missing?
    I'm not sure, but I suspect you may be missing the fact that with those path values, everything under $one_week is also under $one_month, i.e. the find... $one_month is also picking up the stuff being picked up by find... $one_week.

    jdporter
    ...porque es dificil estar guapo y blanco.