Escape whitespace when using backticks

2019-05-27 07:59发布

问题:

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:

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.



回答2:

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);


回答3:

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.



回答4:

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);


回答5:

Quote the variable name inside the backticks:

`mdls "$curr_file_path"`;
`mdls '$curr_file_path'`;