Templater, System Commands, File Lists, Weather, and Git

Today I took a gander at all the forum posts with the Templater tag. Most of them were already closed for replies, but I realized I had some comments that might appply to the following:

As to the question about how to pass a parameter to a User System Command Function in Templater, I don’t think it is possible. However, I stopped using them in favor a User Script Function that allows me to build the command programmatically before passing it to the function to execute.

// In: cmd = a complete command to run in the command line shell. It is up 
//           to the user to make certain the command is properly formatted 
//           for their platform
// Out: an object with two properties:
//      out = the trimmed content the command sent to standard out.
//      err = the trimmed content the command sent to standard error.
async function sh(cmd)
{
   const { promisify } = require('util');
   const exec = promisify(require('child_process').exec);
   const result = await exec(cmd);
   return {out: result.stdout.trim(), err: result.stderr.trim()};
}
module.exports = sh;

With the full power of the terminal (command line) available it opens up a lot of templating opportunities. For instance, one complex template I created built a table of contents using head to extract the first few lines of notes and wc to include their word counts. While that might be a bit complex to present here, I offer some concrete examples of the function in action below.

For the first example, the following template builds a simple list of the markdown files found at the root level of a vault:

<%*
const root = app.vault.adapter.getBasePath();
const escSpace = root.replace(/(\s)/g, '\\$1');  //escape the space characters
const list = await tp.user.sh(`ls ${escSpace}/*.md`);
const files = list.out.split('\n');
files.forEach(file => {tR += `   + ${file}\n`;});
-%>

sample output

   + /Users/ninjineer/Documents/Test/2022.calendar.md
   + /Users/ninjineer/Documents/Test/Book.md
   + /Users/ninjineer/Documents/Test/Counting words.md
   + /Users/ninjineer/Documents/Test/New-Day.md
   + /Users/ninjineer/Documents/Test/TVShow.md
   + /Users/ninjineer/Documents/Test/Terrible Idea 2.md
   + /Users/ninjineer/Documents/Test/Terrible Idea First.md

The above assumes you’re using MacOS or Linux. For Windows, the same template should look something like the following (note this code is untested as I do not have Obsidian running on a Windows computer):

<%*
const root = app.vault.adapter.getBasePath();
const list = await tp.user.sh(`dir "${root}\\*.md" /B`);
const files = list.out.split('\r\n');
files.forEach(file => {tR += `   + ${file}\r\n`;});
-%>

With a little more effort, once could also turn that file list into a list of links, like for a MOC:

<%*
const root = app.vault.adapter.getBasePath();
const escSpace = root.replace(/(\s)/g, '\\$1');
const list = await tp.user.sh(`ls ${escSpace}/*.md`);
const files = list.out.split('\n');
files.forEach(file => {
   const parts = file.split('/');
   const name = parts[parts.length - 1].slice(0,-3)
   tR += `   + [[${name}]]\n`;
});
-%>

sample output

   + [[2022.calendar]]
   + [[Book]]
   + [[Counting words]]
   + [[New-Day]]
   + [[TVShow]]
   + [[Terrible Idea 2]]
   + [[Terrible Idea First]]

Another example comes from the post about inserting weather forcasts. The template for doing that in my location looks like:

<%*
const w = await tp.user.sh(`curl "https://wttr.in/Pohnpei?TuF0"`);
tR += '```\n' + w.out + '\n```\n';
-%>

sample output

Weather report: Pohnpei

                Overcast
       .--.     +78(86) °F     
    .-(    ).   ← 2 mph        
   (___.__)__)  14 mi          
                0.1 in

I’ll leave you with one final example that occurred to me as I wrote this post. Since my vault is also a git repository, I periodically have to jump over to the terminal and commit my changes. With this sh function, I can use a template to automate this task:

<%*
let msg = await tp.system.prompt("Git Commit Message","");
if (msg != null && msg.length > 1)
{
   const root = app.vault.adapter.getBasePath();
   const escSpace = root.replace(/(\s)/g, '\\$1');
   const c = await tp.user.sh(`cd ${escSpace} && git add --all && git commit -m "${msg}"`)
   console.log(c.out);
}
-%>

Note, I directed the output from the command to the console, but I could have just as easily funneled it into a note, if I wanted to keep a log of such commits for some reason.

I hope these examples trigger fresh idea in others. Thanks.

9 Likes

Your solution is really cool, I’m definitely bookmarking it for possible future use. Thank you!

As to the question about how to pass a parameter to a User System Command Function in Templater, I don’t think it is possible.

It is! Sorry, I forgot to post the solution in the forum thread before it got automatically closed. Here is the answer in Obsidian Discord. In short: you use the $parameter syntax on Linux and %parameter% syntax on Windows, and pass the parameter value into the function within the template like this: tp.user.function({ parameter: value }).

Here is actual example of a User System Command Function I named getYoutubeMeta used for getting Youtube video metadata through youtube-dl:

Linux:

youtube-dl $ytLink --skip-download --dump-json

Windows:

youtube-dl %ytLink% --skip-download --dump-json

And that’s how you then call it within a simple example of a note template, together with parsing the resulting JSON to get two example bits of video information (title and channel) into the frontmatter:

<%* let link = await tp.system.prompt("Link");
let youtubeMeta = JSON.parse(await tp.user.getYoutubeMeta({ ytLink: link }));
let title = youtubeMeta.title;
let channel = youtubeMeta.channel;
-%>

---
title: "<% title %>"
channel: "<% channel %>"
---

(The actual getYoutubeMeta function call bit is tp.user.getYoutubeMeta({ ytLink: link }) in case a newbie reading this gets confused. You can get the full list of metadata youtube-dl has access to here.)

2 Likes

This is such a brilliant solution. Thank you so much. Will surely be using it :smile: