rubyshellescapingexiftool

How to introduce a filename into shell script in Ruby script


Ruby script where I want to run a shell script and pass in a filename with spaces. I've tried a dozen ways of doing. Here's one:

filename = "/Users/gscar/Pictures/_Photo Processing Folders/Watched folder for import to Photos/2024.03.25-16.09.59.gs.O.orf"
filename = filename.gsub(' ', '\ ')
'exiftool -Camera:DriveMode #{filename}'

Other include hard coding the path

result = %x{exiftool -Camera:DriveMode /Users/gscar/Pictures/_Photo\ Processing\ Folders/Watched\ folder\ for\ import\ to\ Photos/2024.03.25-16.09.59.gs.O.orf}

which results in

8 files could not be read

and

system 'exiftool -Camera:DriveMode "/Users/gscar/Pictures/_Photo Processing Folders/Watched folder for import to Photos/2024.03.25-16.09.59.gs.O.orf"'

with no output.

Command line

āžœ exiftool -Camera:DriveMode "/Users/gscar/Pictures/_Photo Processing Folders/Watched folder for import to Photos/2024.03.25-16.09.59.gs.O.orf"
Drive Mode                      : Single Shot; Electronic shutter

Command line with escaped spaces works

āžœ exiftool -Camera:DriveMode /Users/gscar/Pictures/_Photo\ Processing\ Folders/Watched\ folder\ for\ import\ to\ Photos/2024.03.25-16.09.59.gs.O.orf
Drive Mode                      : Single Shot; Electronic shutter

What I need eventually is for this something like this to work. The above is trying to figure out how to get the filename working:

filename = "/Users/gscar/Pictures/_Photo Processing Folders/Watched folder for import to Photos/2024.03.25-16.09.59.gs.O.orf"
gsubfilename = filename.gsub(' ', '\ ')
`"exiftool -Camera:DriveMode #{gsubfilename}"`

and the result is:

sh: exiftool -Camera:DriveMode /Users/gscar/Pictures/_Photo\ Processing\ Folders/Watched\ folder\ for\ import\ to\ Photos/2024.03.25-16.09.59.gs.O.orf: No such file or directory

but of course running this in shell works fine.

What is "escaping" me here? Thanks

PS mini_exiftool doesn't seem to handle "DriveMode."


Solution

  • As comments suggest, the best way is to avoid the shell, and invoke the command with arguments in an array. Specifically, for system, if you give at least one argument besides the program name to the function, the shell is not invoked.

    system('exiftool', '-Camera:DriveMode', filename)
    

    In this way, Ruby directly calls exiftool, and passes the given arguments to it. Since shell is not involved, you can't have parameter expansion (e.g. system('ls', '*') will complain that there are no files named *).


    If you really really want to delegate argument parsing to the shell, use Shellwords, which knows how to escape shell arguments correctly:

    require 'Shellwords'
    
    system("exiftool -Camera:DriveMode #{filename.shellescape}")
    

    Invoked this way, system will invoke $SHELL (e.g. /bin/sh), and ask it to execute the passed string. The shell will then process the string like it normally would, splitting it into command and arguments, performing parameter substitutions and everything else the shell does before executing the command. Thus, system('ls *') does what you would expect: lists the files in the current directory (because shell would have expanded * into multiple arguments before invoking ls).