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

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;
})();

1 Like

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?

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?

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.

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

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.

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

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

@complexpoint

I decided to upgrade to Catalina and realized that the following stopped working so that I cannot run JavaScripts from. the terminal this way.

Do you know of any work around? I tried googling a solution but couldn’t come up with anything. At the time I am running the script directly from TaskPaper, but the solution is not optimal since I like how I could just set a script to generate the TaskPaper file, transform that to proper multi markdown, and then convert that to XeLaTex-> PDF. Not a big deal to run the script in TaskPaper first, but as I said, I liked the magic of just having the script print the reports automatically with what seemed to be no user input.

I don’t have a Catalina system here, but it might be worth preceding the osascript line with the line:

#!/bin/bash

to specify use of the Bash shell.

(My understanding is that the default shell has changed in Catalina, and is now zsh, unless you specify something else)

Are you using Keyboard Maestro at all ?

(An Execute JS action in that, perhaps ?)

I tried and actually bought a license for it, but I never used the program enough to actually use all the power of the application. I use Alfred and the workflows, but not sure if you can accomplish something like that using Alfred workflows.

I also misspoke when I said that the script doesn’t run. It runs, but it doesn’t interact with TaskPaper as it should. I get an error about the file not being there (even when I know is there since I create the file myself). Anyway. Maybe I can figure out more about it later.

Hey ComplexPoint. Sorry for resurrecting this old issue. Since this happened, I have been manually creating this report. Since I run this twice a week I have been thinking about just having a CRON job that does everything for me and save me 5 min a week.

The error I get is,

Target directory not found at ~/.....

Although I know that the directory is there. I tried using the whole path instead of ~/My/File/etc/.... but it doesn’t work. Any ideas why?

EDIT:
I just hacked the source and changed the function that checks to see if the directory exits or not to a return true; and it works. No clue what caused that particular function to fail since I upgraded to Catalina back in the day (Now using Big Sur.) Anyway, thank you!

I’ll try to take a look on a Big Sur system at the weekend.

If this helps, this is the area with problems

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

And I changed it to this to make the script work.

    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = strPath => {
        const ref = Ref();
        return true;
    };

What value does this return on your system ?

(for example, from Script Editor)

(() => {
    "use strict";

    // main :: IO ()
    const main = () =>
        doesDirectoryExist("~/Desktop");

    // --------------------- GENERIC ---------------------


    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };

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

true

:slight_smile:

Would you be able to show me (perhaps by direct message) a copy of your version of the script, with the filePath strings you are using ?

ComplexPoint. This is kind of embarrassing. I was using the script that you included in your reply to my question of how to get this to work in bash. In that code, this was the function to check if the directory was valid. And for a while, the script worked in my mac.

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

But when I updated to Catalina, it stopped working. I didn’t notice that in the original example you used the new function,

    // doesDirectoryExist :: FilePath -> IO Bool
    const doesDirectoryExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
            .fileExistsAtPathIsDirectory(
                $(fp)
                .stringByStandardizingPath, ref
            ) && ref[0];
    };

And that function DOES work with Catalina and Big Sur. I should have checked to see the original submission instead of copying the code example to get this running in bash you later provided in the reply. Thank you very much for checking and taking the time to see this. Much appreciated!

1 Like

Good detective work : -)

1 Like