如何从VSCode扩展中运行系统命令

46

我试图创建一个简单的VSCode扩展程序,当我打开一个文件夹时运行一系列命令。基本上这些命令将配置我们的开发环境。我已经开始创建模板并运行了VSCode提供的示例,但我不清楚如何运行系统命令。感激任何帮助或指向与此主题相关的文档。

4个回答

42

您的扩展环境可以访问node.js库,因此您可以使用child_process或任何辅助库来执行命令:

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

16
我们应该这样做吗?没有优雅的VSCode API可以处理这个问题吗?比如,如果用户突然关闭了VSCode怎么办?这是否意味着我的扩展外部进程可能会一直运行下去? - Michael
2
不,这是一个child_process(子进程),此时当VSCode进程终止时,任何子进程也将终止。 - MatBee
5
很遗憾,这不是子进程的工作原理。请参考:https://dev59.com/6Woy5IYBdhLWcg3wdd1v - Domi
有没有一种方法可以做到这一点,但输出结果最终出现在内置的VS Code终端中(或者是使用vscode.window.createTerminal('终端名称')创建的新终端)? - SamTheProgrammer
@Domi 谢谢。我没看到那个。 - SamTheProgrammer
显示剩余3条评论

25
一种替代方案是使用Terminal API,如果您需要进程完全可观察和用户可控,则这是最佳选择。
最大的缺点:Terminal API尚未提供一种内省其内部运行的进程的方法。
如果您实际上想在终端中运行进程,现在唯一安全的方法是使用两层方法,其中您启动一个包装器进程,该进程依次启动和监视实际进程(通过命令行参数输入)。
我们自己制作了TerminalWrapper。
  • 在我们的第一种方法中,包装器使用了socket.io连接,允许与扩展进行通信和控制。
  • 在我们的第二种方法中,我们简化了操作,改用bash -c(非交互式 shell),并使用文件观察器获取结果。这样做更容易,但在进程完成后,用户将无法使用终端窗口(因为它是非交互式的)。出错几率更小,不需要满足socket.io依赖。
实现细节:
  1. 在我们的扩展中,我们使用TerminalWrapper在包装器进程中运行命令,并等待文件包含结果。
  2. 包装器进程在此处。它将结果写入文件。
  3. 使用示例此处
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})`);
}

第二种方法的最大缺点是我们现在需要存在bash,然而(i)我们有一个依赖检查器,如果你没有它会警告你并解释如何获取它,(ii)使用统一的 shell,使运行命令变得更容易,因为我们现在有了一个相当强大的统一特性集,我们知道我们可以依靠它,而不仅仅能使用常见的命令执行语法,(iii)我们甚至可以运行*.sh文件而不必担心。
介绍:VSCode 终端 API 以下所有图像和摘录都是从官方示例存储库中直接复制粘贴的:
创建终端并在其中运行命令
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'");
}));

终端激活事件

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

......还有更多!......

Terminal API Sample Commands

(<3代表对VSCode团队及其辛勤工作的赞赏。)


1
有没有办法知道正在运行的终端是否繁忙?谢谢。 - Nameless
终端API目前非常有限。除了查询它是否打开或关闭、其PID、尺寸和屏幕截图中看到的其他内容之外,它不允许您查询任何关于其状态的信息。您甚至可以使用onDidWriteTerminalData事件获取输出(但是这可能永远不会成为稳定API的一部分,如此处所述)。遗憾的是,除非您将命令包装在观察应用程序中(如我上面所提出的),否则无法知道当前终端内运行的内容或是否正在运行。 - Domi

13
我做的是创建一个基于 Promise 的实用函数,用于运行带有 child_process 的所有 shell 命令。
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);
        });
    });

获取当前目录

const currentDir = await execShell('pwd');

获取当前git分支名称

const branchName = await execShell('git rev-parse --abbrev-ref HEAD');

哇,这真的很完整。谢谢,它帮了我很多。 - SamTheProgrammer

1
在我的情况下,必须在用户终端内执行命令。但我希望在命令成功或失败时得到通知。这种方法不太通用,但可以让我更加掌控。
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);
    });
}

我的想法是按顺序修改两个文件。第二个文件大多数情况下都是虚拟的。但是当我收到第二个文件的触发器时,我知道第一个文件已经完成更新。


在快速连续执行多个命令的某些情况下,可能会出现竞争条件。在这种情况下,我会向触发文件输出递增的序列号,并等待该序列号可用后再读取数据文件。 - anandbibek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接