views:

169

answers:

5

I've had a search around, and from my perspective using backticks is the only way I can solve this problem. I'm trying to call the mdls command from Perl for each file in a directory to find it's last accessed time. The issue I'm having is that in the file names I have from find I have unescaped spaces which bash obviously doesn't like. Is there an easy way to escape all of the white space in my file names before passing them to mdls. Please forgive me if this is an obvious question. I'm quite new to Perl.

my $top_dir = '/Volumes/hydrogen/FLAC';

sub wanted { # Learn about sub routines 
    if ($File::Find::name) { 
         my $curr_file_path = $File::Find::name. "\n";
        `mdls $curr_file_path`;
         print $_;
    }
}

find(\&wanted, $top_dir);
+1  A: 

Quote the variable name inside the backticks:

`mdls "$curr_file_path"`;
`mdls '$curr_file_path'`;
socket puppet
You have to be much more careful than this. " and ' are legal filename characters.
brian d foy
This fails on file names like `sam's "holiday" song.flac` which is legal file name under Unix... In either case, the surrounding quotes are defeated by the embedded single or double quotes. You need to have `'sam\'s "holiday" song.flac'` or `"sam\'s \"holiday\" song.flac"` etc...
drewk
@drewk: be careful with this quoting approach: different shells behave different handling scaping quotes:[Including identical quotes within quotes](http://www.grymoire.com/Unix/Quote.html#toc-uh-8). `echo 'Don\'t quote me'` does not work neither in cshell nor in bash: The quotes turn substitution on and off. They are not used to indicate the starting and ending of a string. So you can only scape quotes unquoted (at least from my experience in bash): `echo 'the Sam'\'s' songs'` >The Sam's songs.
Pablo Marin-Garcia
Yes, escaping is devilishly hard. That is why I usually don't do it and use a Perl open that does not require it.
drewk
+5  A: 

If you are JUST wanting "last access time" in terms of of the OS last access time, mdls is the wrong tool. Use perl's stat. If you want last access time in terms of the Mac registered application (ie, a song by Quicktime or iTunes) then mdls is potentially the right tool. (You could also use osascript to query the Mac app directly...)

Backticks are for capturing the text return. Since you are using mdls, I assume capturing and parsing the text is still to come.

So there are several methods:

  1. Use the list form of system and the quoting is not necessary (if you don't care about the return text);

  2. Use String::ShellQuote to escape the file name before sending to sh;

  3. Build the string and enclose in single quotes prior to sending to sending to the shell. This is harder than it sounds because files names with single quotes defeats your quotes! For example, sam's song.mp4 is a legal file name, but if you surround with single quotes you get 'sam's song.mp4' which is not what you meant...

  4. Use open to open a pipe to the output of the child process like this: open my $fh, '-|', "mdls", "$curr_file" or die "$!";

Example of String::ShellQuote:

use strict; use warnings;
use String::ShellQuote;
use File::Find;

my $top_dir = '/Users/andrew/music/iTunes/iTunes Music/Music';

sub wanted { 
    if ($File::Find::name) { 
         my $curr_file = "$File::Find::name";
         my $rtr;
         return if -d;
         my $exec="mdls ".shell_quote($curr_file);
         $rtr=`$exec`;  
         print "$rtr\n\n";
    }
}

find(\&wanted, $top_dir);

Example of pipe:

use strict; use warnings;
use String::ShellQuote;
use File::Find;

my $top_dir = '/Users/andrew/music/iTunes/iTunes Music/Music';

sub wanted { 
    if ($File::Find::name) { 
         my $curr_file = "$File::Find::name";
         my $rtr;
         return if -d;
         open my $fh, '-|', "mdls", "$curr_file" or die "$!";
         { local $/; $rtr=<$fh>; }  
         close $fh or die "$!";
         print "$rtr\n\n";
    }
}

find(\&wanted, $top_dir);
drewk
+3  A: 

If you just want to find the last access time, is there some weird Mac reason you aren't using stat? When would it be worse than kMDItemLastUsedDate?

 my $last_access = ( stat($file) )[8];

It seems kMDItemLastUsedDate isn't always updated to the last access time. If you work with a file through the terminal (e.g. cat, more), kMDItemLastUsedDate doesn't change but the value that comes back from stat is right. touch appears to do the right thing in both cases.

It looks like you need stat for the real answer, but mdls if you're looking for access through applications.

brian d foy
There is much more info in `mdls` than just the access time.
drewk
There's much more info in stat too. However, he said he was looking for the last access time.
brian d foy
If he meant "last access time" in the OS sense, he should use `stat` if he meant "last used time" by the file type's registered creator (a Mac concept) then I think `mdls` is the right tool. I don't know what he is trying to do tho...
drewk
Which is why I asked that very question. It's all in my answer.
brian d foy
+4  A: 

If you're sure the filenames don't contain newlines (either CR or LF), then pretty much all Unix shells accept backslash quoting, and Perl has the quotemeta function to apply it.

my $curr_file_path = quotemeta($File::Find::name);
my $time = `mdls $curr_file_path`;

Unfortunately, that doesn't work for filenames with newlines, because the shell handles a backslash followed by a newline by deleting both characters instead of just the backslash. So to be really safe, use String::ShellQuote:

use String::ShellQuote;
...
my $curr_file_path = shell_quote($File::Find::name);
my $time = `mdls $curr_file_path`;

That should work on filenames containing anything except a NUL character, which you really shouldn't be using in filenames.

Both of these solutions are for Unix-style shells only. If you're on Windows, proper shell quoting is much trickier.

cjm
+2  A: 

You can bypass the shell by expressing the command as a list, combined with capture() from IPC::System::Simple:

use IPC::System::Simple qw(capture);

my $output = capture('mdls', $curr_file_path);
Ether