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
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
Thanks for that Chris. I tried it in Terminal and all looks good. Will stick it into a variable now.
Building on the info provided by Chris:
~~~ VER: 1.0 2018-06-22 ~~~
Get Count of Files in Folder [Example].kmmacros (2.8 KB)
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.
#!/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
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:
#!/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")
Thanks for sharing.
Actually, both the JXA and AppleScript scripts return all items in a folder, including:
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:
.path
, by making your selection of NSURL keys // 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:
(() => {
'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.
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
I really love your coding style.
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.
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.
PS if shell scripting is something that you use a lot, it might be worth looking at this route into experimenting with Haskell:
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)