Node.js + typescript 写一个命令批处理辅助工具

发表于 3年以前  | 总阅读数:405 次

1.背景

工作中遇到这样一些场景:在 php 混合 html 的老项目中写 css,但是 css 写着不太好用,然后就想使用预编译语言来处理,或者写上 ts。然后问题来了: 每次写完以后都要手动执行一次命令行把文件编译成 css 文件,然后又要再输入一行命令把 css 压缩添加前缀;或者把 ts 编译成 js,然后 js 压缩混淆。

那么有没有办法不用手动输入命令行呢?如果只是为了不手动输入的话,那么可以在 vscode 上安装 compile hero 插件,或者在 webstorm 上开启 file watch 功能。可惜的是这些工具或功能只能对当前文件做处理,处理编译后的文件又要手动去执行命令,不能连续监听或监听一次执行多个命令,比如 webstorm 的 file watch 监听了 sass 文件变化, 那么它不能再监听 css 变化去压缩代码,否则会无限编译下去。

那么为什么不使用 webpack 或者 rollup 之类的打包工具呢?首先是这些打包工具太重了不够灵活,毕竟原项目没到重构的时候, 要想使用新一点的技术,那么只能写一点手动编译一点了。

好在这些预编译语言都提供 cli 工具可在控制台输入命令行编译,那么完全可以把它们的命令关联起来,做一个批量执行的工具。其实 shell 脚本也可以完成这些功能, 但是其一:shell 在 windows 上的话只能在 git bash 里运行,在 cmd 控制台上不能运行,需要专门打开一个 git bash,少了一点便利性;其二:在 windows 上不能监听文件变化。那么既然 nodejs 能够胜任,那么用前端熟悉的 js 做那是再好不过了。

2.目标

1 . 基础功能

  • 通过控制台输入指令启动:获取控制台输入的命令
  • 运行命令
  • 运行多个命令
  • 通过指定配置文件执行

2 . 进阶功能

  • 前后生命周期

  • 遍历文件夹查找匹配运行

  • url 模板替换

  • 执行配置中的命令

  • 执行配置中的 js

  • 监听文件改动

  • 可通过指令显示隐藏 log

  • 可通过指令显示隐藏运行时间

  • npm 全局一次安装,随处执行

3 . 额外功能

  • 搜索文件或文件夹

  • 忽略大小写

  • 忽略文件夹

  • 帮助功能

  • 打开文件

  • 直接运行文件

  • 在打开资源管理器并选中目标文件

  • 在 cmd 控制台打开对应的路径

4 . 配置

  • 依次执行多个命令;
  • 生命周期回调
  • 忽略文件夹
  • 匹配规则
  • 匹配成功
  • 执行相应命令;
  • 执行相应 js;

ok,那么接下来进入正文吧(源码见底部 github 链接)。

3.基本功能

3.1 获取控制台输入的命令

首先是获取到控制台输入的命令,这里抽取出来做为一个工具函数。格式为以"="隔开的键值对,键名以"-"开头,值为空时设置该值为 true,变量之间用空格隔开。

// util.ts
/**
 * 获取命令行的参数
 * @param prefix 前缀
 */
export function getParams(prefix = "-"): { [k: string]: string | true } {
    return process.argv.slice(2).reduce((obj, it) => {
        const sp = it.split("=");
        const key = sp[0].replace(prefix, "");
        obj[key] = sp[1] || true;
        return obj;
    }, {} as ReturnType<typeof getParams>);
}

调用

console.log(getParams());

运行结果

3.2 运行单个命令

能获取到命令行参数那就好办了,接下来实现执行命令功能。

先实现一个简单的执行命令函数,这要用到 child_process 模块里的 exec 函数。

const util = require("util");
const childProcess = require('child_process');
const exec = util.promisify(childProcess.exec); // 这里把exec promisify

需要知道执行状态,所以把它封装一下,不能 try catch,出错就直接 reject 掉,避免后面的命令继续执行。

async function execute(cmd: string): Promise<string> {
    console.log('执行"' + cmd + '"命令...');
    const {stdout} = await exec(cmd);
    console.log('success!');
    console.log(stdout);
    return stdout;
}

设定命令参数为-command,且必须用”” ““包起来,多个则用“,”隔开

在工具中通过-command/-cmd=启用

调用

const args = getParams();
execute(args.command as string);

运行

3.3 运行多个命令

现在运行单个命令是没问题的,但是运行多个命令呢?

看结果可以发现:结果马上就报错了,把它改成顺序执行

async function mulExec(command: string[]) {
    for (const cmd of command) {
        await execute(cmd);
    }
}

运行

mulExec((args.command as string).split(","));

3.4 通过指定配置文件运行命令

在工具中通过-config/-c=设置配置的路径

这样通过命令行命令,执行相应的功能就完成了,但是可能会有情况下是要运行很多条命令的,每次都输入一长串命令就不那么好了,所以要添加一个通过配置文件执行的功能。

首先是定义配置文件格式。先来个最简单的

export interface ExecCmdConfig{
    command: string[]; // 直接执行命令列表
}

定义一下命令行配置文件变量名为-config

-config= 配置的路径

例如:cmd-que -config="test/cmd.config.js"

配置文件 test/cmd.config.js

module.exports = {
    command: [
        "stylus E:\\project\\cmd-que\\test\\test.styl",
        "stylus test/test1.styl",
    ]
};

加载配置文件

const Path = require("path");
const configPath = Path.resolve(process.cwd(), args.config);
try {
    const config = require(configPath);
    mulExec(config.command);
} catch (e) {
    console.error("加载配置文件出错", process.cwd(), configPath);
}

运行

搞定

4.进阶功能

到这里,一个简单的命令批量执行工具代码就已经基本完成了。但是需求总是会变的。

4.1 前后生命周期

为什么要添加生命周期?因为编译 pug 文件总是需要在编译完 js、css 之后,不可能总是需要手动给 pug 编译命令加上 debounce,所以加上结束的回调就很有必要了。

生命周期回调函数类型:

type execFn = (command: string) => Promise<string>;
export interface Config {
    beforeStart: (exec: execFn) => Promise<unknown> | unknown;
    beforeEnd: (exec: execFn) => Promise<unknown> | unknown;
}

代码

const Path = require("path");
const configPath = Path.resolve(process.cwd(), args.config);
try {
    const config = require(configPath);
    // beforeStart调用
    if (config.beforeStart) await config.beforeStart(execute);
    await mulExec(config.command);
    // beforeEnd调用
    config.beforeEnd && config.beforeEnd(execute);
} catch (e) {
    console.error("加载配置文件出错", process.cwd(), configPath);
}

配置文件 cmd.config.js

module.exports = {
    beforeStart() {
        console.time("time");
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("start");
                resolve();
            }, 1000);
        });
    },
    beforeEnd() {
        console.log("end");
        console.timeEnd("time");
    },
    command: [
        // "stylus D:\\project\\cmd-que\\test\\test.styl",
        "stylus E:\\project\\cmd-que\\test\\test.styl",
        "stylus test/test1.styl",
    ]
};

运行

4.2 遍历文件夹查找匹配运行

到现在,如果只是执行确定的命令,那么已经完全没问题了,但是有时候需要编译的文件会有很多,像 stylus、pug 这些可以直接编译整个文件夹的还好, 像 ts 的话就只能一个文件写一条命令,那也太麻烦了。

所以得增加一个需求:遍历文件夹查找目标文件, 然后执行命令的功能。

写一个遍历文件夹的函数:

// util.ts
const fs = require("fs");
const Path = require("path");

/**
 * 遍历文件夹
 * @param path
 * @param exclude
 * @param cb
 * @param showLog
 */
export async function forEachDir(
    path: string,
    exclude: RegExp[] = [],
    cb?: (path: string, basename: string, isDir: boolean) => true | void | Promise<true | unknown>,
    showLog = false,
) {
    showLog && console.log("遍历", path);
    try {
        const stats = fs.statSync(path);
        const isDir = stats.isDirectory();
        const basename = Path.basename(path);

        const isExclude = () => {
            const raw = String.raw`${path}`;
            return exclude.some((item) => item.test(raw));
        };
        if (isDir && isExclude()) return;


        const callback = cb || ((path, isDir) => undefined);
        const isStop = await callback(path, basename, isDir);

        if (!isDir || isStop === true) {
            return;
        }

        const dir = fs.readdirSync(path);
        for (const d of dir) {
            const p = Path.resolve(path, d);
            await forEachDir(p, exclude, cb, showLog);
        }
    } catch (e) {
        showLog && console.log("forEachDir error", path, e);
        // 不能抛出异常,否则遍历到System Volume Information文件夹报错会中断遍历
        // return Promise.reject(e);
    }
}

然后正则验证文件名,如果符合就执行命令

forEachDir("../test", [], (path, basename, isDir) => {
    if (isDir) return;
    const test = /\.styl$/;
    if (!test.test(basename)) return;
    return execute("stylus " + path);
});

运行

4.3 通过配置遍历文件夹

url 模板替换

看上面的执行情况可以看出,执行的每一条命令路径都是具体的,但是如果我们要遍历文件夹执行命令的话那么这样就不够用了。因为命令都是字符形式的无法根据情况改变,那么有两种方法解决这样的情况:

  1. 使用字符串模板替换掉对应的字符
  2. 使用js执行,根据传回的字符来替换掉对应的字符,再执行命令

现在实现一个模板替换的功能(模板来源于 webstorm 上的 file watcher 功能,有所增减)

export function executeTemplate(command: string, path = "") {
    const cwd = process.cwd();
    path = path || cwd;
    const basename = Path.basename(path);

    const map: { [k: string]: string } = {
        "\\$FilePath\\$": path, // 文件完整路径
        "\\$FileName\\$": basename, // 文件名
        "\\$FileNameWithoutExtension\\$": basename.split(".").slice(0, -1).join("."), // 不含文件后缀的路径
        "\\$FileNameWithoutAllExtensions\\$": basename.split(".")[0], // 不含任何文件后缀的路径
        "\\$FileDir\\$": Path.dirname(path), // 不含文件名的路径
        "\\$Cwd\\$": cwd, // 启动命令所在路径
        "\\$SourceFileDir\\$": __dirname, // 代码所在路径
    };
    const mapKeys = Object.keys(map);
    command = mapKeys.reduce((c, k) => c.replace(new RegExp(k, "g"), map[k]), String.raw`${command}`);
    return execute(command);
}

配置文件格式最终版如下:

type execFn = (command: string) => Promise<string>;

/**
 * @param eventName watch模式下触发的事件名
 * @param path 触发改动事件的路径
 * @param ext 触发改动事件的文件后缀
 * @param exec 执行命令函数
 */
type onFn = (eventName: string, path: string, ext: string, exec: execFn) => Promise<void>


type Rule = {
   test: RegExp,
   on: onFn,
   command: string[];
};

export type RuleOn = Omit<Rule, "command">;
type RuleCmd = Omit<Rule, "on">;
export type Rules = Array<RuleOn | RuleCmd>;

export interface Config {
   beforeStart: (exec: execFn) => Promise<unknown> | unknown;
   beforeEnd: (exec: execFn) => Promise<unknown> | unknown;
}

export interface ExecCmdConfig extends Config {
   command: string[]; // 直接执行命令列表 占位符会被替换
}


export interface WatchConfig extends Config {
   exclude?: RegExp[]; // 遍历时忽略的文件夹
   include?: string[] | string; // 要遍历/监听的文件夹路径 // 默认为当前文件夹
   rules: Rules
}

export function isRuleOn(rule: RuleOn | RuleCmd): rule is RuleOn {
   return (rule as RuleOn).on !== undefined;
}

实现

import {getParams, mulExec, forEachDir, executeTemplate} from "../src/utils";
import {isRuleOn, Rules} from "../src/configFileTypes";


(async function () {

    // 获取命令行参数
    const args = getParams();


    // 匹配正则
    async function test(eventName: string, path: string, basename: string, rules: Rules = []) {
        for (const rule of rules) {
            if (!rule.test.test(basename)) continue;
            if (isRuleOn(rule)) {
                await rule.on(
                    eventName,
                    path,
                    Path.extname(path).substr(1),
                    (cmd: string) => executeTemplate(cmd, path),
                );
            } else {
                await mulExec(rule.command, path);
            }
        }
    }

    // 遍历文件夹
    function foreach(
        path: string,
        exclude: RegExp[] = [],
        cb: (path: string, basename: string, isDir: boolean) => true | void | Promise<true | void>,
    ) {
        return forEachDir(path, exclude, (path: string, basename: string, isDir: boolean) => {
            return cb(path, basename, isDir);
        });
    }

    const Path = require("path");
    const configPath = Path.resolve(process.cwd(), args.config);
    try {
        const config = require(configPath);
        // beforeStart调用
        if (config.beforeStart) await config.beforeStart(executeTemplate);
        const include = config.include;
        // 设置默认路径为命令启动所在路径
        const includes = include ? (Array.isArray(include) ? include : [include]) : ["./"];
        const rules = config.rules;
        for (const path of includes) {
            await foreach(path, config.exclude, (path, basename) => {
                return test("", path, basename, rules);
            });
        }
        // beforeEnd调用
        config.beforeEnd && config.beforeEnd(executeTemplate);
    } catch (e) {
        console.error("加载配置文件出错", process.cwd(), configPath);
    }
})();

执行配置中的命令

配置文件如下:

// test-cmd.config.js
module.exports = {
    exclude: [
        /node_modules/,
        /\.git/,
        /\.idea/,
    ],
    rules: [
        {
            test: /\.styl$/,
            command: [
                "stylus <$FilePath$> $FileDir$\\$FileNameWithoutAllExtensions$.wxss",
                "node -v"
            ]
        }
    ]
};

运行结果

执行配置中的 js

module.exports = {
    beforeEnd(exec) {
        return exec("pug $Cwd$")
    },
    exclude: [
        /node_modules/,
        /\.git/,
        /\.idea/,
        /src/,
        /bin/,
    ],
    include: ["./test"],
    rules: [
        {
            test: /\.styl$/,
            on: async (eventName, path, ext, exec) => {
                if (eventName === "delete") return;
                const result = await exec("stylus $FilePath$");
                console.log("on", result);
            }
        },
        {
            test: /\.ts$/,
            on: (eventName, path, ext, exec) => {
                if (eventName === "delete") return;
                return exec("tsc $FilePath$");
            }
        },
    ]
};

运行结果

4.4 监听文件变动

在工具中通过-watch/-w 开启 需要与-config 搭配使用

监听文件变动 nodejs 提供了两个函数可供调用:

1 . fs.watch(filename[, options][, listener])

  • filename | |

  • options |

  • persistent 指示如果文件已正被监视,进程是否应继续运行。默认值: true。

  • recursive 指示应该监视所有子目录,还是仅监视当前目录。这适用于监视目录时,并且仅适用于受支持的平台(参见注意事项)。默认值: false。

  • encoding 指定用于传给监听器的文件名的字符编码。默认值: 'utf8'。

  • listener | 默认值: undefined。

  • eventType

  • filename |

  • 返回: <fs.FSWatcher>

  • 监视 filename 的更改,其中 filename 是文件或目录。

    2 . fs.watchFile(filename[, options], listener)

    • filename | |

    • options

    • bigint 默认值: false。

    • persistent 默认值: true。

    • interval 默认值: 5007。

    • listener

    • current <fs.Stats>

    • previous <fs.Stats>

    • Returns: <fs.StatWatcher>

    • 监视 filename 的更改。每当访问文件时都会调用 listener 回调。因为 watchFile 必须监听每个文件,所以选 watch 函数

      文档显示optionsrecursive参数为true时 监视所有子目录但是文档又说

      仅在 macOS 和 Windows 上支持 recursive 选项。当在不支持该选项的平台上使用该选项时,则会抛出 ERR_FEATURE_UNAVAILABLE_ON_PLATFORM 异常。

      在 Windows 上,如果监视的目录被移动或重命名,则不会触发任何事件。当监视的目录被删除时,则报告 EPERM 错误。

      所以我这里在判断子文件是否文件夹后,需要手动添加监听子文件夹

      import {getParams, mulExec, forEachDir, executeTemplate, debouncePromise} from "../src/utils";
      import {isRuleOn, RuleOn, Rules, WatchConfig} from "../src/configFileTypes";
      
      
      (async function () {
      
          // 获取命令行参数
          const args = getParams();
      
      
          /**
           * @param config 配置
           * @param watchedList watch列表用于遍历文件夹时判断是否已经watch过的文件夹
           */
          async function watch(config: WatchConfig, watchedList: string[]) {
              if (!config.rules) throw new TypeError("rules required");
              // 编辑器修改保存时会触发多次change事件
              config.rules.forEach(item => {
                  // 可能会有机器会慢一点 如果有再把间隔调大一点
                  (item as RuleOn).on = debouncePromise(isRuleOn(item) ? item.on : (e, p) => {
                      return mulExec(item.command, p);
                  }, 1);
              });
      
              const FS = require("fs");
              const HandleForeach = (path: string) => {
                  if (watchedList.indexOf(path) > -1) return;
      
                  console.log("对" + path + "文件夹添加监听\n");
      
                  const watchCB = async (eventType: string, filename: string) => {
                      if (!filename) throw new Error("文件名未提供");
                      const filePath = Path.resolve(path, filename);
                      console.log(eventType, filePath);
                      // 判断是否需要监听的文件类型
                      try {
                          const exist = FS.existsSync(filePath);
                          await test(exist ? eventType : "delete", filePath, filename);
                          if (!exist) {
                              console.log(filePath, "已删除!");
                              // 删除过的需要在watchArr里面去掉,否则重新建一个相同名称的目录不会添加监听
                              const index = watchedList.indexOf(filePath);
                              if (index > -1) {
                                  watchedList.splice(index, 1);
                              }
                              return;
                          }
                          // 如果是新增的目录,必须添加监听否则不能监听到该目录的文件变化
                          const stat = FS.statSync(filePath);
                          if (stat.isDirectory()) {
                              foreach(filePath, config.exclude, HandleForeach);
                          }
                      } catch (e) {
                          console.log("watch try catch", e, filePath);
                      }
      
                  };
      
                  const watcher = FS.watch(path, null, watchCB);
      
                  watchedList.push(path); // 记录已watch的
      
                  watcher.addListener("error", function (e: any) {
                      console.log("addListener error", e);
                  });
              };
      
              const include = config.include;
      
              const includes = include ? (Array.isArray(include) ? include : [include]) : ["./"];
      
              for (const path of includes) {
                  await foreach(path, config.exclude, (path, basename, isDir) => {
                      if (isDir) HandleForeach(path);
                  });
              }
          }
      
      
          // 匹配正则
          async function test(eventName: string, path: string, basename: string, rules: Rules = []) {
              for (const rule of rules) {
                  if (!rule.test.test(basename)) continue;
                  if (isRuleOn(rule)) {
                      await rule.on(
                          eventName,
                          path,
                          Path.extname(path).substr(1),
                          (cmd: string) => executeTemplate(cmd, path),
                      );
                  } else {
                      await mulExec(rule.command, path);
                  }
              }
          }
      
          // 遍历文件夹
          function foreach(
              path: string,
              exclude: RegExp[] = [],
              cb: (path: string, basename: string, isDir: boolean) => true | void | Promise<true | void>,
          ) {
              return forEachDir(path, exclude, (path: string, basename: string, isDir: boolean) => {
                  return cb(path, basename, isDir);
              });
          }
      
          const Path = require("path");
          const configPath = Path.resolve(process.cwd(), args.config);
          try {
              const config = require(configPath);
              // beforeStart调用
              if (config.beforeStart) await config.beforeStart(executeTemplate);
              await watch(config, []);
              // beforeEnd调用
              config.beforeEnd && config.beforeEnd(executeTemplate);
          } catch (e) {
              console.error("加载配置文件出错", process.cwd(), configPath);
          }
      })();

      配置文件

      // watch-cmd.config.js
      module.exports = {
          beforeEnd() {
              console.log("end")
          },
          rules: [
              {
                  test: /\.styl$/,
                  command: [
                      "stylus $FilePath$",
                      "node -v"
                  ]
              },
          ],
          exclude: [
              /node_modules/,
              /\.git/,
              /\.idea/,
              /src/,
              /bin/,
          ],
          include: ["./test"],
      };

      运行

      当我改动文件时

      从结果可以看出,文件 watch 回调触发了多次。其实我们不用编辑器改动文件的话,回调只会触发一次,这是编辑器的问题。

      那么细心的读者可能会想到为什么命令不会执行多次呢?

      是因为我用 debouncePromise 把 rule.on 包裹了一层。

      普通的防抖函数是这样的

      export function debounce<CB extends (...args: any[]) => void>(callback: CB, delay: number): CB {
          let timer: any = null;
          return function (...args: any[]) {
              if (timer) {
                  clearTimeout(timer);
                  timer = null;
              }
              timer = setTimeout(() => {
                  timer = null;
                  callback.apply(this, args);
              }, delay);
          } as CB;
      }

      但是这种没办法处理原函数返回 promise 的情况,也没办法 await

      所以要改造一下,让它可以处理 promise:每次在间隔内执行的时候,都把上一次的 promise reject 掉

      export function debouncePromise<T, CB extends (...args: any[]) => Promise<T>>(callback: CB, delay: number): CB {
          let timer: any = null;
          let rej: Function;
      
          return function (this: unknown, ...args: any[]) {
              return new Promise<T>((resolve, reject) => {
                  if (timer) {
                      clearTimeout(timer);
                      timer = null;
                      rej("debounce promise reject");
                  }
                  rej = reject;
                  timer = setTimeout(async () => {
                      timer = null;
                      const result = await callback.apply(this, args);
                      resolve(result);
                  }, delay);
              });
          } as CB;
      }

      加到逻辑上

      为什么不加到 watch 的回调上,则是因为部分浏览器最后保存的是目标文件的副本,如果加到 watch 回调上的话,那就会漏掉目标文件变动了

      这样就虽然还是会触发多次监听回调,但只执行最后一次回调。

      5.额外功能

      5.1 帮助功能

      在工具中通过-help/-h 启动

      console.log(`
          -config/-c=             配置的路径
          -help/-h                帮助
          -search/-s=             搜索文件或文件夹
          -search-flag/-sf=       搜索文件或文件夹 /\\w+/flag
          -search-exclude/-se=    搜索文件或文件夹 忽略文件夹 多个用逗号(,)隔开
          -open/-o=               打开资源管理器并选中文件或文件夹
          -open-type/-ot=         打开资源管理器并选中文件或文件夹
          -watch/-w               监听文件改变 与-config搭配使用
          -log                    遍历文件夹时是否显示遍历log
          -time/t                 显示执行代码所花费的时间
          -command/-cmd=          通过命令行执行命令 多个则用逗号(,)隔开 必须要用引号引起来
      `);

      5.2 搜索文件或文件夹

      在工具中通过-search/-s 启动

      其实这功能和我这工具相关性不大,为什么会加上这样的功能呢?是因为 windows 上搜索文件,经常目标文件存在都搜索不到,而且这工具遍历文件夹已经很方便了,所以就把搜索文件功能集成到这个工具上了

      实现

      import {getParams, forEachDir} from "../src/utils";
      
      const args = getParams()
      const search = args.search;
      
      const flag = args["search-flag"];
      const se = args["search-exclude"];
      if (search === true || search === undefined || flag === true || se === true) {
          throw new TypeError();
      }
      const reg = new RegExp(search, flag);
      console.log("search", reg);
      const exclude = se?.split(",").filter(i => i).map(i => new RegExp(i));
      forEachDir("./", exclude, (path, basename) => {
          if (reg.test(basename)) console.log("result ", path);
      });

      忽略大小写

      在工具中-search-flag/-sf=

      未忽略大小写

      忽略大小写

      忽略文件夹

      在工具中-search-exclude/-se=

      5.3 打开文件功能

      搜索到文件之后,自然是要打开文件了(只支持 windows)

      工具中通过-open/o=打开对应的文件

      代码

      import {getParams} from "../src/utils";
      const Path = require("path")
      
      enum OpenTypes {
          select = "select",
          cmd = "cmd",
          run = "run",
      }
      
      type ExecParams = [string, string[]];
      
      const args = getParams();
      
      const open = args.open;
      const path = Path.resolve(process.cwd(), open === true ? "./" : open);
      const stat = require("fs").statSync(path);
      const isDir = stat.isDirectory();
      const ot = args["open-type"];
      
      const type: string = !ot || ot === true ? OpenTypes.select : ot;
      const spawnSync = require('child_process').spawnSync;
      const match: { [k in OpenTypes]: ExecParams } = {
          // 运行一次就会打开一个资源管理器,不能只打开一个相同的
          [OpenTypes.select]: ["explorer", [`/select,"${path}"`]],
          [OpenTypes.run]: ['start', [path]],
          [OpenTypes.cmd]: ["start", ["cmd", "/k", `"cd ${isDir ? path : Path.dirname(path)}"`]],
      };
      const exec = ([command, path]: ExecParams) => spawnSync(command, path, {shell: true});
      console.log(path);
      exec(match[type as OpenTypes] || match[OpenTypes.select]);

      打开资源管理器并且选中文件

      命令

      结果

      在 cmd 中打开

      命令

      结果

      用默认 app 打开

      命令

      结果

      1. 上传到 npm

      接下来就把它发布到 npm 上,到时候全局安装后就可以在任意路径上运行了

      发布

      安装

      npm i -g @mxssfd/cmd-que

      测试

      1. 配合 webstorm file watcher 自动编译 less 并 postcss 编译

      1. 首先安装 cmd-que

      2. 开启 file watcher

      3 . 新建 less 文件

      4 . 修改 less 文件

      5 . 结果 这样配置好以后,每次修改文件就不用手动开启命令而是会自动执行编译命令了

      1. 最后

      写到这里,功能总算完成了,其实再叫做命令队列执行工具已经有点超纲了,不过常用功能还是用于执行命令的

      git 地址

      github.com/mengxinssfd: https://github.com/mengxinssfd/cmd-que

      本文由哈喽比特于3年以前收录,如有侵权请联系我们。
      文章来源:https://mp.weixin.qq.com/s/_hhpCM0mtENpAK6FrtsgPQ

       相关推荐

      刘强东夫妇:“移民美国”传言被驳斥

      京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

      发布于:1年以前  |  808次阅读  |  详细内容 »

      博主曝三大运营商,将集体采购百万台华为Mate60系列

      日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

      发布于:1年以前  |  770次阅读  |  详细内容 »

      ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

      据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

      发布于:1年以前  |  756次阅读  |  详细内容 »

      抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

      今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

      发布于:1年以前  |  648次阅读  |  详细内容 »

      威马CDO:中国每百户家庭仅17户有车

      日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

      发布于:1年以前  |  589次阅读  |  详细内容 »

      研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

      近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

      发布于:1年以前  |  449次阅读  |  详细内容 »

      苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

      据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

      发布于:1年以前  |  446次阅读  |  详细内容 »

      千万级抖音网红秀才账号被封禁

      9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

      发布于:1年以前  |  445次阅读  |  详细内容 »

      亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

      9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

      发布于:1年以前  |  444次阅读  |  详细内容 »

      苹果上线AppsbyApple网站,以推广自家应用程序

      据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

      发布于:1年以前  |  442次阅读  |  详细内容 »

      特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

      特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

      发布于:1年以前  |  441次阅读  |  详细内容 »

      光刻机巨头阿斯麦:拿到许可,继续对华出口

      据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

      发布于:1年以前  |  437次阅读  |  详细内容 »

      马斯克与库克首次隔空合作:为苹果提供卫星服务

      近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

      发布于:1年以前  |  430次阅读  |  详细内容 »

      𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

      据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

      发布于:1年以前  |  428次阅读  |  详细内容 »

      荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

      9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

      发布于:1年以前  |  423次阅读  |  详细内容 »

      AI操控无人机能力超越人类冠军

      《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

      发布于:1年以前  |  423次阅读  |  详细内容 »

      AI生成的蘑菇科普书存在可致命错误

      近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

      发布于:1年以前  |  420次阅读  |  详细内容 »

      社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

      社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

      发布于:1年以前  |  411次阅读  |  详细内容 »

      国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

      2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

      发布于:1年以前  |  406次阅读  |  详细内容 »

      罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

      罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

      发布于:1年以前  |  398次阅读  |  详细内容 »
       相关文章
      Android插件化方案 5年以前  |  237231次阅读
      vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
       目录