I am trying to create a simple VSCode extension to run a set of commands when I open a folder. Basically these commands will set up our development environment. I have started off creating the boilerplace and ran through the example that VSCode provided but I am not clear how to run system commands. Appreciate any help or point me to some documentation about this topic.
4 Answers
Your extension environment has access to node.js libraries, so you can just use child_process or any helper libraries to execute commands:
const cp = require('child_process')
cp.exec('pwd', (err, stdout, stderr) => {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (err) {
console.log('error: ' + err);
}
});
7 Comments
One alternative could be to use the Terminal API which is the best option if you have the need for the process to be fully observable and controllable to/by the user.
Biggest downside: The Terminal API does not yet offer a way to introspect the processes that run inside of it.
As of 9/2024, the Terminal API has some events that you can hook into to observe what is going on inside the Terminal.
The most reliable way to run an observable process in the terminal, is a two-layer approach, where you start a wrapper process that in turn launches and observes the actual process (taken in via command line args).
Our self-made TerminalWrapper
We tried this ourselves.
In our first approach, the wrapper used a
socket.ioconnection that allows for communication with and control by the extension.In our second approach, we simplified and instead created the terminal using
bash -c(non-interactive shell), and used a file watcher to get the results instead. Easier this way but after the process is done, the user won't be able to use the Terminal window (because its non-interactive). A lot less error-prone and does not require fulfilling socket.io dependency.
Implementation Details
- In our extension, we use a
TerminalWrapperwhich runs the command inside a wrapper process, and waits for a file to contain the results. - The wrapper process is here. It writes the result to a file.
- Usage example here:
const cwd = '.';
const command = `node -e "console.log('hi!');"`;
const { code } = await TerminalWrapper.execInTerminal(cwd, command, {}).waitForResult();
if (code) {
const processExecMsg = `${cwd}$ ${command}`;
throw new Error(`Process failed with exit code ${code} (${processExecMsg})`);
}
Biggest downside with the second approach is that we now need bash to be present, however (i) we do have a dependency checker that will warn you if you don't and explain how to get it, and (ii) using a unified shell, makes running commands a lot easier, as we now have a pretty strong unified feature set, we know we can rely on, rather than only being able to use common command execution syntax, and (iii) we can even run *.sh files without having to worry.
Introducing: VSCode Terminal API
All of the following imagery and excerpts are just straight up copy-and-paste'd from their official sample repository:
Create a terminal and run a command in it
context.subscriptions.push(vscode.commands.registerCommand('terminalTest.createAndSend', () => {
const terminal = vscode.window.createTerminal(`Ext Terminal #${NEXT_TERM_ID++}`);
terminal.sendText("echo 'Sent text immediately after creating'");
}));
Terminal activation event
vscode.window.onDidChangeActiveTerminal(e => {
console.log(`Active terminal changed, name=${e ? e.name : 'undefined'}`);
});
TerminalQuickPickItem
function selectTerminal(): Thenable<vscode.Terminal | undefined> {
interface TerminalQuickPickItem extends vscode.QuickPickItem {
terminal: vscode.Terminal;
}
const terminals = <vscode.Terminal[]>(<any>vscode.window).terminals;
const items: TerminalQuickPickItem[] = terminals.map(t => {
return {
label: `name: ${t.name}`,
terminal: t
};
});
return vscode.window.showQuickPick(items).then(item => {
return item ? item.terminal : undefined;
});
}
...and a lot more!...
(<3 for the VSCode team and their hard work.)
2 Comments
onDidWriteTerminalData event (however that will probably never be part of the stable API as discussed here). Sadly there is no way to know if or what is currently running inside of a terminal, unless you wrap your command in an observer application as I propopsed above.What I did was to create a promise based utility function to run all shell command with child_process
import * as cp from "child_process";
const execShell = (cmd: string) =>
new Promise<string>((resolve, reject) => {
cp.exec(cmd, (err, out) => {
if (err) {
return reject(err);
}
return resolve(out);
});
});
To get current directory
const currentDir = await execShell('pwd');
To get current git branch name
const branchName = await execShell('git rev-parse --abbrev-ref HEAD');
1 Comment
In my case, it was mandatory that I execute my commands within the user terminal. But I wanted to get notified on command success or failure. It is less generic but gives me more control.
import * as vscode from 'vscode';
import * as fs from 'fs';
async function executeAndRead(command: string): Promise<string> {
ensureTerminalExists();
const terminal = await selectTerminal();
if (terminal) {
terminal.sendText(command + ` > ${outputFile} || pwd > ${triggerFile}`);
return waitForFileUpdate(outputFile, triggerFile);
}
return Promise.reject('Could not select terminal');
}
async function waitForFileUpdate(outputFile: string, triggerFile: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const watcher = fs.watch(triggerFile);
watcher.on('change', () => {
watcher.close();
fs.readFile(outputFile, 'utf8', (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
watcher.on('error', reject);
});
}
The idea is that I'm modifying two files in sequence. The second is mostly a dummy. But I know that first file has finished updating when I get the 2nd file's trigger.
