FileManagerLib Features, etc


(Ed Stockly) #1

This is odd. I’m rewriting a script that’s at least 20 years old, that finds all files on the desktop (or any designated folder) and sorts them by kind. It’s working perfectly, in a fraction of the time the old one (originally written on system 7x, and updated with very minor tweaks).

But for some reason, this command doesn’t pick up a PDF file that’s on the desktop. (It’s the first file in the second list.

This script has always been a big help, because I use the desktop as a catch-all for anything I’m working on.

I’ve tested the FileManagerLib command with numerous files and folders of every variety, including other PDF files and it works flawlessly except for this one pdf file.


use scripting additions
use script "FileManagerLib"

set itemsInFolder to contents of (path to desktop as alias) ¬
   searching subfolders false ¬
   include invisible items false ¬
   include folders true ¬
   include files true ¬
   result type files list

--This is what it finds: 
{file "Macintosh HD:Users:stocklys:Desktop:KetoKookBook alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop:calibre.app alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop: Items from Desktop", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Script Libraries", ¬
   file "Macintosh HD:Users:stocklys:Desktop:My Cloud", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Testing", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Folders", ¬
   file "Macintosh HD:Users:stocklys:Desktop:ProductionScripts alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Items from Desktop:"}

--This is what it should find: 
{file "Macintosh HD:Users:stocklys:Desktop:Inizio BS Enroll.pdf", ¬
   file "Macintosh HD:Users:stocklys:Desktop:KetoKookBook alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop:calibre.app alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop: Items from Desktop", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Script Libraries", ¬
   file "Macintosh HD:Users:stocklys:Desktop:My Cloud", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Testing", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Folders", ¬
   file "Macintosh HD:Users:stocklys:Desktop:ProductionScripts alias", ¬
   file "Macintosh HD:Users:stocklys:Desktop:Items from Desktop:"}
   

(Jonas Whale) #2

Can this be of any help ?

use AppleScript version "2.5"
use framework "Foundation"
use framework "AppKit"
use scripting additions

set thePath to current application's NSString's stringWithString:"~/Desktop/"
set thePath to thePath's stringByExpandingTildeInPath()
set theURL to current application's |NSURL|'s fileURLWithPath:thePath
set theFileManager to current application's NSFileManager's defaultManager()
set allURLs to theFileManager's contentsOfDirectoryAtURL:theURL includingPropertiesForKeys:{} options:4 |error|:(missing value)
allURLs as list

(Shane Stanley) #3

Strange. The use of contents of rather than objects of suggests you’re using an older version, but that shouldn’t matter. What happens if you try other result types? And include invisible items?

(Jonas’s script calls almost identical code, but it won’t hurt to try it.)


#4

Footnote:

a variant which lists files under UTI group headings:

use AppleScript version "2.4"
use framework "Foundation"
use framework "Appkit"
use scripting additions

on run
    
    set fldr to filePath("~/Desktop")
    script nameAndUTI
        on |λ|(strName)
            if strName starts with "." then
                {}
            else
                {{name:strName, uti:fileUTI(fldr & "/" & strName)}}
            end if
        end |λ|
    end script
    
    script groupListing
        on |λ|(a, gp)
            script indented
                on |λ|(x)
                    tab & "- " & (name of x)
                end |λ|
            end script
            
            a & uti of item 1 of gp & ":" & linefeed & ¬
                unlines(map(indented, gp)) & ¬
                linefeed & linefeed
        end |λ|
    end script
    
    fldr & linefeed & linefeed & ¬
        foldl(groupListing, "", ¬
            groupBy(|on|(my eq, fpUTI), ¬
                sortBy(mappendComparing({fpUTI, fpName}), ¬
                    concatMap(nameAndUTI, getDirectoryContents(fldr)))))
end run



on fpUTI(x)
    uti of x
end fpUTI

on fpName(x)
    name of x
end fpName

on label(x)
    (name of x) -- & tab & (uti of x)
end label


-- GENERIC FUNCTIONS ----------------------------------------------------

-- https://github.com/RobTrew/prelude-applescript

-- Left :: a -> Either a b
on |Left|(x)
    {type:"Either", |Left|:x, |Right|:missing value}
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    {type:"Either", |Left|:missing value, |Right|:x}
end |Right|

-- Tuple (,) :: a -> b -> (a, b)
on Tuple(a, b)
    {type:"Tuple", |1|:a, |2|:b, length:2}
end Tuple

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Right| of m then
        mReturn(mf)'s |λ|(|Right| of m)
    else
        m
    end if
end bindLR

-- Ordering  :: (-1 | 0 | 1)
-- compare :: a -> a -> Ordering
on compare(a, b)
    if a < b then
        -1
    else if a > b then
        1
    else
        0
    end if
end compare

-- concatMap :: (a -> [b]) -> [a] -> [b]
on concatMap(f, xs)
    set lng to length of xs
    if 0 < lng and class of xs is string then
        set acc to ""
    else
        set acc to {}
    end if
    tell mReturn(f)
        repeat with i from 1 to lng
            set acc to acc & |λ|(item i of xs, i, xs)
        end repeat
    end tell
    return acc
end concatMap


-- eq :: a -> a -> Bool
on eq(a, b)
    a = b
end eq

-- filePath :: String -> FilePath
on filePath(s)
    ((current application's ¬
        NSString's stringWithString:s)'s ¬
        stringByStandardizingPath()) as string
end filePath

-- fileStatus :: FilePath -> Either String Dict
on fileStatus(fp)
    set e to reference
    set {v, e} to current application's NSFileManager's defaultManager's ¬
        attributesOfItemAtPath:fp |error|:e
    if v is not missing value then
        |Right|(v as record)
    else
        |Left|((localizedDescription of e) as string)
    end if
end fileStatus

-- fileUTI :: FilePath -> String
on fileUTI(fp)
    set {uti, e} to (current application's ¬
        NSWorkspace's sharedWorkspace()'s ¬
        typeOfFile:fp |error|:(reference)) as list
    if uti is missing value then
        e's localizedDescription() as text
    else
        uti as text
    end if
end fileUTI

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

-- getDirectoryContents :: FilePath -> IO [FilePath]
on getDirectoryContents(strPath)
    set ca to current application
    (ca's NSFileManager's defaultManager()'s ¬
        contentsOfDirectoryAtPath:(stringByStandardizingPath of ¬
            (ca's NSString's stringWithString:(strPath))) ¬
            |error|:(missing value)) as list
end getDirectoryContents


-- Typical usage: groupBy(on(eq, f), xs)
-- groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
on groupBy(f, xs)
    set mf to mReturn(f)
    
    script enGroup
        on |λ|(a, x)
            if length of (active of a) > 0 then
                set h to item 1 of active of a
            else
                set h to missing value
            end if
            
            if h is not missing value and mf's |λ|(h, x) then
                {active:(active of a) & {x}, sofar:sofar of a}
            else
                {active:{x}, sofar:(sofar of a) & {active of a}}
            end if
        end |λ|
    end script
    
    if length of xs > 0 then
        set dct to foldl(enGroup, {active:{item 1 of xs}, sofar:{}}, tail(xs))
        if length of (active of dct) > 0 then
            sofar of dct & {active of dct}
        else
            sofar of dct
        end if
    else
        {}
    end if
end groupBy

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

-- mappendComparing :: [(a -> b)] -> (a -> a -> Ordering)
on mappendComparing(fs)
    script
        on |λ|(x, y)
            script
                on |λ|(ordr, f)
                    if ordr ≠ 0 then
                        ordr
                    else
                        tell mReturn(f)
                            compare(|λ|(x), |λ|(y))
                        end tell
                    end if
                end |λ|
            end script
            foldl(result, 0, fs)
        end |λ|
    end script
end mappendComparing

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

-- e.g. sortBy(|on|(compare, |length|), ["epsilon", "mu", "gamma", "beta"])
-- on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
on |on|(f, g)
    script
        on |λ|(a, b)
            tell mReturn(g) to set {va, vb} to {|λ|(a), |λ|(b)}
            tell mReturn(f) to |λ|(va, vb)
        end |λ|
    end script
end |on|

-- partition :: predicate -> List -> (Matches, nonMatches)
-- partition :: (a -> Bool) -> [a] -> ([a], [a])
on partition(f, xs)
    tell mReturn(f)
        set ys to {}
        set zs to {}
        repeat with x in xs
            set v to contents of x
            if |λ|(v) then
                set end of ys to v
            else
                set end of zs to v
            end if
        end repeat
    end tell
    Tuple(ys, zs)
end partition

-- Enough for small scale sorts.
-- Use instead sortOn :: Ord b => (a -> b) -> [a] -> [a]
-- which is equivalent to the more flexible sortBy(comparing(f), xs)
-- and uses a much faster ObjC NSArray sort method
-- sortBy :: (a -> a -> Ordering) -> [a] -> [a]
on sortBy(f, xs)
    if length of xs > 1 then
        set h to item 1 of xs
        set f to mReturn(f)
        script
            on |λ|(x)
                f's |λ|(x, h) ≤ 0
            end |λ|
        end script
        set lessMore to partition(result, rest of xs)
        sortBy(f, |1| of lessMore) & {h} & ¬
            sortBy(f, |2| of lessMore)
    else
        xs
    end if
end sortBy

-- tail :: [a] -> [a]
on tail(xs)
    if xs = {} then
        missing value
    else
        rest of xs
    end if
end tail

-- unlines :: [String] -> String
on unlines(xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, linefeed}
    set str to xs as text
    set my text item delimiters to dlm
    str
end unlines


(Ed Stockly) #5

So, in testing my script I’m putting all kinds of files on the desktop and running the script, and making sure they were all handled correctly. Out of hundreds of files and folders of all types and kinds that was the only file that got left behind.

Then, during one run I put dozens of PDF files on the desktop and the stranded file got moved with the others. I haven’t been able to recreate the error.

I appreciate the sample codes to resolve the issue, but my purpose was more to give feedback to Shane on his very useful script library.

I have been wanting to stop using finder/system events in favor of a good script library.

If it happens again, I’ll try the samples provided by Complex Point and Jonas, too, but I’m hoping a library will be the solution.


(Ed Stockly) #6

I’m using version 2.1 Created and last modified May 9.

What’s the current version?


(Shane Stanley) #7

It’s 2.1.1. But the change from contents of to objects of was made earlier than that.


(Ed Stockly) #8

OK, just installed. Sometimes when there’s a dictionary change the terminology in the script is automatically updated when compiled. This time I had to change each obsolete command.

Tried it on three macs and it seems to work.


(Ed Stockly) #9

So while I have your attention, Shane, here’s a few things I’d like to see in a file managing library.

  • Copy/move object with name a new name for the destination file

  • Sorting based on any of the options that appear in the Parse object command

  • Filtering based on any of options that appear in the parse object command

  • Option to return any file path or reference as an appleScript alias or alias list

use script "Filemanagerlib"

set resultAny to copy object directParamAny ¬
   to folder toFolderAny ¬
   with name newName ¬
   replacing replacingBoolean ¬
   return path returnPathBoolean

set resultAny to move object directParamAny ¬
   to folder toFolderAny ¬
   with name newName ¬
   replacing replacingBoolean ¬
   return path returnPathBoolean

set resultAnyList to objects of directParamAny ¬
   whose full_name contains "Foo"
   searching subfolders searchingSubfoldersBoolean ¬
   include invisible items includeInvisibleItemsBoolean ¬
   include folders includeFoldersBoolean ¬
   include files includeFilesBoolean ¬
   result type resultTypeResultTypes


(Shane Stanley) #10

They’re not unreasonable requests (well, perhaps apart from the last one — where do you need an alias instead of a file?), but the lib is getting a bit big for my comfort.

The first is just a matter of using rename object, and in AppleScript terms I think that’s arguably more readable code — the extra overhead is tiny. I actually added it to the command at one stage, and I felt it just made the dictionary more complex and potentially confusing.

I can see value in sorting by extension, but I’d need to see arguments for the other options.

Filtering just gets too complicated terminology-wise. NSPredicate is your friend.


(Ed Stockly) #11

I have 20+years of AppleScripts that I’m updating to use with libraries, most of them managing files using finder/system events. While updating these scripts, some still used every day, I came across this in the instructions:
“After you start the script, check your email or get up and stretch your legs, and come back in a few minutes and the script should be done.”

That same script, using FileManagerLib runs in just a few seconds now.

In my case, all of the scripts I’ve written over the years use alias/alias list whenever possible. I just updated one script that was written before Finder had the alias list option, and it has a routine stepping through a list of files, coercing each item to an alias. I’ll use the exact same handler for output from filemanagerlib. Maybe I don’t need to, and I wouldn’t write new scripts that way, but it would make updating old scripts much easier.

So without it, I need to move a file to a temp folder, make sure there’s not a file with that name already in the temp folder, then rename it and move it to the destination folder. In this scenario I don’t want keep duplicate files. I can’t rename it in place because there may be a file with the new name already in the source folder. I can’t move it with the old name because there is a file there in the destination folder.

Well, first, I’m wondering if “Kind” could be added to this list, or if that’s a uniquely finder/system events thing?

full_name
name_extension
name_stub
parent_folder_path
parent_folder_name
displayed_name

My thinking in including all of these is that if just if the information is there, and we may need it in relation to one item, we may need it to sort a group of items.

Sorting by name is already in the sort command, and for my purposes that’s enough, but I could imagine a scenario where sorting by name_stub, as opposed to full_name would be appropriate. (I don’t recall what the difference between displayed_name and full_name is, unless it’s based on whether extensions showing is on or off?) (also what does sort use now?)

I can see a scenario where I use entire contents to extract files, and then want them sorted by parent folder path. I could build a list of lists containing all the parsed data and then sort the list, but I think it would be faster to have the library do it (would certainly be simpler).

Got it. The solution would be to build the list of lists using parsed data, and extract what I need from the list based on what element I want.


(Shane Stanley) #12

The lib would have to do the repeat routine. Using as alias list makes sense with the Finder, but I’m not sure there are many (any?) places where you can’t use a «furl» instead of an alias.

OK, I’m convinced :smile:

I suppose there’s some argument for it. I’m always wary because it can be inconsistent.

The aim is to cover reasonably common requirements, not everything imaginable. I’m never going to keep up with your imagination. :wink:

Because of the way enumeration is done, they’re going to returned in that order automatically.


(Jim Underwood) #13

If you’re doing all that just to determine if a file exists, there is a much easier way:

--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on doesItemExist(pPosixPath) -- @Path @File @Finder @ASObjC
  (*  VER: 1.0    2017-03-02
---------------------------------------------------------------------------------
  PURPOSE:  Determine if the Path Actually Exists
  PARAMETERS:
    • pPOSIXPath    | text  | POSIX Path to check
  RETURNS:  boolean │  true IF the file/folder does exist; else false
  
  AUTHOR:  JMichaelTX
  BASED ON:  Script by Chris Stone & Shane Stanley
  REF:
    1. Does a NSURL Have a Valid Target
        https://lists.apple.com/archives/applescript-users/2017/Mar/msg00010.html
        
—————————————————————————————————————————————————————————————————————————————————
*)
  ##  Requires:  use framework "Foundation"
  
  local myNSURL, doesItExistBool
  
  --- Expand tilde (~) in Path (if it exists) ---
  set pPosixPath to (current application's NSString's stringWithString:pPosixPath)'s stringByExpandingTildeInPath
  
  --- GET NSURL & DETERMINE IF IT ACTUALLY EXISTS ---
  set myNSURL to current application's |NSURL|'s fileURLWithPath:pPosixPath
  set doesItExistBool to (myNSURL's checkResourceIsReachableAndReturnError:(missing value)) as boolean
  
  return doesItExistBool
end doesItemExist
--~~~~~~~~~~~~~~~ END OF handler doesItemExist ~~~~~~~~~~~~~~~~~~~~~~~~~


(Ed Stockly) #14

Doing all that to move a file to another folder with a unique name. If an item already exists with that name, then add a “-1,” or increment there of, to the end of the file name stub.

Shane has completely resolved the issue. :smile: The script checks if an item exists with that name, and if it does give the a unique name before moving.


(Ed Stockly) #15

But here’s the thing, I’m updating all these scripts that are passing aliases to all the handlers and apps.

The best examples are things like open file and read file, they get aliases in variables in all my scripts (seemed like a good idea at the time).

So now I either have to coerce every file reference I get to alias, or go back and look for (and hope I find) all the times I pass an alias in a variable that’s to a handler, command or app and rewrite the call or coerce to alias there.

Your right, I can use other forms of file reference everywhere in new scripts, and I will, but for now I’ve got these legacy scripts that I’m hoping to update with as little pain as possible.


(Shane Stanley) #16

But does it make any difference which you pass? If you have:

read x

it works exactly the same whether x is an alias or a «class furl». I haven’t checked all possibilities, but you might find you don’t have to change any code at all.


(Ed Stockly) #17

Wouldn’t I just have to coerce the file reference into a class furl instead? (I see no advantage to that over alias, and I prefer no chevron characters )

I tried all the result types from objects of and none work without conversion to alias (or presumably furl) or rewriting the call.

But if it returned alias list…

use script "Filemanagerlib"
use scripting additions
set aFolder to choose folder

set fileList to objects of aFolder ¬
   searching subfolders false ¬
   include invisible items false ¬
   include folders false ¬
   include files true ¬
   result type files list

repeat with myFile in fileList
    set openFile to open for access myFile with write permission
   close access openFile
end repeat

(Shane Stanley) #18

No — that’s what it is already. Try this and see:

set x to choose file name
class of x

Are you sure? The snippet you posted works fine as it is (under 10.11 and later).


(Shane Stanley) #19

FileManagerLib version 2.1 is now available. See:


(Ed Stockly) #20

I’m noticing an unexpected behavior in the exists object command.

If you use it directly in an if then statement, it correctly follows the conditional, but true/false doesn’t appear in the result window. Instead it’s some NSURL path (see comments). Whether it’s true or false.

use scripting additions
use script "FileManagerLib" version "2.2.1"
set targetFile to "Foo"
set targetFile to choose file
exists object targetFile
if exists object targetFile then
	--(NSURL) file:///Users/user/Documents/Pics/Reverie-2.jpg
	beep
else
	beep
	--(NSURL) file:///Foo
end if