Querying one file, and writing a report to another – an example


#1

An illustrative script - querying one file for items now due, and writing a sorted list of them to another file:

(() => {
    'use strict';

    // FilePaths to query from and report to:
    const
        strSourceFilePath = '~/Notes/notes.taskpaper', // IN
        strReportFilePath = '~/Desktop/report.taskpaper'; // OUT

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = (editor, options) => {

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        const sortBy = (f, xs) =>
            xs.slice()
            .sort(f);

        // MAIN --------------------------------------------------------------

        // xs :: Date-sorted list of items now due
        const xs = sortBy(
            comparing(x => x.getAttribute('data-due', Date)),
            editor.outline.evaluateItemPath(
                '//@due <= [d] now and not @done'
            )
        );

        return xs.map(x => x.bodyString, xs)
            .join('\n');
    };

    // JXA CONTEXT------------------------------------------------------------

    // GENERIC FUNCTIONS -----------------------------------------------------

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

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

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

    // takeDirectory :: FilePath -> FilePath
    const takeDirectory = strPath =>
        strPath !== '' ? (() => {
            const xs = (strPath.split('/'))
                .slice(0, -1);
            return xs.length > 0 ? (
                xs.join('/')
            ) : '.';
        })() : '.';

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            $(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    // MAIN ------------------------------------------------------------------
    const
        tp3 = Application('TaskPaper'),
        maybeDoc = doesFileExist(strSourceFilePath) ? {
            nothing: false,
            just: tp3.open(filePath(strSourceFilePath))
        } : {
            nothing: true,
            msg: 'Source file not found at ' + strSourceFilePath
        },
        strFolder = takeDirectory(strReportFilePath),
        maybeReport = maybeDoc.nothing ? (
            maybeDoc
        ) : (() => {
            const strReport = maybeDoc.just.evaluate({
                script: taskpaperContext.toString(),
                withOptions: {}
            });

            return doesDirectoryExist(strFolder) ? {
                nothing: false,
                just: (
                    // Effect ------------------------------------------------
                    writeFile(strReportFilePath, strReport),
                    // Value -------------------------------------------------
                    'Report written to ' + strReportFilePath + '\n\n' +
                    strReport
                )
            } : {
                nothing: true,
                msg: 'Target directory not found at ' + strFolder
            };
        })();

    // Report (if source file and target folder both found), or message.
    return maybeReport.nothing ? maybeReport.msg : maybeReport.just;
})();


#2

Thank you very much for the code. Now, I have another question.

Every once in a while, the Birch library is mentioned. I had the impression that one could not query documents using Birch, but am I understanding you correctly when you say that this may be possible?

If that is possible, how would this example look?


#3

Also, you mentioned in one of our conversations that one could run this from a bash script.

This is relevant to me, because I have been collecting bash scripts for a while and combining this would be very important for me. So, if one would like to run this from the bash, would this be how it runs?

osascript -l JavaScript <<JXA_END 2>/dev/null

// Take the code from this example and paste it here!!

JXA_END

I see that one could also run the script from Keyboard Maestro and also run bash scripts from Maestro. Would that be the best way to get this done?


#4

FYI, It might be good to preserve the document tree instead of returning the results without the tabs. The file runs great. I love it! Again, thank you.


#5

For Bash use, the only edit required in the JXA is to replace the $(...) abbreviations in some of the file functions with the full ObjC.wrap( ... ). (I’ll change those in my library too - the clash in that context hadn’t occurred to me. The alternative is to manually place a backslash before each instance of the $ of $(strPath)).

The result would be something like :

#!/bin/bash
osascript -l JavaScript <<JXA_END 2>/dev/null
(() => {
    'use strict';

    // FilePaths to query from and report to:
    const
        strSourceFilePath = '~/Notes/notes.taskpaper', // IN
        strReportFilePath = '~/Desktop/report.taskpaper'; // OUT

    // TASKPAPER 3 CONTEXT ---------------------------------------------------
    const taskpaperContext = (editor, options) => {

        // comparing :: (a -> b) -> (a -> a -> Ordering)
        const comparing = f =>
            (x, y) => {
                const
                    a = f(x),
                    b = f(y);
                return a < b ? -1 : (a > b ? 1 : 0);
            };

        // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
        const sortBy = (f, xs) =>
            xs.slice()
            .sort(f);

        // MAIN --------------------------------------------------------------

        // xs :: Date-sorted list of items now due
        const xs = sortBy(
            comparing(x => x.getAttribute('data-due', Date)),
            editor.outline.evaluateItemPath(
                '//@due <= [d] now and not @done'
            )
        );

        return xs.map(x => x.bodyString, xs)
            .join('\n');
    };

    // JXA CONTEXT------------------------------------------------------------

    // GENERIC FUNCTIONS -----------------------------------------------------

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

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

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

    // takeDirectory :: FilePath -> FilePath
    const takeDirectory = strPath =>
        strPath !== '' ? (() => {
            const xs = (strPath.split('/'))
                .slice(0, -1);
            return xs.length > 0 ? (
                xs.join('/')
            ) : '.';
        })() : '.';

    // writeFile :: FilePath -> String -> IO ()
    const writeFile = (strPath, strText) =>
        $.NSString.alloc.initWithUTF8String(strText)
        .writeToFileAtomicallyEncodingError(
            ObjC.wrap(strPath)
            .stringByStandardizingPath, false,
            $.NSUTF8StringEncoding, null
        );

    // MAIN ------------------------------------------------------------------
    const
        tp3 = Application('TaskPaper'),
        maybeDoc = doesFileExist(strSourceFilePath) ? {
            nothing: false,
            just: tp3.open(filePath(strSourceFilePath))
        } : {
            nothing: true,
            msg: 'Source file not found at ' + strSourceFilePath
        },
        strFolder = takeDirectory(strReportFilePath),
        maybeReport = maybeDoc.nothing ? (
            maybeDoc
        ) : (() => {
            const strReport = maybeDoc.just.evaluate({
                script: taskpaperContext.toString(),
                withOptions: {}
            });

            return doesDirectoryExist(strFolder) ? {
                nothing: false,
                just: (
                    // Effect -------------------------------------------------
                    writeFile(strReportFilePath, strReport),
                    
                    // Value -------------------------------------------------
                    'Report written to ' + strReportFilePath + '\n\n' +
                    strReport
                )
            } : {
                nothing: true,
                msg: 'Target directory not found at ' + strFolder
            };
        })();

    // Report (if source file and target folder both found), or message.
    return maybeReport.nothing ? maybeReport.msg : maybeReport.just;
})();
JXA_END

#6

Under the hood Keyboard Maestro’s execute Javascript action is also using bash, but it does provide a very good wrapper - keyboard assignments, input and output variables, integration with other workflow elements.


#7

Sure. Various options there – including ancestral paths and or descendants etc, which would need adjustments to the query itself.

(Just including the tabs without the ancestral paths would, of course, risk the creation of confused lineages in the output file, with ‘children’ inadvertently indented under unrelated ‘parents’)


#8

If [querying documents in Birch] is possible, how would this example look ?

I’ll give some thought to how to create a standalone example that works in the context of a thread like this. A custom Keyboard Maestro action is one possibility if you have KM