Skip to content

Instantly share code, notes, and snippets.

@jakecraige
Last active July 28, 2026 21:38
Show Gist options
  • Select an option

  • Save jakecraige/f03d32bf1df1b3636e3745147ac9d811 to your computer and use it in GitHub Desktop.

Select an option

Save jakecraige/f03d32bf1df1b3636e3745147ac9d811 to your computer and use it in GitHub Desktop.
A (partial) replacement for obsidian-shellcommands for direct use with Obsidian QuickAdd
/**
* This script is intended to be used with QuickAdd to support spawning custom subprocesses
* which have context about the current note being viewed in Obsidian.
*
* It functions as a much more incomplete version of https://community.obsidian.md/plugins/obsidian-shellcommands
* But is sufficient for my needs and helps me avoid extra plugin dependencies.
*
* Usage:
* 1. Add this file as a User Script for QuickAdd (https://quickadd.obsidian.guide/docs/Choices/MacroChoice/#add-a-user-script-command)
* 2. Create a "Macro Choice" in QuickAdd
* 3. In its options, select this user script to add it as a step in the macro.
* 4. Use the settings to configure the command to be executed.
* - Uses QuickAdd's Format Syntax: https://quickadd.obsidian.guide/docs/FormatSyntax/
* - Tip: If you want user input (or much more) for the commands, see how to do so in their docs.
* - Custom variables provided for use in command template:
* workingDir (absolute path), vaultDir (absolute path),
* fileName, fileExtension,
* filePath (relative to working directory), fileDir (relative to working directory)
* 5. That's all! Active it from QuickAdd's. I've also included the commandOutput in the variables so this could be chained with
* other scripts if desired, though it's not a feature I've used yet.
*
* SECURITY NOTICE: Obviously running arbitrary shell commands with dynamic input can be dangerous and allow for fairly arbitrary
* access to your device. Be careful about what inputs you use to this. If they can be controlled by third-parties, that exposes
* you to serious risk.
*
* Cheers, Jake Craige (jakecraige)
* License: MIT
**/
const notice = (msg) => new Notice(msg, 5000);
const log = (msg) => console.log('shell-command:', msg);
module.exports = {
entry: start,
settings: {
name: "Shell Commands",
author: "Jake Craige",
options: {
"Command Template": {
type: "format",
placeholder: "echo {{VALUE:filePath}}",
description: "Command to execute within your default shell. Available variables: workingDir, vaultDir, fileName, fileExtension, filePath, fileDir"
},
"Show Output": {
type: "toggle",
defaultValue: true,
description: "Show command output in a popup when complete"
},
"Working Directory For Execution": {
type: "dropdown",
defaultValue: "active-note-directory",
options: ["active-note-directory", "vault-root"],
description: "Which directory will the command be run from"
},
"Execution Timeout (ms)": {
type: "number",
defaultValue: 10000,
description: "Maximum time for the process to execute before being killed"
},
}
}
}
async function start(params, settings) {
const { app, quickAddApi, obsidian, variables } = params;
const executionTimeout = parseInt(settings["Execution Timeout (ms)"], 10);
const showOutput = settings["Show Output"];
const workingDirSelection = settings["Working Directory For Exectution"];
const commandFormat = settings["Command Template"];
if (commandFormat === "") {
notice("No command template provided, set one in the settings");
return;
}
const vaultAbsolutePath = getVaultAbsolutePath(app, obsidian);
const activeFile = app.workspace.getActiveFile();
const path = require("path");
let workingDir = null;
switch (workingDirSelection) {
case "active-note-directory":
if (!activeFile) {
notice("No active file -- cannot use active-note-directory working directory option.");
return;
}
workingDir = path.join(vaultAbsolutePath, activeFile.parent.path);
break;
case "vault-root":
workingDir = vaultAbsolutePath;
break;
default:
throw new Error("Invalid working directory dropdown selection: " + workingDirSelection);
}
variables.workingDir = workingDir;
variables.vaultDir = vaultAbsolutePath;
if (activeFile) {
variables.fileName = activeFile.basename;
variables.fileExtension = activeFile.extension;
variables.filePath = path.relative(variables.workingDir, path.join(vaultAbsolutePath, path.normalize(activeFile.path)));
variables.fileDir = path.relative(variables.workingDir, path.join(vaultAbsolutePath, path.normalize(activeFile.parent?.path))) || ".";
}
log("variables:")
log(" workingDir: " + variables.workingDir);
log(" vaultDir: " + variables.vaultDir);
log(" fileName: " + variables.fileName);
log(" filePath: " + variables.filePath);
log(" fileDir: " + variables.fileDir);
const formattedCommand = await quickAddApi.format(commandFormat); // variables automatically available to format func
try {
variables.commandOutput = null;
variables.commandError = null;
let output = await spawn(formattedCommand, { cwd: workingDir, timeout: executionTimeout, shell: true });
log("Command success: " + output);
notice("Command successful.");
variables.commandOutput = output;
if (showOutput) {
notice("Output: " + output);
}
} catch (err) {
variables.commandError = err;
log("Command failed.")
log(err);
notice(err);
notice("Command failed.");
}
}
function getVaultAbsolutePath(app, obsidian) {
// Borrowed from here: https://github.com/Taitava/obsidian-shellcommands/blob/main/src/Common.ts#L48-L56
const adapter = app.vault.adapter;
if (adapter instanceof obsidian.FileSystemAdapter) {
return adapter.getBasePath();
}
throw new Error("Could not retrieve vault path. No DataAdapter was found from app.vault.adapter.");
}
function spawn(command, args) {
const childProcess = require("child_process");
return new Promise(function(resolve, reject) {
log("Running command:")
log(" command: " + command);
log(" args: " + JSON.stringify(args));
try {
let child = childProcess.spawn(command, args);
let scriptOutput = "";
child.stdout.setEncoding('utf8');
child.stdout.on('data', function(data) {
data=data.toString();
scriptOutput+=data;
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', function(data) {
data=data.toString();
scriptOutput+=data;
});
child.on('close', function(code) {
if (code === 0) {
resolve(scriptOutput);
} else {
console.log("reject error");
reject({code, out: scriptOutput});
}
});
} catch (err) {
console.log("weird error");
reject({code: null, err});
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment