Select Random Files from Folder(s)

Hello,

I often need to select a few dozen random files out of thousands. As I don't know how to automate this, I do this job manually. It's so inefficient.

How can I select certain number of files from (a) specific folder(s)? How can I possibly automate this job?

Best,
Kyu

The basics of something which choses a different subset of files on each run might look something like this:

N random files from folder.kmmacros (22.0 KB)
nRandomFiles

JS Source

(() => {
    'use strict';

    const main = () => {

        // nRandomFilesFromFolderLR :: FilePath -> n ->
        //                              Either String [FilePath]
        const nRandomFilesFromFolderLR = (fp, n) =>
            bindLR(
                doesDirectoryExist(fp) ? (
                    Right(getDirectoryContents(fp))
                ) : Left('Folder not found: ' + fp),
                xs => n <= xs.length ? (
                    Right(take(n, knuthShuffle(xs)))
                ) : Left(
                    'Less than ' + n.toString() + ' files in \n' + fp
                )
            );

        const
            kme = Application('Keyboard Maestro Engine'),
            lrFiles = nRandomFilesFromFolderLR(
                kme.getvariable('folderPath'),
                parseInt(kme.getvariable('fileCount') || '1', 10),
            );
        return lrFiles.Left || unlines(lrFiles.Right);
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = strPath => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };

    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = (m, n) =>
        m <= n ? iterateUntil(
            x => n <= x,
            x => 1 + x,
            m
        ) : [];

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

    // iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
    const iterateUntil = (p, f, x) => {
        const vs = [x];
        let h = x;
        while (!p(h))(h = f(h), vs.push(h));
        return vs;
    };

    // knuthShuffle :: [a] -> [a]
    const knuthShuffle = xs => {
        const swapped = (iFrom, iTo, xs) =>
            xs.map(
                (x, i) => iFrom !== i ? (
                    iTo !== i ? x : xs[iFrom]
                ) : xs[iTo]
            );
        return enumFromTo(0, xs.length - 1)
            .reduceRight((a, i) => {
                const iRand = randomRInt(0, i);
                return i !== iRand ? (
                    swapped(i, iRand, a)
                ) : a;
            }, xs);
    };


    // randomRInt :: Int -> Int -> Int
    const randomRInt = (low, high) =>
        low + Math.floor(
            (Math.random() * ((high - low) + 1))
        );

    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = (n, xs) =>
        'GeneratorFunction' !== xs.constructor.constructor.name ? (
            xs.slice(0, n)
        ) : [].concat.apply([], Array.from({
            length: n
        }, () => {
            const x = xs.next();
            return x.done ? [] : [x.value];
        }));

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // MAIN ---
    return main();
})();

Thank you for your help, but it doesn't work... or I possibly don't understand how to utilize it.

:slight_smile: That snippet just provides the basics of drawing N rabbits out of a hat.

For something that you can use directly, you may need to tell us a little more about what you mean by:
'I often need to select ...'

Does that just mean extend the Finder selection to N randomly chosen files ?

Or perhaps do something further with each of those files in the next stage of your automation ?

but it doesn't work...

What exactly have you tried to do ?
and what was the result ?

It would be helpful to see a screenshot of the values which you gave to the folderPath and fileCount variables.

If, for example, what you wanted was to set the Finder selection (in the front Finder window) to N randomly chosen files (so that you could drag them elsewhere, or copy/print, etc) then you could perhaps try something like this:

Select N random files in front Finder window.kmmacros (23.7 KB)

selectInFinder

(() => {
    'use strict';

    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            finder = Application('Finder'),
            folder = getFinderDirectory(),
            lrResult = bindLR(
                nRandomFilesFromFolderLR(
                    folder,
                    parseInt(kme.getvariable('fileCount') || '1', 10),
                ),
                xs => {
                    try {
                        // Effect
                        finder.reveal(map(x => Path(folder + x), xs));
                    } catch (e) {
                        // Value
                        return Left(e.message)
                    }
                    // Value
                    return Right(xs);
                }
            );
        return lrResult.Left || unlines(lrResult.Right);
    };

    // RANDOM FILE SELECTION ------------------------------

    // nRandomFilesFromFolderLR :: FilePath -> n ->
    //                              Either String [FilePath]
    const nRandomFilesFromFolderLR = (folderPath, n) => {
        const fp = filePath(folderPath) + '/';
        return bindLR(
            doesDirectoryExist(fp) ? (
                Right(
                    filter(
                        // Except folders and invisibles
                        x => !doesDirectoryExist(fp + x) && !('.' === x[0]),
                        getDirectoryContents(fp)
                    )
                )
            ) : Left('Folder not found: ' + fp),
            xs => n <= xs.length ? (
                Right(take(n, knuthShuffle(xs)))
            ) : Left(
                'Less than ' + n.toString() + ' files in \n' + fp
            )
        );
    };

    // FINDER ---------------------------------------------

    // getFinderDirectory :: IO FilePath
    const getFinderDirectory = () =>
        Application('Finder')
        .insertionLocation()
        .url()
        .slice(7);

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = strPath => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && 1 !== ref[0];
    };

    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = strPath => {
        const ref = Ref();
        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(strPath)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };

    // enumFromTo :: Int -> Int -> [Int]
    const enumFromTo = (m, n) =>
        m <= n ? iterateUntil(
            x => n <= x,
            x => 1 + x,
            m
        ) : [];

    // filePath :: String -> FilePath
    const filePath = s =>
        ObjC.unwrap(ObjC.wrap(s)
            .stringByStandardizingPath);

    // filter :: (a -> Bool) -> [a] -> [a]
    const filter = (f, xs) => xs.filter(f);

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

    // iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
    const iterateUntil = (p, f, x) => {
        const vs = [x];
        let h = x;
        while (!p(h))(h = f(h), vs.push(h));
        return vs;
    };

    // knuthShuffle :: [a] -> [a]
    const knuthShuffle = xs => {
        const swapped = (iFrom, iTo, xs) =>
            xs.map(
                (x, i) => iFrom !== i ? (
                    iTo !== i ? x : xs[iFrom]
                ) : xs[iTo]
            );
        return enumFromTo(0, xs.length - 1)
            .reduceRight((a, i) => {
                const iRand = randomRInt(0, i);
                return i !== iRand ? (
                    swapped(i, iRand, a)
                ) : a;
            }, xs);
    };

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) =>
        (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // randomRInt :: Int -> Int -> Int
    const randomRInt = (low, high) =>
        low + Math.floor(
            (Math.random() * ((high - low) + 1))
        );

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

    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = (n, xs) =>
        'GeneratorFunction' !== xs.constructor.constructor.name ? (
            xs.slice(0, n)
        ) : [].concat.apply([], Array.from({
            length: n
        }, () => {
            const x = xs.next();
            return x.done ? [] : [x.value];
        }));

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // MAIN ---
    return main();
})();

Yes, that's exactly what I need. I just downloaded the macro you had attached, opened a download folder in Finder, and hit 'Try', but nothing happened. :disappointed_relieved:

I hate to take much of your time but I think I don't really know how to use your macro. Could you help me with it?

" hit 'Try' "

Not entirely sure that I know what you mean by that:

  • The Try action option of a contextual menu for one of the actions ?
  • Or the "Run Selected Macro" button at the top ?

Are you running a pre-Sierra version of macOS ?

" nothing happened "

  • What were you expecting to see ?
  • Did you check for changes in the selection pattern (which files were selected) in the front Finder window ?

It worked for me... I just needed to add it to a function key.
Just a note, this will select FILES not FOLDERS - incase this is your issue.

The 'Try' button at the bottom of the window, between Edit and Record.

Yes, El Capitan.

Of course, I did. fileCount is 5 for now.

Mine looks different than yours. The fonts. What does it mean?

El Capitan is the issue :slight_smile: ES6 JavaScript is only available on Sierra onwards.

I'll sketch an Applescript version later on.

Tho before I do that, you may find that this version, which contains a mechanical translation from ES6 JS to ES5 JS (via https://babeljs.io/en/repl) works on your El Capitan system:

ES5 Select N random files in front Finder window.kmmacros (23.8 KB)

Babel down-translation from ES6 to ES5

(function() {
    'use strict';

    var main = function main() {
        var kme = Application('Keyboard Maestro Engine'),
            finder = Application('Finder'),
            folder = getFinderDirectory(),
            lrResult = bindLR(nRandomFilesFromFolderLR(folder,
                    parseInt(kme.getvariable('fileCount') || '1', 10)),
                function(xs) {
                    try {
                        // Effect
                        finder.reveal(map(function(x) {
                            return Path(folder + x);
                        }, xs));
                    } catch (e) {
                        // Value
                        return Left(e.message);
                    }
                    // Value
                    return Right(xs);
                });
        return lrResult.Left || unlines(lrResult.Right);
    };

    // RANDOM FILE SELECTION ------------------------------

    // nRandomFilesFromFolderLR :: FilePath -> n ->
    //                              Either String [FilePath]
    var nRandomFilesFromFolderLR = function nRandomFilesFromFolderLR(folderPath, n) {
        var fp = filePath(folderPath) + '/';
        return bindLR(doesDirectoryExist(fp) ? Right(filter(
            // Except folders and invisibles
            function(x) {
                return !doesDirectoryExist(fp + x) && !('.' === x[0]);
            }, getDirectoryContents(fp))) : Left('Folder not found: ' + fp), function(xs) {
            return n <= xs.length ? Right(take(n, knuthShuffle(xs))) : Left('Less than ' + n.toString() + ' files in \n' + fp);
        });
    };

    // FINDER ---------------------------------------------

    // getFinderDirectory :: IO FilePath
    var getFinderDirectory = function getFinderDirectory() {
        return Application('Finder').insertionLocation().url().slice(7);
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    var Left = function Left(x) {
        return {
            type: 'Either',
            Left: x
        };
    };

    // Right :: b -> Either a b
    var Right = function Right(x) {
        return {
            type: 'Either',
            Right: x
        };
    };

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    var bindLR = function bindLR(m, mf) {
        return undefined !== m.Left ? m : mf(m.Right);
    };

    // doesFileExist :: FilePath -> IO Bool
    var doesFileExist = function doesFileExist(strPath) {
        var ref = Ref();
        return $.NSFileManager.defaultManager.fileExistsAtPathIsDirectory($(strPath).stringByStandardizingPath, ref) && 1 !== ref[0];
    };

    // doesDirectoryExist :: FilePath -> IO Bool
    var doesDirectoryExist = function doesDirectoryExist(strPath) {
        var ref = Ref();
        return $.NSFileManager.defaultManager.fileExistsAtPathIsDirectory($(strPath).stringByStandardizingPath, ref) && ref[0];
    };

    // enumFromTo :: Int -> Int -> [Int]
    var enumFromTo = function enumFromTo(m, n) {
        return m <= n ? iterateUntil(function(x) {
            return n <= x;
        }, function(x) {
            return 1 + x;
        }, m) : [];
    };

    // filePath :: String -> FilePath
    var filePath = function filePath(s) {
        return ObjC.unwrap(ObjC.wrap(s).stringByStandardizingPath);
    };

    // filter :: (a -> Bool) -> [a] -> [a]
    var filter = function filter(f, xs) {
        return xs.filter(f);
    };

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

    // iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
    var iterateUntil = function iterateUntil(p, f, x) {
        var vs = [x];
        var h = x;
        while (!p(h)) {
            h = f(h), vs.push(h);
        }
        return vs;
    };

    // knuthShuffle :: [a] -> [a]
    var knuthShuffle = function knuthShuffle(xs) {
        var swapped = function swapped(iFrom, iTo, xs) {
            return xs.map(function(x, i) {
                return iFrom !== i ? iTo !== i ? x : xs[iFrom] : xs[iTo];
            });
        };
        return enumFromTo(0, xs.length - 1).reduceRight(function(a, i) {
            var iRand = randomRInt(0, i);
            return i !== iRand ? swapped(i, iRand, a) : a;
        }, xs);
    };

    // map :: (a -> b) -> [a] -> [b]
    var map = function map(f, xs) {
        return (Array.isArray(xs) ? xs : xs.split('')).map(f);
    };

    // randomRInt :: Int -> Int -> Int
    var randomRInt = function randomRInt(low, high) {
        return low + Math.floor(Math.random() * (high - low + 1));
    };

    // showJSON :: a -> String
    var showJSON = function showJSON(x) {
        return JSON.stringify(x, null, 2);
    };

    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    var take = function take(n, xs) {
        return 'GeneratorFunction' !== xs.constructor.constructor.name ? xs.slice(0, n) : [].concat.apply([], Array.from({
            length: n
        }, function() {
            var x = xs.next();
            return x.done ? [] : [x.value];
        }));
    };

    // unlines :: [String] -> String
    var unlines = function unlines(xs) {
        return xs.join('\n');
    };

    // MAIN ---
    return main();
})();

El Capitan wasn't able to compile ES6 Javascript (the current standard), so the syntax highlight coloring and general rich text rendering wasn't displayed.

THANK YOU very much for your help!
Although it has a minor error, it works like magic!

The error is: it can't find the folder location when folder and/or volume name has space or/and Korean letter.

Sorry for the late respond. Thanks!

That will need a bit of unpacking before I can understand or do much about it :slight_smile:

What actually happens ?

To look at this we would need at the very least

  • a sample file path and
  • a screen pic of what actually happens.

'Tell less, show more' always works better.

As you already had helped me a lot, I just didn't want to take more of your time :sweat_smile:

My results:

I wanted to select files from a folder in my external HDD named 'Kyu HD'. Failed :arrow_down:

I changed the volume's name into 'KyuHD' and tried it again. Successful :arrow_down:

And, in the same folder, I made a new folder with a Korean name, tested on it. Failed :arrow_down:

Conclusion: It can't find any folder that has space and/or a Korean letter in its name.

Perfect documentation – thanks !

(and that helps me to fix my getFinderDirectory() function – so helpful to me too)

We need to reverse the URL encoding – first sketch below:

ES5 Select N random files in front Finder window.kmmacros (23.8 KB)

Updated JS Source

(function() {
    'use strict';

    var main = function main() {
        var kme = Application('Keyboard Maestro Engine'),
            finder = Application('Finder'),
            folder = getFinderDirectory(),
            lrResult = bindLR(nRandomFilesFromFolderLR(folder,
                    parseInt(kme.getvariable('fileCount') || '1', 10)),
                function(xs) {
                    try {
                        // Effect
                        finder.reveal(map(function(x) {
                            return Path(folder + x);
                        }, xs));
                    } catch (e) {
                        // Value
                        return Left(e.message);
                    }
                    // Value
                    return Right(xs);
                });
        return lrResult.Left || unlines(lrResult.Right);
    };

    // RANDOM FILE SELECTION ------------------------------

    // nRandomFilesFromFolderLR :: FilePath -> n ->
    //                              Either String [FilePath]
    var nRandomFilesFromFolderLR = function nRandomFilesFromFolderLR(folderPath, n) {
        var fp = filePath(folderPath) + '/';
        return bindLR(doesDirectoryExist(fp) ? Right(filter(
            // Except folders and invisibles
            function(x) {
                return !doesDirectoryExist(fp + x) && !('.' === x[0]);
            }, getDirectoryContents(fp))) : Left('Folder not found: ' + fp), function(xs) {
            return n <= xs.length ? Right(take(n, knuthShuffle(xs))) : Left('Less than ' + n.toString() + ' files in \n' + fp);
        });
    };

    // FINDER -------------------------------------------

    // getFinderDirectory :: IO FilePath
    var getFinderDirectory = function getFinderDirectory() {
        return decodeURIComponent(
            Application('Finder').insertionLocation()
            .url().slice(7)
        );
    };

    // GENERIC FUNCTIONS --------------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    var Left = function Left(x) {
        return {
            type: 'Either',
            Left: x
        };
    };

    // Right :: b -> Either a b
    var Right = function Right(x) {
        return {
            type: 'Either',
            Right: x
        };
    };

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    var bindLR = function bindLR(m, mf) {
        return undefined !== m.Left ? m : mf(m.Right);
    };

    // doesFileExist :: FilePath -> IO Bool
    var doesFileExist = function doesFileExist(strPath) {
        var ref = Ref();
        return $.NSFileManager.defaultManager.fileExistsAtPathIsDirectory($(strPath).stringByStandardizingPath, ref) && 1 !== ref[0];
    };

    // doesDirectoryExist :: FilePath -> IO Bool
    var doesDirectoryExist = function doesDirectoryExist(strPath) {
        var ref = Ref();
        return $.NSFileManager.defaultManager.fileExistsAtPathIsDirectory($(strPath).stringByStandardizingPath, ref) && ref[0];
    };

    // enumFromTo :: Int -> Int -> [Int]
    var enumFromTo = function enumFromTo(m, n) {
        return m <= n ? iterateUntil(function(x) {
            return n <= x;
        }, function(x) {
            return 1 + x;
        }, m) : [];
    };

    // filePath :: String -> FilePath
    var filePath = function filePath(s) {
        return ObjC.unwrap(ObjC.wrap(s).stringByStandardizingPath);
    };

    // filter :: (a -> Bool) -> [a] -> [a]
    var filter = function filter(f, xs) {
        return xs.filter(f);
    };

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

    // iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
    var iterateUntil = function iterateUntil(p, f, x) {
        var vs = [x];
        var h = x;
        while (!p(h)) {
            h = f(h), vs.push(h);
        }
        return vs;
    };

    // knuthShuffle :: [a] -> [a]
    var knuthShuffle = function knuthShuffle(xs) {
        var swapped = function swapped(iFrom, iTo, xs) {
            return xs.map(function(x, i) {
                return iFrom !== i ? iTo !== i ? x : xs[iFrom] : xs[iTo];
            });
        };
        return enumFromTo(0, xs.length - 1).reduceRight(function(a, i) {
            var iRand = randomRInt(0, i);
            return i !== iRand ? swapped(i, iRand, a) : a;
        }, xs);
    };

    // map :: (a -> b) -> [a] -> [b]
    var map = function map(f, xs) {
        return (Array.isArray(xs) ? xs : xs.split('')).map(f);
    };

    // randomRInt :: Int -> Int -> Int
    var randomRInt = function randomRInt(low, high) {
        return low + Math.floor(Math.random() * (high - low + 1));
    };

    // showJSON :: a -> String
    var showJSON = function showJSON(x) {
        return JSON.stringify(x, null, 2);
    };

    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    var take = function take(n, xs) {
        return 'GeneratorFunction' !== xs.constructor.constructor.name ? xs.slice(0, n) : [].concat.apply([], Array.from({
            length: n
        }, function() {
            var x = xs.next();
            return x.done ? [] : [x.value];
        }));
    };

    // unlines :: [String] -> String
    var unlines = function unlines(xs) {
        return xs.join('\n');
    };

    // MAIN ---
    return main();
})();

Thank you very much! Now it works perfectly! :laughing:
Would it be possible for me to make it work for other apps like Leap?