Counting Files in a Folder

Is there something in KM that can do this or Terminal that I can run with a shell script? I want to create a counter so that as my process works through the files, I get like 1/25, 2/25 etc so I can judge how long it will all take.

Thanks,

Jon

Hey Jon,

AppleScript:

set theFolder to alias ((path to home folder as text) & "test_directory:Many_Files_1000:")
tell application "Finder"
  set itemCount to count of (items of theFolder as alias list)
end tell

This gets progressively slower as the number of items goes over about 500.

The Shell:

'ls' -1 | wc -l

Using the shell is fine PROVIDED you don’t have any file bundles. Bundles are seen by the shell as folders.

If you want to know about the commands involved the see the man pages in the Terminal for:

ls
wc

man ls
man wc

-Chris

1 Like

Thanks for that Chris. I tried it in Terminal and all looks good. Will stick it into a variable now. :smile:

Building on the info provided by Chris:


MACRO:   Get Count of Files in Folder [Example]


#### DOWNLOAD:
<a class="attachment" href="/uploads/default/original/3X/f/d/fdd87cb6ed0dc81d1622d4b7e1e1b6808aec0084.kmmacros">Get Count of Files in Folder [Example].kmmacros</a> (2.8 KB)
**Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.**

---



![image|503x653](upload://bJRLmlPCLUdCMdhvNzlojy9Rr3N.jpg)

---

### Shell Script

```bash
#!/bin/bash
# Use when you need to use StdIn from the KM Action in a command that does not accept StdIn

myFolder=$(cat)

'ls' -1 "$myFolder" | wc -l
```
1 Like

One follow-up on this. The Bash ls command will list both files and folders.
If you want just a count of files, and exclude all hidden files, then you need to use the Bash find command, like this:

image

#!/bin/bash
# Use when you need to use StdIn from the KM Action in a command that does not accept StdIn

myFolder=$(cat)

find "$myFolder"  -maxdepth 1 -type f -not -path '*/\.*' | wc -l

For more info, see Bash Find Command Document

Another variation would be to get the length of the list returned by this Javascript for Automation function:

// getDirectoryContents :: FilePath -> IO [FilePath]
const getDirectoryContents = strPath =>
    ObjC.deepUnwrap(
        $.NSFileManager.defaultManager
        .contentsOfDirectoryAtPathError(
            $(strPath)
            .stringByStandardizingPath, null
        )
    );

(Or an Applescript equivalent )

Tho of course, you will have to decide what files to count. This AS version for example, will include invisible dot-prefixed files in the list, which you may or may not want to filter out of your count;

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

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


getDirectoryContents("~/Desktop")
1 Like

Thanks for sharing.

Actually, both the JXA and AppleScript scripts return all items in a folder, including:

  • Hidden items
  • Subfolders
  • Aliases
  • Symlinks
  • and, of course, regular files

For more fine-grained counts, (files of particular types, folders etc)

we can filter a list of files decorated with properties like $.NSURLIsDirectoryKey, $.NSURLTypeIdentifierKey etc.

There is a full list of these keys at: https://developer.apple.com/documentation/foundation/nsurlresourcekey?language=objc

For example, listing / counting just the directories in a folder:

(() => {
    'use strict';

    const main = () => {
        const strPath = '~/Desktop/'

        // For available keys, see:
        // https://developer.apple.com/documentation/foundation/nsurlresourcekey?language=objc

        // nsKeys :: [NS Identifier]
        const nsKeys = [
            // $.NSURLAddedToDirectoryDateKey,
            // $.NSURLContentModificationDateKey,
            $.NSURLIsDirectoryKey,
            $.NSURLTypeIdentifierKey
        ];

        return showJSON(
            folderFilesWithKeyValues(strPath, nsKeys)
            .filter(x => x.IsDirectory)
        );
    };

    // folderFilesWithKeyValues :: String -> [$.NSURLConstant] -> [Dictionary]
    const folderFilesWithKeyValues = (strPath, nsKeys) => {
        const
            uw = ObjC.unwrap,
            ks = nsKeys.map(uw),
            fp = $(strPath).stringByStandardizingPath;

        return uw(
            $.NSFileManager.defaultManager[
                'contentsOfDirectoryAtURL' +
                'IncludingPropertiesForKeysOptionsError'
            ]($.NSURL.fileURLWithPathIsDirectory(fp, true), ks, 1 << 2, null)
        ).map(x => ks.reduce(
            (a, k) => {
                const ref = $();
                return (
                    x.getResourceValueForKeyError(
                        ref, k, null
                    ),
                    Object.assign(a, {
                        [k.slice(5, -3)]: uw(ref)
                    })
                )
            }, {
                path: uw(x.path)
            }
        ))
    };

    // showJSON :: a -> String
    const showJSON = x => JSON.stringify(x, null, 2);

    return main();
})();

Thanks for sharing all this. It is very helpful but I'm unable to mod your script to just get a text list of regular files:

NSURLIsRegularFileKey
Key for determining whether the resource is a regular file, as opposed to a directory or a symbolic link. Returned as a Boolean NSNumber object (read-only).

I don't want a JSON output. I want, what I suspect most KM users would want, is a text list (one line per path) of the found files.

Could you please mod your script, or provide instructions/comments so that those of us with lesser JXA skills could modify it?

Thanks.

a text list (one line per path)

can be derived by mapping [Dict] -> [String]
and folding [String] -> String

For example, at the end of the main() above:

   ...

    return folderFilesWithKeyValues(strPath, nsKeys)
        .filter(x => x.IsDirectory)
        .map(x => x.path)
        .join('\n')
};

Thanks. That solves one problem. But it appears your code is hard-coded to only return directories. So even though I have changed:

FROM:

        const nsKeys = [
            // $.NSURLAddedToDirectoryDateKey,
            // $.NSURLContentModificationDateKey,
            $.NSURLIsDirectoryKey,
            $.NSURLTypeIdentifierKey
        ];

TO:

       const nsKeys = [
            $.NSURLIsRegularFileKey
        ];

It still returns directories.

So, again, how do we:

with NO directories.
Thanks.

It still returns directories.

Sounds like you are still applying the x => x.IsDirectory filter in:

    return folderFilesWithKeyValues(strPath, nsKeys)
        .filter(x => x.IsRegularFile)  // <-- FILTERING HERE
        .map(x => x.path)
        .join('\n')

folderFilesWithKeyValues() returns a list of Dictionaries

 // folderFilesWithKeyValues :: String -> [$.NSURLConstant] -> [Dictionary]
    const folderFilesWithKeyValues = (strPath, nsKeys) =>

You can:

  1. Specify what keys those dictionaries have, in addition to .path, by making your selection of NSURL keys
  2. Filter the list down, in terms of those keys, to what you want
  3. Map and fold to the report format that you need:
    // For available keys, see:
    // https://developer.apple.com/documentation/foundation/nsurlresourcekey?language=objc

    // 1. Specify what keys those dictionaries have:

    // nsKeys :: [NS Identifier]
    const nsKeys = [
        // $.NSURLAddedToDirectoryDateKey,
        // $.NSURLContentModificationDateKey,
        // $.NSURLIsDirectoryKey,
        $.NSURLIsRegularFileKey,
        $.NSURLTypeIdentifierKey
    ];

    return folderFilesWithKeyValues(strPath, nsKeys)

    // 2. Filtering the list down, in terms of those keys, to what you want

        .filter(x => x.IsRegularFile)  

   //  3. Mapping and folding to the report format that you need:

        .map(x => x.path)
        .join('\n')

OK, Thanks for all your help @ComplexPoint.
I think we now have a JXA script that will:

  1. Return a text list (one path/line) of ONLY FILES in a folder
    • EXCLUDES: Folders, Symlinks
    • Includes: Alias files
    • File list can be eliminated by just commenting out one line of code
  2. Count of those files
  3. Get the source folder path from a KM Variable: Local__SourceFolder
(() => {
    'use strict';

    const main = () => {
        
        var app = Application.currentApplication()
        app.includeStandardAdditions = true
        var kmInst = app.systemAttribute("KMINSTANCE");
        var kmeApp = Application("Keyboard Maestro Engine")
 
        // --- GET Source Folder Path from KM Variable (or use default) ---
        var sourceFolderPath = kmeApp.getvariable("Local__SourceFolder",  {instance: kmInst}) || "~/Documents";

        // For available keys, see:
        // https://developer.apple.com/documentation/foundation/nsurlresourcekey?language=objc

        // nsKeys :: [NS Identifier]
        const nsKeys = [
            // $.NSURLAddedToDirectoryDateKey,
            // $.NSURLContentModificationDateKey,
           // $.NSURLIsDirectoryKey,
            $.NSURLIsRegularFileKey,
            $.NSURLTypeIdentifierKey
        ];
        
        // ##JMTX:  Move from Return to Variable
        var fileList = folderFilesWithKeyValues(sourceFolderPath, nsKeys)
            // filter for only "Regular" Files, which EXCLUDE Folders & Symlinks
            .filter(x => x.IsRegularFile)    
            .map(x => x.path);
            
        // --- Output an Easy-To-Read One Path/Line List of Files ---
        //     (comment out next line if all you want is count)
        console.log(fileList.join('\n'));  // ##JMTX ADD
        
        // --- Return Count of Files Found ---
        return fileList.length;            // ##JMTX Chg
        
    };

    // folderFilesWithKeyValues :: String -> [$.NSURLConstant] -> [Dictionary]
    const folderFilesWithKeyValues = (sourceFolderPath, nsKeys) => {
        const
            uw = ObjC.unwrap,
            ks = nsKeys.map(uw),
            fp = $(sourceFolderPath).stringByStandardizingPath;

        return uw(
            $.NSFileManager.defaultManager[
                'contentsOfDirectoryAtURL' +
                'IncludingPropertiesForKeysOptionsError'
            ]($.NSURL.fileURLWithPathIsDirectory(fp, true), ks, 1 << 2, null)
        ).map(x => ks.reduce(
            (a, k) => {
                const ref = $();
                return (
                    x.getResourceValueForKeyError(
                        ref, k, null
                    ),
                    Object.assign(a, {
                        [k.slice(5, -3)]: uw(ref)
                    })
                )
            }, {
                path: uw(x.path)
            }
        ))
    };

    // showJSON :: a -> String
    const showJSON = x => JSON.stringify(x, null, 2);

    return main();
})();
try
	set f to system attribute "Local__SourceFolder" as POSIX file as alias
on error
	set f to path to desktop folder
end try


tell application "System Events" to set flist to ¬
	POSIX path of files of f ¬
	whose visible is true ¬
	and type identifier is not "public.symlink"


set beginning of flist to count flist

set my text item delimiters to linefeed
flist as text

My guess is that the JXA script will be quicker and obviously more familiar to those at home with JavaScript. But I think this AppleScript achieves a similar outcome, i.e. excludes folders and symlinks, but includes alias files.

1 Like

Using System Events seems a good approach in AS ...

We could also get at the NS properties of paths through the ObjC bridge from Applescript too, for example by writing a function like:

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

(See fuller code below)

But System Events is always to hand, and plenty fast enough in practice.


For reference: using a library which includes a fileStatus function

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


property _ : missing value

--  EDIT THESE FILEPATHS TO MATCH YOUR SYSTEM:

-- Library files at: https://github.com/RobTrew/prelude-applescript
property jsonPath : "~/prelude-applescript/asPreludeDict.json"
property asPreludeLibPath : "~/prelude-applescript/asPrelude.applescript"

on run
    if _ is missing value then set _ to prelude(asPreludeLibPath)
    set fp to "~/Desktop/"
    
    script isRegular
        on |λ|(x)
            tell _ to set lrStatus to fileStatus(filePath(fp & x))
            if |Left| of lrStatus is missing value then
                if "." is not first character of x and ¬
                    "NSFileTypeRegular" = NSFileType of |Right| of lrStatus then
                    {x}
                else
                    {}
                end if
            else
                {}
            end if
        end |λ|
    end script
    
    tell _ to length of concatMap(isRegular, getDirectoryContents(fp))
end run


-- prelude :: FilePath -> Script
on prelude(filePath)
    -- (path to a library file which returns a 'me' value)
    
    set ca to current application
    set {bln, int} to (ca's NSFileManager's defaultManager's ¬
        fileExistsAtPath:((ca's NSString's stringWithString:filePath)'s ¬
            stringByStandardizingPath) isDirectory:(reference))
    
    if (bln and (int ≠ 1)) then
        set strPath to filePath
        run script (((ca's NSString's ¬
            stringWithString:strPath)'s ¬
            stringByStandardizingPath) as string)
    end if
end prelude
3 Likes

I really love your coding style.

2 Likes

That's very generous of you – any credit goes, I think, to Haskell and especially to http://haskellformac.com/

It seems to be a common experience that experimenting with Haskell soon has quite a big effect on the way one thinks about these things – constructing values from generic 'Lego' bricks turns out to make scripting rather faster, and pushes down the edge case bug-count too.

Above all, I think it just provides a very helpful model for problem-solving.

2 Likes

It all makes sense now. Language—verbal or coding—is a reflection on the way we think. From your style of coding, I got the overwhelming impression that you were a mathematician. Having had a quick look at the Haskell link, it would seem to make sense that I might have that notion.

Definitely going to investigate Haskell further. Thanks for pointing me to it.

2 Likes

PS if shell scripting is something that you use a lot, it might be worth looking at this route into experimenting with Haskell:

1 Like

Your script fails running from Script Debugger 7 7.0.3 (7A55) on macOS 10.12.6, with this error:

Expected function name, command name or function name but found property.

on this line:

run script (((ca's NSString's ¬
      stringWithString:strPath)'s ¬
      stringByStandardizingPath) as string)