Want to highlight a helpful answer? Upvote!

Did someone help you, or did an answer or User Tip resolve your issue? Upvote by selecting the upvote arrow. Your feedback helps others! Learn more about when to upvote >

Looks like no one’s replied in a while. To start the conversation again, simply ask a new question.

Ruby, irb and spotlight - possibly some AppleScript

I've been playing with ruby, specifically interactive ruby and now playing with the command line spotlight commands mdfind and mdls.


Basically I'm trying to write a small collection of functions that can find image files which are certain types, that are specific dimensions, or larger than a certain dimensions, or have been created in the last x days, or created y months ago.


I'm using the ruby that comes with Mavericks and I'm running 10.9.3, I'm not running the ruby code in any kind of server environment. Just a user account. I would like to see what I've written used by others so since I'm new to this I thought I'd get someone else to pass an eye over it.


I'd also like to do the same for AppleScript, but though I've written quite a bit of AppleScript in the distant past I'm pretty rusty now and know longer know how to go write a library of routines, so hints on how to do that would appreciated.


I think I've covered myself on security related issues, no shell expansion and I believe the options passed in are just arguments for the command which is the first item. But I would still like comments from someone with experience?


For testing purposes I've got a require_relative in a .irbrc file where I start irb from which loads the following:


#!/usr/bin/env ruby


require 'Open3'


module SpotlightCommand

def self.get_imagewidthheight(imageFilePath)

finalResult = [ -1, -1 ] # width, height

resultStr, exitVal = Open3.capture2("mdls", "-name", "kMDItemPixelWidth",

"-name", "kMDItemPixelHeight", imageFilePath)

unless exitVal.exitstatus.zero?

return finalResult

end


resultArray = resultStr.split("\n")

if (resultArray.length.eql? 2)

tempArray = resultArray[0].split(" ")

if (tempArray[0].eql? "kMDItemPixelHeight")

finalResult[1] = tempArray[2].to_i

elsif (tempArray[0].eql? "kMDItemPixelWidth")

finalResult[0] = tempArray[2].to_i

end


tempArray = resultArray[1].split

if (tempArray[0].eql? "kMDItemPixelWidth")

finalResult[0] = tempArray[2].to_i

elsif (tempArray[0].eql? "kMDItemPixelHeight")

finalResult[1] = tempArray[2].to_i

end

end

return finalResult

end



def self.make_contenttypepartofquery(fileType)

contentTypeQueryPart = nil

typesHash = { :"public.jpeg" => "public.jpeg",

:"public.png" => "public.png",

:"public.tiff" => "public.tiff",

:"com.compuserve.gif" => "com.compuserve.gif" }

unless fileType.nil?

fileType = typesHash[fileType.intern]

end



if fileType.nil?

contentTypeQueryPart = "kMDItemContentTypeTree == public.image"

else

contentTypeQueryPart = "kMDItemContentType == " + fileType

end

return contentTypeQueryPart

end



def self.find_imagefileswith_widthheightfiletype(width, height,

fileType = nil, onlyInDirPath = nil)

theCommand = [ "mdfind" ]

unless onlyInDirPath.nil?

theCommand.push("-onlyin")

theCommand.push(onlyInDirPath)

end


query = self.make_contenttypepartofquery(fileType) + " && "



query += "kMDItemPixelWidth == " + width.to_s + " && "

query += "kMDItemPixelHeight == " + height.to_s

theCommand.push(query)

puts theCommand.to_s

theOutput = ""

IO.popen(theCommand) do |io|

theOutput = io.read

end

theOutput = theOutput.split("\n")

return theOutput

end



def self.find_imagefileswith_widthheightgreaterthan_filetype(

greaterThanWidth, greaterThanHeight, fileType = nil, onlyInDirPath = nil)

theCommand = [ "mdfind" ]

unless onlyInDirPath.nil?

theCommand.push("-onlyin")

theCommand.push(onlyInDirPath)

end


query = self.make_contenttypepartofquery(fileType) + " && "



query += "kMDItemPixelWidth >= " + greaterThanWidth.to_s + " && "

query += "kMDItemPixelHeight >= " + greaterThanHeight.to_s

theCommand.push(query)

puts theCommand.to_s

theOutput = ""

IO.popen(theCommand) do |io|

theOutput = io.read

end

theOutput = theOutput.split("\n")

return theOutput

# return ""

end


def self.find_imagefilescreated_inmonthsago(

monthsAgo, fileType = nil, onlyInDirPath = nil)

monthsAgo =(-( monthsAgo.to_i )).to_s

monthsAgoPlus1 = (monthsAgo.to_i + 1).to_s



theCommand = [ "mdfind" ]

unless onlyInDirPath.nil?

theCommand.push("-onlyin")

theCommand.push(onlyInDirPath)

end


query = self.make_contenttypepartofquery(fileType) + " && "



query += "kMDItemContentCreationDate > $time.this_month(" + monthsAgo+")" +

" && kMDItemContentCreationDate < $time.this_month("+monthsAgoPlus1+")"

theCommand.push(query)

puts theCommand.to_s

theOutput = ""

IO.popen(theCommand) do |io|

theOutput = io.read

end

theOutput = theOutput.split("\n")

return theOutput

end



def self.find_imagefilescreated_sincedaysago(

daysAgo, fileType = nil, onlyIn = nil)

daysAgo = (-(daysAgo.to_i)).to_s

theCommand = [ "mdfind" ]

unless onlyIn.nil?

theCommand.push("-onlyin")

theCommand.push(onlyIn)

end


query = self.make_contenttypepartofquery(fileType) + " && "



query += "kMDItemContentCreationDate >= $time.today(" + daysAgo + ")"

theCommand.push(query)

puts theCommand.to_s

theOutput = ""

IO.popen(theCommand) do |io|

theOutput = io.read

end

theOutput = theOutput.split("\n")

return theOutput

end

end


When running interactive ruby (irb) I then call the routines like so:


results = SpotlightCommand.get_imagewidthheight("/Users/ktam/Pictures/NewYearsEveAndJan20 04/DSCN0706.JPG") #1

results = SpotlightCommand.find_imagefileswith_widthheightfiletype(2272, 1704, "public.image", "/Users/ktam/Pictures") # 2

results = SpotlightCommand.find_imagefileswith_widthheightgreaterthan_filetype(2273, 1705) # 3

results = SpotlightCommand.find_imagefilescreated_inmonthsago(3, "public.image") # 4

results = SpotlightCommand.find_imagefilescreated_sincedaysago(30, "public.image") # 5

Posted on May 27, 2014 3:17 AM

Reply
7 replies

May 27, 2014 4:02 AM in response to ktam2

Argghhh, apologies. I kept automatically doing command S, which posts the above message so that it has multiple edits. For some reason I can now no longer edit the message. My last and what I thought was the final edit with tags set etc. was refused. Hopefully I can resist the automatic command S now.


I wanted to add the following:


The result of command 1 is a ruby array whose first element is the image width, and the second the image height.

The result of command 2 is a list of the image files in my Pictures folder which have a width of 2272 pixels and a height of 1704 pixels.

The result of command 3 is a list of every image file on my computer with the width greater than 2272 pixels and height greater than 1705 pixels.

The result of command 4 is a list of image files on my computer that have a content creation date more recently than 3 months ago, but older than 2 months ago.

The result of command 5 is a list of every image file whose content was created in the last 30 days.


Commands 2 to 5 can produce huge amounts of output. I was bit concerned the pipe would fill and block the command but everything appears to work as it should.

May 27, 2014 1:24 PM in response to ktam2

Those are some serious method names - you might look into using named and/or optional parameters. I mainly see a bit of duplication with the statements that perform the shell task (note that you should specify an encoding, since I got an error when a file name contains special characters), and you could probably use more of the language features to tighten up some of the statements, for example:

theCommand.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?


Anything else would most likely be a style preference - I've found that the RuboCop gem is helpful to spot a lot of the style stuff that hard-core rubyists will give you grief over (don't look at me).


AppleScript would probably be a more-or-less direct conversion, but if you get much more complex, you will wind up adding a lot more statements to make up for stuff that is a part of the Ruby language (i.e. my previous example). New in Mavericks are AppleScript Libraries, so sharing your script libraries is easier than ever.

May 28, 2014 11:22 AM in response to red_menace

Thanks red menace. Here is my next attempt:


#!/usr/bin/env ruby


require 'Open3'


module SpotlightCommand

# returns a two item array, [ width, height ]

def self.get_imagewidthheight(imageFilePath)

finalResult = [ -1, -1 ] # width, height

resultStr, exitVal = Open3.capture2("mdls", "-name", "kMDItemPixelWidth",

"-name", "kMDItemPixelHeight", imageFilePath)

return finalResult unless exitVal.exitstatus.zero?

resultArray = resultStr.split("\n")

if (resultArray.length.eql? 2)

tempArray = resultArray[0].split(" ")

if (tempArray[0].eql? "kMDItemPixelHeight")

finalResult[1] = tempArray[2].to_i

elsif (tempArray[0].eql? "kMDItemPixelWidth")

finalResult[0] = tempArray[2].to_i

end

tempArray = resultArray[1].split(" ")

if (tempArray[0].eql? "kMDItemPixelWidth")

finalResult[0] = tempArray[2].to_i

elsif (tempArray[0].eql? "kMDItemPixelHeight")

finalResult[1] = tempArray[2].to_i

end

end

return finalResult

end


# essentially a private module method, though I've not found a easy solution def self.make_contenttypepartofquery(fileType)

contentTypeQueryPart = nil

typesHash = { :"public.jpeg" => "public.jpeg",

:"public.png" => "public.png",

:"public.tiff" => "public.tiff",

:"com.compuserve.gif" => "com.compuserve.gif" }



fileType = typesHash[fileType.intern] unless fileType.nil?

contentTypeQueryPart = if fileType.nil?

"kMDItemContentTypeTree == public.image"

else

"kMDItemContentType == " + fileType

end

return contentTypeQueryPart

end


# essentially a private module method, though I've not found a easy solution

def self.runquerycommand(theCommand)

theOutput = ""

IO.popen(theCommand) do |io|

theOutput = io.read

end

theOutput = theOutput.split("\n")

return theOutput

end


def self.find_imagefiles(width: 800, height: 600,

fileType: "public.image", onlyInDirPath: nil)

theCommand = [ "mdfind" ]

theCommand.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?



query = self.make_contenttypepartofquery(fileType) + " && "

query += "kMDItemPixelWidth == " + width.to_s + " && "

query += "kMDItemPixelHeight == " + height.to_s

theCommand.push(query)

return self.runquerycommand(theCommand)

end


def self.find_imagefiles_largerthan(width: 800, height: 600,

fileType: nil, onlyInDirPath: nil)

theCommand = [ "mdfind" ]

theCommand.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?

query = self.make_contenttypepartofquery(fileType) + " && "

query += "kMDItemPixelWidth >= " + width.to_s + " && "

query += "kMDItemPixelHeight >= " + height.to_s

theCommand.push(query)

return self.runquerycommand(theCommand)

end


def self.find_imagefilescreated(monthsAgo: 3, fileType: nil,

onlyInDirPath: nil)

monthsAgo =(-( monthsAgo.to_i )).to_s

monthsAgoPlus1 = (monthsAgo.to_i + 1).to_s



theCommand = [ "mdfind" ]

theCommand.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?



query = self.make_contenttypepartofquery(fileType) + " && "

query += "kMDItemContentCreationDate > $time.this_month(" + monthsAgo+")" +

" && kMDItemContentCreationDate < $time.this_month("+monthsAgoPlus1+")"

theCommand.push(query)

return self.runquerycommand(theCommand)

end


def self.find_imagefilescreated_since(daysAgo: 20, fileType: nil,

onlyInDirPath: nil)

daysAgo = (-(daysAgo.to_i)).to_s

theCommand = [ "mdfind" ]

theCommand.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?

query = self.make_contenttypepartofquery(fileType) + " && "

query += "kMDItemContentCreationDate >= $time.today(" + daysAgo + ")"

theCommand.push(query)

return self.runquerycommand(theCommand)

end

end

May 28, 2014 3:07 PM in response to red_menace

I've been looking at the encoding issue. First my configuation. Mavericks 10.9.3. Standard ruby and irb.


I put an image file in a folder called temp with the ß character in the name and a few characters with accents like ü, îéå etc.


If using irb I paste in the full path into:


result = SpotlightCommand.get_imagewidthheight("/Users/ktam/Pictures/temp/DSCßüéåî_0196. JPG")


It doesn't work. If I however use:


pathToFile = SpotlightCommand.find_imagefiles(width: 1280, height: 1024, fileType: "public.jpeg", onlyInDirPath: "/Users/ktam/Pictures/temp")[0]


and then do:


result = SpotlightCommand.get_imagewidthheight(pathToFile)


It works.


I can see when I paste into the irb session that the path isn't right. If I exit irb and paste into the same terminal window on the command line the pasted file path is correct. The string handling of the routines themselves seems to be find so I did some stackoverflowing and that suggests that there is a problem with irb and encodings on OS X with no one actually specifying solutions.


Starting irb with "irb -E UTF-8:UTF-8" doesn't help with typing in non roman paths, or even pasting in non roman paths.


If you are, that is "red menace" (excellent name by the way) getting the same behaviour as me then that is something that seems to a problem with irb and can be worked around. If you are seeing extra added broken behaviour let me know.


Thanks again

Kevin

May 28, 2014 4:51 PM in response to ktam2

I am running the script straight from BBEdit instead of irb, but adding UTF-8 encoding when runing the query command works for me. I'm not sure what the default encoding is, but it looks like maybe ASCII - I don't think I had much more than an ellipse or something in there somewhere.


Ruby statements usually return their results, so you normally don't need an explicit return statement at the end of a method, and if appropriate the last expression result can even be used. I also noticed a couple of other odd things such as converting a string to a symbol so that you could look up the symbol in a hash to get a string (??). There are a few other optimizations that could probably be done, but after playing with it for a little while stripping out some of the redundant and AppleScripty looking stuff, I'm posting my flavor to show what I did with the returns and a couple of the query methods:

#!/usr/bin/env ruby require 'Open3' module Spotlight    # returns a two item array, [width, height]    def self.get_imageWidthHeight(imageFilePath)       finalResult = [-1, -1] # width, height       resultStr, exitVal = Open3.capture2('mdls', '-name', 'kMDItemPixelWidth',                                           '-name', 'kMDItemPixelHeight', imageFilePath)       return finalResult unless exitVal.exitstatus.zero? && !resultStr.include?('null')       resultStr.split("\n").each do |item|          if item.include?('kMDItemPixelWidth')             finalResult[0] = item.partition(' = ').last.to_i # width          else             finalResult[1] = item.partition(' = ').last.to_i # height          end       end       finalResult    end    # essentially a private module method, though I've not found a easy solution    def self.make_contentTypePartOfQuery(fileType)       fileType = 'public.image' if fileType.nil?       "kMDItemContentTypeTree == #{fileType}"    end    # essentially a private module method, though I've not found a easy solution    def self.runQueryCommand(command)       output = ''       IO.popen(command, encoding: 'UTF-8') { |io| output = io.read }       output.split("\n")    end    def self.find_imageFiles(width: 800, height: 600,                             fileType: 'public.image', onlyInDirPath: nil)       command = ['mdfind']       command.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?       query = make_contentTypePartOfQuery(fileType) + ' && '       query += "kMDItemPixelWidth == #{width} && "       query += "kMDItemPixelHeight == #{height}"       runQueryCommand(command.push(query))    end    def self.find_imageFiles_largerThan(width: 800, height: 600,                                        fileType: nil, onlyInDirPath: nil)       command = ['mdfind']       command.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?       query = make_contentTypePartOfQuery(fileType) + ' && '       query += "kMDItemPixelWidth >= #{width} && "       query += "kMDItemPixelHeight >= #{height}"       runQueryCommand(command.push(query))    end    def self.find_imageFilesCreated(monthsAgo: 3, fileType: nil,                                    onlyInDirPath: nil)       command = ['mdfind']       command.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?       query = make_contentTypePartOfQuery(fileType) + ' && '       query += "kMDItemContentCreationDate > $time.this_month(#{(-monthsAgo.to_i)} && "       query += "kMDItemContentCreationDate < $time.this_month(#{(-monthsAgo.to_i + 1)})"       runQueryCommand(command.push(query))    end    def self.find_imageFilesCreatedSince(daysAgo: 20, fileType: nil,                                         onlyInDirPath: nil)       command = ['mdfind']       command.push('-onlyin', onlyInDirPath) unless onlyInDirPath.nil?       query = make_contentTypePartOfQuery(fileType) + ' && '       query += "kMDItemContentCreationDate >= $time.today(#{(-daysAgo.to_i)})"       runQueryCommand(command.push(query))    end end

p Spotlight.get_imageWidthHeight('/path/to/some/file') p Spotlight.find_imageFiles(width: 640, height: 480, fileType: 'public.image', onlyInDirPath: '/path/to/some/folder/') p Spotlight.find_imageFiles_largerThan(width: 800, height: 600) p Spotlight.find_imageFilesCreated(monthsAgo: 3, fileType: 'public.image') p Spotlight.find_imageFilesCreatedSince(daysAgo: 30, fileType: 'public.image')


Also note that Rubyists will still probably give you a bit of lip about your method names, so be sure to have your Nomex® underwear handy when posting snippets on StackOverflow. The standard is to use snake_case, but as long as you are consistent it doesn't really matter - I use camelCase myself, since that is what all of the Objective-C stuff uses.

May 29, 2014 1:59 AM in response to red_menace

I am running the script straight from BBEdit instead of irb, but adding UTF-8 encoding when runing the query command works for me. I'm not sure what the default encoding is, but it looks like maybe ASCII - I don't think I had much more than an ellipse or something in there somewhere.

When running scripts directly from the command line things works. I'm surprised BBEdit doesn't default to UTF-8.

Ruby statements usually return their results, so you normally don't need an explicit return statement at the end of a method, and if appropriate the last expression result can even be used

The return for me is basically making it clear what the method is going to return, not just a particular value, but a clue as to the type of object as well. I've never liked things that you get one result as a side effect of another action. (Make an assignment, get a return value), so it is a personal style thing

I also noticed a couple of other odd things such as converting a string to a symbol so that you could look up the symbol in a hash to get a string (??).

I put the code in their to try and sanitise the input for the IO.popen method, though in the end I learnt that using the array form of the popen method reduces the need to sanitise input as each item in the array after the first is past as arguments (argv) to the first item in the array without expansion. So the security risk is already mostly dealt with. In the end I left it in because I was using the hash like a set, where if the uti file type passed in wasn't in the set then the hash returned nil and in that case I could fall back to assigning to the default of "public.image"


I like that you've used a block (lambda) for the split string. I think this is probably the biggest thing I'm missing when writing ruby is by default looking for the block solution. They're not something that comes naturally to me yet.


I'm not sure why the "#{width}" is so popular. Why is it preferred to just concatenating things together? Is there a reason or is it just that cool kids use it? Certainly that form is common on code I've seen posted on stack overflow.


Your solution for specifying the encoding in the IO.popen method doesn't fix things for entering paths when using irb, there is definitely something about hinky about irb's input when it comes to non roman characters. Your fix stays though because I don't know where input strings might come from.


I noticed that snake case is what is expected for method names, so I found your mixing my over verbose snake case with camel case a bit confusing. Nevertheless a big thank you for your input, I've learnt a lot.

May 29, 2014 6:45 AM in response to ktam2

BBEdit is using UTF-8, the error I get is from split when using it on the results of the io.read, which sometimes has a special character in some of the file paths. I normally use system() or backticks when doing shell commands, but it does the same thing using that, so there must be some issue when getting the results from the shell.


I think the string interpolation is popular because you don't have to close and then reopen the string or use other statements to get some text to use, and coercions to_s are taken care of. Using that and other things such as blocks (which is one of the more powerful features of Ruby) does take a bit of getting used to (tap is another one I'm still trying to figure out). Once you get the hang of it though, when you go back to some of your earlier projects you wind up wondering what the heck you were thinking.


Sorry about the method names, I was trying to figure out your style and made a guess that you were using the underscore to define the kind of method - find_, make_, get_, etc. Normally snake_case uses an underscore between each word, so it winds up something like make_content_type_part_of_query.

Ruby, irb and spotlight - possibly some AppleScript

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple ID.