Select by tag

Hi Everyone,

Is there anyway that I am able to filter by tag? So I have a folder that contains 100 folder and 50 them have a red tag. I want to only delete the 50 folders that are tagged red? I have looked around the forum and can only find how to add a red tag but not how to filter by tag?

Thank you in advance for any help :slightly_smiling_face:

This example macro will create a list of items that do (and do not) have the red tag. Be sure to update the directory path in the for each action where it currently says ~/Library/CloudStorage/Dropbox to point to your directory.

This is just an example, but from it you should be able to figure out how to use the list(s) to do more stuff. If not, just let me know and I can provide some more info. :+1:t2:

Download Macro(s): Create list of tagged and untagged items.kmmacros (7.2 KB)

Macro Image (Click to expand/collapse)

Macro Notes (Click to expand/collapse)
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
  • The user must ensure the macro is enabled.
  • The user must also ensure the macro's parent macro-group is enabled.
System Information (Click to expand/collapse)
  • macOS 13.6.3
  • Keyboard Maestro v11.0.2
5 Likes

Works great! I would never have worked that out especially the beginning with the apple script! Apple script is like me trying to read Chinese! i modified it a bit like you said to make it do what I want it to do and have uploded my version in case it helps someone else trying to delete a certain coloured tag.

Thank you very much for your help (again)... :slightly_smiling_face:

Regards,
Margate
Create list of tagged and untagged items.kmmacros (5.1 KB)

Glad to help! I would suggest changing all the variables from debug__ to local__. I only use the debug__ when testing/building macros since they are global variables and persist after execution so I can look at their contents if needed. Once the macro is finished, I convert them to local variables by swapping debug__ to local__ (I even have a macro that does this for me automatically). So your end result would look like the screenshot below.

Macro Screenshot (click to expand/collapse)

The AppleScript is just to ensure any debug__ variables I placed in a macro that I share publicly are deleted so as not to clutter other user’s environments.

1 Like

Understood and done. Thank's again :slightly_smiling_face:

1 Like

FWIW, a Javascript for Automation Objective-C solution. The key function inside the script is:

// finderTags :: FilePath -> [String]
const finderTags = fp => {
    const
        ref = Ref(),
        error = $(),
        aURL = $.NSURL.fileURLWithPath(
            fp
        ),
        result = aURL.getResourceValueForKeyError(
            ref,
            $.NSURLTagNamesKey,
            error
        );
    return ref[0].isNil() ? (
        []
    ) : ObjC.deepUnwrap(ref[0])
};

Download Macro(s): List of tagged items.kmmacros (13 KB)

Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 14.3.1
  • Keyboard Maestro v11.0.2

This is great! Thanks for sharing. Quick question, though, is there a way to make it do a recursive search, including files in any subdirectories?

Glad that you like it.

I see that your solution is recursive; there should be a way to do it. I could look into it at some point. Thank you for mentioning it.

1 Like

@cdthomer, I created a listDirectoryRecursive function to accomplish what you asked. Does that work for you ?

// listDirectoryRecursive :: FilePath -> [FilePath]
const listDirectoryRecursive = fp => 
    ObjC.unwrap(
        $.NSFileManager.defaultManager
        .subpathsAtPath(
            $(fp).stringByStandardizingPath
        ))
    .map(ObjC.unwrap)

Download Macro(s): List of tagged items (recursive).kmmacros (14 KB)

Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 14.3.1
  • Keyboard Maestro v11.0.2

Nice, it works great, thanks!

1 Like

Apple suggests using subPathsOfDirectory for macOS 10.5+, so I post an updated macro:

Javascript source:

return (() => {
    "use strict";

    // Copyright (c) 2023 Gabriel Scalise
    // Twitter - @unlocked2412

    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    // SOFTWARE.

    // jxaContext :: IO ()
    const jxaContext = () => {
        // main :: IO ()
        const main = () => {
            const
                // dir = filePath("~/Dropbox"),
                // colorName = "Yellow";
                dir = filePath(kmvar.localDir),
                colorName = kmvar.localColorName;
            return bindLR(
                listDirectoryRecursiveLR(dir)
            )(
                compose(
                    Right,
                    filter(compose(elem(colorName), finderTags)),
                    map(combine(dir))
                )
            )
        };

        // GENERICS ----------------------------------------------------------------
        // listDirectoryRecursiveLR :: FilePath -> Either String (IO [FilePath])
        const listDirectoryRecursiveLR = fp => {
            const
                e = $(),
                ns = $.NSFileManager.defaultManager
                .subpathsOfDirectoryAtPathError(
                    $(fp).stringByStandardizingPath,
                    e
                );
            return ns.isNil() ? (
                Left(ObjC.unwrap(e.localizedDescription))
            ) : Right(
                ObjC.deepUnwrap(ns)
            )
        };

        // JS For Automation -------------------------------------------
        // finderTags :: FilePath -> [String]
        const finderTags = fp => {
            const
                ref = Ref(),
                error = $(),
                aURL = $.NSURL.fileURLWithPath(
                    fp
                ),
                result = aURL.getResourceValueForKeyError(
                    ref,
                    $.NSURLTagNamesKey,
                    error
                );
            return ref[0].isNil() ? (
                []
            ) : ObjC.deepUnwrap(ref[0])
        };

        // JS Prelude --------------------------------------------------
        // 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 = lr =>
            // Bind operator for the Either option type.
            // If lr has a Left value then lr unchanged,
            // otherwise the function mf applied to the
            // Right value in lr.
            mf => "Left" in lr ? (
                lr
            ) : mf(lr.Right);

        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            // A pair of values, possibly of
            // different types.
            b => ({
                type: "Tuple",
                "0": a,
                "1": b,
                length: 2,
                *[Symbol.iterator]() {
                    for (const k in this) {
                        if (!isNaN(k)) {
                            yield this[k];
                        }
                    }
                }
            });

        // all :: (a -> Bool) -> [a] -> Bool
        const all = p =>
            // True if p(x) holds for every x in xs.
            xs => [...xs].every(p);

        // and :: [Bool] -> Bool
        const and = xs =>
            // True unless any value in xs is false.
            [...xs].every(Boolean);

        // any :: (a -> Bool) -> [a] -> Bool
        const any = p =>
            // True if p(x) holds for at least
            // one item in xs.
            xs => [...xs].some(p);

        // combine (</>) :: FilePath -> FilePath -> FilePath
        const combine = fp =>
            // The concatenation of two filePath segments,
            // without omission or duplication of "/".
            fp1 => Boolean(fp) && Boolean(fp1) ? (
                "/" === fp1.slice(0, 1) ? (
                    fp1
                ) : "/" === fp.slice(-1) ? (
                    fp + fp1
                ) : `${fp}/${fp1}`
            ) : fp + fp1;

        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            // A function defined by the right-to-left
            // composition of all the functions in fs.
            fs.reduce(
                (f, g) => x => f(g(x)),
                x => x
            );

        // cycle :: [a] -> Generator [a]
        const cycle = function* (xs) {
            // An infinite repetition of xs,
            // from which a prefix of arbritrary
            // length may be drawn.
            const n = xs.length;
            let i = 0;

            while (true) {
                yield xs[i];
                i = (1 + i) % n;
            }
        };

        // elem :: Eq a => a -> [a] -> Bool
        const elem = x =>
            // True if xs contains an instance of x.
            xs => {
                const t = xs.constructor.name;

                return "Array" !== t ? (
                    xs["Set" !== t ? "includes" : "has"](x)
                ) : xs.some(eq(x));
            };

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = a =>
            // True when a and b are equivalent in the terms
            // defined below for their shared data type.
            b => {
                const t = typeof a;

                return t !== typeof b ? (
                    false
                ) : "object" !== t ? (
                    "function" !== t ? (
                        a === b
                    ) : a.toString() === b.toString()
                ) : (() => {
                    const kvs = Object.entries(a);

                    return kvs.length !== Object.keys(b).length ? (
                        false
                    ) : kvs.every(([k, v]) => eq(v)(b[k]));
                })();
            };

        // filePath :: String -> FilePath
        const filePath = s =>
            // The given file path with any tilde expanded
            // to the full user directory path.
            ObjC.unwrap(
                ObjC.wrap(s).stringByStandardizingPath
            );

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = p =>
            // The elements of xs which match
            // the predicate p.
            xs => [...xs].filter(p);

        // first :: (a -> b) -> ((a, c) -> (b, c))
        const first = f =>
            // A simple function lifted to one which applies
            // to a tuple, transforming only its first item.
            ([x, y]) => Tuple(f(x))(y);

        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f => {
            // The catamorphism on trees. A summary
            // value obtained by a depth-first fold.
            const go = tree => f(
                root(tree)
            )(
                nest(tree).map(go)
            );

            return go;
        };

        // length :: [a] -> Int
        const length = xs =>
            // Returns Infinity over objects without finite
            // length. This enables zip and zipWith to choose
            // the shorter argument when one is non-finite,
            // like cycle, repeat etc
            "Node" !== xs.type ? (
                "GeneratorFunction" !== xs.constructor
                .constructor.name ? (
                    xs.length
                ) : Infinity
            ) : lengthTree(xs);

        // lengthTree :: Tree a -> Int
        const lengthTree = tree =>
            // The count of nodes in a given tree.
            foldTree(
                () => xs => xs.reduce(
                    (a, x) => a + x, 1
                )
            )(tree);

        // list :: StringOrArrayLike b => b -> [a]
        const list = xs =>
            // xs itself, if it is an Array,
            // or an Array derived from xs.
            Array.isArray(xs) ? (
                xs
            ) : Array.from(xs || []);

        // map :: (a -> b) -> [a] -> [b]
        const map = f =>
            // The list obtained by applying f
            // to each element of xs.
            // (The image of xs under f).
            xs => [...xs].map(f);

        // nest :: Tree a -> [a]
        const nest = tree => {
            // Allowing for lazy (on-demand) evaluation.
            // If the nest turns out to be a function –
            // rather than a list – that function is applied
            // here to the root, and returns a list.
            const xs = tree.nest;

            return "function" !== typeof xs ? (
                xs
            ) : xs(root(tree));
        };

        // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
        const on = f =>
            // e.g. groupBy(on(eq)(length))
            g => a => b => f(g(a))(g(b));

        // or :: [Bool] -> Bool
        const or = xs =>
            xs.some(Boolean);

        // repeat :: a -> Generator [a]
        const repeat = function* (x) {
            while (true) {
                yield x;
            }
        };

        // root :: Tree a -> a
        const root = tree =>
            // The value attached to a tree node.
            tree.root;

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

    return JSON.stringify(
        jxaContext()
    );
})();

Download Macro(s): List of tagged items (recursive).kmmacros (17 KB)

Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 14.3.1
  • Keyboard Maestro v11.0.2
2 Likes