What is Best Method to Determine Base Finder Item Name?

finder

(Jim Underwood) #1

Is there a better method than using Satimage.osax?

### REQUIRES Satimage.osax ###
# It handles Folders and Files, and Files without an extension

tell application "Finder" to set fItemPath to POSIX path of (item 1 of (get selection as alias list))
set fItemBaseName to change ".+\\/([^\\/\\.]+).*" into "\\1" in fItemPath syntax "PERL" with regexp
-->TEST_DEV   for a folder
-->TEST Chrome Archive   for a file

But maybe there is a better method without using RegEx?

BTW, I have learn that one should always use the clause syntax “PERL” with the Satimage.osax find and change commands. By default it uses RUBY, which is not good.


(Nigel Garvey) #2

Hi Jim.

Possibly a better regex:

tell application "Finder" to set fItemPath to POSIX path of (item 1 of (get selection as alias list))
-- Delete anything in the path which is a slash optionally followed by sections ending with a slash (except at the very end), 
-- or which is a dot followed by characters which don't include dots or slashes (except possibly for a slash at the very end),
-- or which is otherwise a slash at the very end.
set fItemBaseName to change "/(?:[^/]*/)*(?!$)|\\.[^\\./]+/?$|/$" into "" in fItemPath syntax "PERL" with regexp

Vanilla, ASObjC, and shell scripts all offer other methods. I imagine that whether or not any of them are better than using Satimage would depend on where you’re starting, what else you’re doing, what you find easiest, and for whom you’re writing the script.


(Shane Stanley) #3

Best is pretty subjective. I’d argue that this is more self-documenting to all but regex fiends:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions


tell application "Finder" to set fItemPath to POSIX path of (item 1 of (get selection as alias list))
set fItemBaseName to (current application's NSString's stringWithString:fItemPath)'s lastPathComponent()'s stringByDeletingPathExtension() as text

#4

Perhaps the Jamie Zawinski paradox is rooted in the problem that:

  1. it takes a lot of practice to become fluent in the operations of a Kleene algebra (hence a lot of rationalisable but still obfuscating over-use of regular expressions), and
  2. only when they have come to feel entirely natural and familiar, do you finally discover that in fact you rarely need them :slight_smile:

(Nigel Garvey) #5

I’m not sure about that. I find myself using them more now than I did before I knew how. :wink:


#6

Yes – there’s a phase transition at that point, and then another later, when most of it sublimes away :slight_smile:


#7

PS, being, alas, much lazier than Shane, I would probably do it by composing something from a set of pasted generic functions:

on run
    
    map(takeBaseName, selectedPaths())
    
end run

-- selectedPaths :: () -> [FilePath]
on selectedPaths()
    tell application "Finder"
        if (count of windows) > 0 then
            script
                on |λ|(x)
                    set fp to POSIX path of x
                    if last character of fp = "/" then
                        text 1 thru -2 of fp
                    else
                        fp
                    end if
                end |λ|
            end script
            
            my map(result, selection as alias list)
        else
            {}
        end if
    end tell
end selectedPaths

-- GENERICS -----------------------------------------------------------

-- intercalateString :: String -> [String] -> String
on intercalateString(sep, xs)
    set {dlm, my text item delimiters} to {my text item delimiters, sep}
    set s to xs as text
    set my text item delimiters to dlm
    return s
end intercalateString

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
    set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
    set xs to text items of strMain
    set my text item delimiters to dlm
    return xs
end splitOn

-- takeBaseName :: FilePath -> String
on takeBaseName(strPath)
    if strPath ≠ "" then
        if text -1 of strPath = "/" then
            ""
        else
            set fn to item -1 of splitOn("/", strPath)
            if fn contains "." then
                intercalateString(".", items 1 thru -2 of splitOn(".", fn))
            else
                fn
            end if
        end if
    else
        ""
    end if
end takeBaseName


(Nigel Garvey) #8

I’d do it using either my script or Shane’s. Both only remove the last elements of file names containing more than one dot and both produce something resembling a name when the selected item’s a folder or a package.


#9

Yes - there are, of course, various approaches and conventions that one could go for.

With folder paths, the takeBaseName above returns:

  1. takeBaseName "/Users/houthakker" -> “houthakker”
  2. takeBaseName "/Users/houthakker/" -> “”

(Interpreting the latter as a reference to no file in the parent folder)

For various reasons, I like to use a function which returns a value from the input string only, rather than from a combination of that and the current state of the filesystem, which I prefer to probe separately.

(When in doubt, for better or for worse, I adopt the conventions of eponymous functions in the standard Haskell modules - it just simplifies the mental model for me when I am working with various different scripting languages).

(So, for example, in JavaScript for Automation:

// takeBaseName :: FilePath -> String
const takeBaseName = strPath =>
  strPath !== '' ? (
    strPath[strPath.length - 1] !== '/' ? (() => {
      const fn = strPath.split('/').slice(-1)[0];
      return fn.includes('.') ? (
        fn.split('.').slice(0, -1).join('.')
      ) : fn;
    })() : ''
  ) : '';

#10

PS in the light of @NigelGarvey’s point about selecting items which yield a final / in their url (folder and packages), I’ve adjusted selectedPaths() above to shed any final “/”, allowing for more useful composition with takeBaseName()

Thanks !


(Nigel Garvey) #11

But at this point, your script’s still stripping everything after and including the first dot in any name instead of the last. eg. “Screen Shot 2018-03-21 at 13.44.58.png” is being returned as “Screen Shot 2018-03-21 at 13” instead of “Screen Shot 2018-03-21 at 13.44.58”.


#12

Thanks ! Updated takeBaseName() above to prune only after final dot.

( and a good reminder that I must finish writing a set of tests for these functions )


(Nigel Garvey) #13

More efficiently in vanilla:

tell application "Finder" to set HFSPath to (beginning of (get selection)) as text
takeBaseName(HFSPath, ":")

on takeBaseName(pathOrName, pathDelimiter)
	set astid to AppleScript's text item delimiters
	
	set AppleScript's text item delimiters to pathDelimiter
	if (pathOrName ends with pathDelimiter) then
		set baseName to text item -2 of pathOrName
	else -- pathOrName either doesn't end with or doesn't contain a path delimiter.
		set baseName to text item -1 of pathOrName
	end if
	
	if (baseName contains ".") then
		set AppleScript's text item delimiters to "."
		set baseName to text 1 thru text item -2 of baseName
	end if
	
	set AppleScript's text item delimiters to astid
	return baseName
end takeBaseName

#14

Looks good.

As a footnote on parameterising the path delimiter, and the choice of argument order (which prima facie seems arbitrary), if you flip the arguments, bringing the (less frequently varying) path delimiter argument into first position, you can then derive, at runtime, a path-delimiter-specific variant by applying a generic curry function.

This can turn out to be useful if you want to map your takeBaseName over a list of file paths.

For example:

on run
    
    -- ALLOWING FOR MULTIPLE SELECTIONS IN THE FINDER
    
    tell application "Finder" to set HFSPaths to selection as alias list
    
    map(|λ|(":") of curry(takeBaseName), HFSPaths)
    
end run


on takeBaseName(pathDelimiter, fp)
    set pathOrName to fp as text
    
    set astid to AppleScript's text item delimiters
    
    set AppleScript's text item delimiters to pathDelimiter
    if (pathOrName ends with pathDelimiter) then
        set baseName to text item -2 of pathOrName
    else -- pathOrName either doesn't end with or doesn't contain a path delimiter.
        set baseName to text item -1 of pathOrName
    end if
    
    if (baseName contains ".") then
        set AppleScript's text item delimiters to "."
        set baseName to text 1 thru text item -2 of baseName
    end if
    
    set AppleScript's text item delimiters to astid
    return baseName
end takeBaseName


-- GENERICS ------------------------------------------------------------------

-- curry :: ((a, b) -> c) -> a -> b -> c
on curry(f)
    script
        on |λ|(a)
            script
                on |λ|(b)
                    |λ|(a, b) of mReturn(f)
                end |λ|
            end script
        end |λ|
    end script
end curry


-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

(Jim Underwood) #15

Wow! Thanks guys for a very spirited and informative discussion! :+1:

I have selected the solution by @ShaneStanley as the best.
But the solution by @NigelGarvey is a close second.

Sorry Shane, but I have to disagree with you that “Best is pretty subjective”. If the proper testing and evaluation is done, like here in this thread, then, IMO, “best” becomes objective.

Nigel made it clear:

Best Solution

because it is very accurate (100% AFAIK), does not require any external osax/libs, and it is also compact and easy to read, even if you know nothing about ASObjC:
lastPathComponent()'s stringByDeletingPathExtension()

Second Best

It is a great solution, but RegEx requires either an external OSAX or more complicated ASObjC code, and is harder to read, if you didn’t write it.
Nigel, thanks for finding and correcting the error in my RegEx. Yours is definitely better.

Well, if by “fiends” you mean avid user and advocate, then I would plead guilty. :wink:

RegEx is but one of many tools in my tool kit. I always try to use the best tool (that I know how to use) for the job. I do have to say that I find RegEx very useful, often using it many times a day. Probably my most frequent use is in BBEdit, where the Find/Replace tool is excellent, and I use it to clean up a variety of data.

Just one more point for those interested in RegEx. I find the tool at RegEx101.com to be excellent for developing, testing, and documenting RegEx patterns.

Many thanks again to all who participated in this thread.


(Shane Stanley) #16

To be picky, the original question referred to the “Base Finder Item Name”. That implies how the name looks in the Finder, which is not necessarily what any of these scripts return.

Even ignoring potential localization issues, only Nigel’s delimiters-based code will give the correct result for names containing ‘/’.

The method is presumably called lastPathComponent rather than pathName because of the distinction.


(Jim Underwood) #17

Thanks for being “picky”, LOL.
But you are inferring more than I was implying.
I used the phrase “Finder Item” because I don’t know of another way to indicate either a file or folder. So I call them “Finder Items”. What would you suggest?

Using “/” or “:” in a file name in any OS is asking for disaster, IMO. I never do it, even though technically the Mac Finder permits “/”.

OK, so what do you want us to do with that great piece of info? Are you saying that we should not use your script? Or is this just a case of too much information? LOL

Now, on to more important matters. I came back to the forum tonight (this morning) to ask you a question about using your method in another script.

I have this script, which I think you wrote, that returns a list of paths. I want it to also return the base name of the file/folder:

  set theURLs to fileManager's contentsOfDirectoryAtURL:sourceURL includingPropertiesForKeys:{} options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
  set theURLs to theURLs's allObjects()
  set foundItemList to current application's NSPredicate's predicateWithFormat_("lastPathComponent matches %@", findPattern)
  set foundItemList to theURLs's filteredArrayUsingPredicate:foundItemList
  set foundPathList to (foundItemList's valueForKey:"path") as list
  
  set foundNameList to {}
  
  repeat with oPath in foundPathList
    set end of foundNameList to (current application's NSString's stringWithString:oPath)'s lastPathComponent()'s stringByDeletingPathExtension() as text
  end repeat

This seems like brute force to get the base name. Is there a better way?
I tried:
set foundNameList to (foundItemList's valueForKey:"name") as list
but that failed.

Thanks.


(Shane Stanley) #18

No. I was just musing out loud that name means different things in different contexts, and that what you intend to do with it can affect how you should discover it.

Sure. URLs also have a lastPathComponent property, so:

set foundNameList to (foundItemList's valueForKeyPath:"lastPathComponent.stringByDeletingPathExtension") as list