欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

關(guān)于commander.js使用及源碼分析

 更新時(shí)間:2023年06月06日 11:00:39   作者:dralexsanderl  
這篇文章主要介紹了關(guān)于commander.js使用及源碼分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

commander.js

commander是一個(gè)輕巧的nodejs模塊,提供了用戶命令行輸入和參數(shù)解析強(qiáng)大功能。

commander的特性:

  • 自記錄代碼
  • 自動(dòng)生成幫助
  • 合并短參數(shù)
  • 默認(rèn)選項(xiàng)
  • 強(qiáng)制選項(xiàng)??
  • 命令解析
  • 提示符

安裝

npm install commander

使用

在文件中引入commander,可以通過直接引入一個(gè)program對(duì)象或者創(chuàng)建一個(gè)實(shí)例的方式進(jìn)行使用。

const { program } = require('commander');
program.version("v1.0.0");
const { Command } = require('commander');
const program = new Command();
program.version("v1.0.0");

通過查看源碼可以得知program其實(shí)就是一個(gè)剛創(chuàng)建的實(shí)例,可以更明確地訪問全局命令。

源碼片段:

exports = module.exports = new Command();
exports.program = exports;
exports.Command = Command;

option選項(xiàng)

Commander 使用.option()方法來定義選項(xiàng),同時(shí)可以附加選項(xiàng)的簡介。

每個(gè)選項(xiàng)可以定義一個(gè)短選項(xiàng)名稱(-后面接單個(gè)字符)和一個(gè)長選項(xiàng)名稱(--后面接一個(gè)或多個(gè)單詞),使用逗號(hào)、空格或|分隔。

語法:

options(flags, description, fn, defaultValue)
commander.option(
    "-f, --filename [filename]",
    "The filename to use when reading from stdin. This will be used in source-maps, errors etc."
);

源碼解析:

lib\command.js option()

采用柯里化寫法, 實(shí)際調(diào)用的是_optionsEx()方法

  options(flags, description, fn, defaultValue) {
    return this._optionEx({}, flags, description, fn, defaultValue);
  }

_optionsEx(), 創(chuàng)建一個(gè)options實(shí)例,fn可以是函數(shù)、正則等形式,需要注意的是盡量不要使用正則形式,自 Commander v7 起,該功能不再推薦使用。

  _optionEx(config, flags, description, fn, defaultValue) {
    // 創(chuàng)建一個(gè)option實(shí)例
    const option = this.createOption(flags, description);
    if (typeof fn === "function") {
      option.default(defaultValue).argParser(fn);
    } else if (fn instanceof RegExp) {
      // deprecated
      ...
    } else {
      option.default(fn);
    }
    return this.addOption(option);
  }

Option構(gòu)造函數(shù)中,會(huì)調(diào)用一個(gè)splitOptionFlags()方法,用于解析長、斷標(biāo)識(shí),比如說-m,--mixed <value>。

attributeName()會(huì)返回一個(gè)camelcase的字符,例如--file-name會(huì)被解析成fileName。

通過空格、|,來切割出不同的字符。

function splitOptionFlags(flags) {
  let shortFlag;
  let longFlag;
  const flagParts = flags.split(/[ |,]+/);
  if (flagParts.length > 1 && !/^[[<]/.test(flagParts[1]))
    shortFlag = flagParts.shift();
  longFlag = flagParts.shift();
  if (!shortFlag && /^-[^-]$/.test(longFlag)) {
    shortFlag = longFlag;
    longFlag = undefined;
  }
  return { shortFlag, longFlag };
}

同時(shí),根據(jù)flags中的字符進(jìn)行判斷是否有設(shè)置接收參數(shù)。

<value>表示執(zhí)行該命令時(shí)必須傳入一個(gè)參數(shù),<value...>表示可接受多個(gè)參數(shù),[value]表示可配置參數(shù)。

this.required = flags.includes("<"); 
this.optional = flags.includes("[");
this.variadic = /\w\.\.\.[>\]]$/.test(flags)

在創(chuàng)建了一個(gè)option對(duì)象后,將其放入addOption()中進(jìn)行處理,該方法會(huì)對(duì)option進(jìn)行注冊、監(jiān)聽等操作。

  addOption(option) {
    const oname = option.name();
    const name = option.attributeName();
    // 注冊
    this.options.push(option);
    const handleOptionValue = (val, invalidValueMessage, valueSource) => {
      // ...
    };
    this.on("option:" + oname, (val) => {
      const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`;
      handleOptionValue(val, invalidValueMessage, "cli");
    });
    if (option.envVar) {
      this.on("optionEnv:" + oname, (val) => {
        const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`;
        handleOptionValue(val, invalidValueMessage, "env");
      });
    }
    return this;
  }

Command繼承了node中的事件模塊EventEmitter實(shí)現(xiàn)對(duì)option的監(jiān)聽和觸發(fā)。

在寫好option配置后,需要調(diào)用program.parse(process.argv)方法對(duì)用戶的輸入進(jìn)行解析。

parse(argv, parseOptions)
parse(argv, parseOptions) {
  const userArgs = this._prepareUserArgs(argv, parseOptions);
  this._parseCommand([], userArgs);
  return this;
}

_prepareUserArgvs方法中解析讀取用戶的輸入。

process.argv獲取的數(shù)組前兩個(gè)元素分別是node安裝地址跟運(yùn)行的script路徑,后邊的才是用戶的輸入,因此需要先過濾掉這兩個(gè)。

同時(shí)還支持多個(gè)argv約定。

_prepareUserArgs(argv, parseOptions) {
  parseOptions = parseOptions || {};
  this.rawArgs = argv.slice();
  let userArgs;
  switch (parseOptions.from) {
    case undefined:
    case 'node':
      this._scriptPath = argv[1];
      userArgs = argv.slice(2);
      break;
    case 'electron':
      if (process.defaultApp) {
        this._scriptPath = argv[1];
        userArgs = argv.slice(2);
      } else {
        userArgs = argv.slice(1);
      }
      break;
    case 'user':
      userArgs = argv.slice(0);
      break;
    default:
      throw new Error(
        `unexpected parse option { from: '${parseOptions.from}' }`
      );
  }
  if (!this._scriptPath && require.main) {
    this._scriptPath = require.main.filename;
  }
  this._name =
    this._name ||
    (this._scriptPath &&
      path.basename(this._scriptPath, path.extname(this._scriptPath)));
  return userArgs;
}

在獲取了用戶輸入后,就是對(duì)這些參數(shù)進(jìn)行解析。

_parseCommand()

_parseCommand(operands, unknown) {
    const parsed = this.parseOptions(unknown);
    // command部分下邊繼續(xù)講解
}

parseOptions讀取輸入的配置

parseOptions(argv) {
  const operands = [];
  const unknown = [];
  let dest = operands;
  const args = argv.slice();
  // 判斷是否是option配置, 必須要有一個(gè)-開頭
  function maybeOption(arg) {
    return arg.length > 1 && arg[0] === '-';
  }
  let activeVariadicOption = null;
  // 逐步讀取配置
  while (args.length) {
    const arg = args.shift();
    // --終止讀取 也就是說 -- 后邊的配置并不會(huì)生效
    // 例如 node src/index.js -  --number 1 2 3 -- -c a b
    // -c其實(shí)沒有用
    if (arg === '--') {
      if (dest === unknown) dest.push(arg);
      dest.push(...args);
      break;
    }
    // 處理多參數(shù)情況
    if (activeVariadicOption && !maybeOption(arg)) {
      // 當(dāng)觸發(fā)這個(gè)監(jiān)聽時(shí)會(huì)執(zhí)行handleOptionValue,里邊會(huì)判斷variadic的值將所有的參數(shù)放進(jìn)一個(gè)數(shù)組中
      this.emit(`option:${activeVariadicOption.name()}`, arg);
      continue;
    }
    activeVariadicOption = null;
    if (maybeOption(arg)) {
      // 查看是否已經(jīng)配置有該命令
      const option = this._findOption(arg);
      if (option) {
          // 前邊提到的當(dāng)配置option時(shí)如果有配置 <value> 必填參數(shù),那么該option的required值為true
        if (option.required) {
            // 讀取value
          const value = args.shift();
          if (value === undefined) this.optionMissingArgument(option);
          // 觸發(fā)監(jiān)聽方法
          this.emit(`option:${option.name()}`, value);
        // 配置了可選參數(shù)
        } else if (option.optional) {
          let value = null;
          if (args.length > 0 && !maybeOption(args[0])) {
            value = args.shift();
          }
          this.emit(`option:${option.name()}`, value);
        } else {
          // boolean flag
          this.emit(`option:${option.name()}`);
        }
        // 前面提到當(dāng)配置的option中存在 ...> 如<value...>時(shí),variadic值為true
        activeVariadicOption = option.variadic ? option : null;
        continue;
      }
    }
    // 組合flag, 比如像 -abc , 會(huì)遍歷判斷 -a -b -c是否是配置過的option
    if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') {
      // 拆解組合
      const option = this._findOption(`-${arg[1]}`);
      if (option) {
        if (
          option.required ||
          (option.optional && this._combineFlagAndOptionalValue)
        ) {
          this.emit(`option:${option.name()}`, arg.slice(2));
        } else {
          this.emit(`option:${option.name()}`);
          args.unshift(`-${arg.slice(2)}`);
        }
        continue;
      }
    }
    // 解析--foo=bar傳參格式
    if (/^--[^=]+=/.test(arg)) {
      const index = arg.indexOf('=');
      const option = this._findOption(arg.slice(0, index));
      if (option && (option.required || option.optional)) {
        this.emit(`option:${option.name()}`, arg.slice(index + 1));
        continue;
      }
    }
    if (maybeOption(arg)) {
      dest = unknown;
    }
    // 解析command 下面在講解
    dest.push(arg);
  }
  return { operands, unknown };
}

從上面的源碼解析中可以總結(jié)如下:

  • 多個(gè)短選項(xiàng)可以合并簡寫,最后一個(gè)選項(xiàng)可以附加參數(shù), 比如-a -b -c 1可以寫成-abc 1。
  • --可以標(biāo)記選項(xiàng)的結(jié)束,后續(xù)的參數(shù)均不會(huì)被命令解釋,可以正常使用。
  • 可以通過=傳參
  • 可以傳遞多個(gè)參數(shù)

常用選項(xiàng)類型

  • boolean型, 選項(xiàng)無需配置參數(shù), 通常我們使用的都是此類型。
  • 設(shè)置參數(shù)(使用尖括號(hào)聲明在該選項(xiàng)后,如--expect <value>),如果在命令行中不指定具體的選項(xiàng)及參數(shù),則會(huì)被定義為undefined。
const { Command } = require('commander');
const program = new Command();
program.option('-e --example', 'this is a boolean type option');
program.option('-t --type <type>', 'must set an param or an error will occur');
program.parse(process.argv);
console.log(program.opts());

eg.

node index.js -e
{ example: true }
node index.js -t
error: option '-t --type <type>' argument missing
node index.js -t a
{ type: 'a' }

取反, 可以定義一個(gè)以 no- 開頭的 boolean長選項(xiàng)

在命令行中使用該選項(xiàng)時(shí),會(huì)將對(duì)應(yīng)選項(xiàng)的值置為 false

當(dāng)只定義了帶 no- 的選項(xiàng),未定義對(duì)應(yīng)不帶 no- 的選項(xiàng)時(shí),該選項(xiàng)的默認(rèn)值會(huì)被置為 true值。

program.option('--no-example', 'no example');
node index.js --no-example
{ example: false }

源碼解析:

在配置了option之后,會(huì)對(duì)該命令進(jìn)行監(jiān)聽,執(zhí)行該flag會(huì)觸發(fā)handleOptionValue方法,根據(jù)用戶是否有設(shè)置取反或者有默認(rèn)值,來設(shè)置該option的值

handleOptionValue

const handleOptionValue = (val, invalidValueMessage, valueSource) => {
  const oldValue = this.getOptionValue(name);
  // 參數(shù)處理這部分放在自定義選項(xiàng)部分講解
  // .. 
  if (typeof oldValue === 'boolean' || typeof oldValue === 'undefined') {
    if (val == null) {
      this.setOptionValueWithSource(
        name,
        // 是否取反,取反的話值為false, 不取反就判斷是否有值,沒有就賦值為true
        option.negate ? false : defaultValue || true,
        valueSource
      );
    } else {
      this.setOptionValueWithSource(name, val, valueSource);
    }
  } else if (val !== null) {
    this.setOptionValueWithSource(
      name,
      option.negate ? false : val,
      valueSource
    );
  }
};

可選參數(shù) (--optional [value]), 該選項(xiàng)在不帶參數(shù)時(shí)可用作 boolean 選項(xiàng),在帶有參數(shù)時(shí)則從參數(shù)中得到值。

program.option('-f [filename]', 'optional');
node index.js -f
{ f: true }
node index.js -f index.js
{ f: 'index.js' }

必填選項(xiàng)

通過.requiredOption()方法可以設(shè)置選項(xiàng)為必填。

必填選項(xiàng)要么設(shè)有默認(rèn)值,要么必須在命令行中輸入,對(duì)應(yīng)的屬性字段在解析時(shí)必定會(huì)有賦值。該方法其余參數(shù)與.option()一致。

program.requiredOption('-r --required <type>', 'must');
node index.js   
error: required option '-r --required <type>' not specified

當(dāng)然可以設(shè)置一個(gè)默認(rèn)值

program.requiredOption('-r --required <type>', 'must', 'a');
node index.js   
{ required: 'a' }

變長參數(shù)選項(xiàng)

定義選項(xiàng)時(shí),可以通過使用...來設(shè)置參數(shù)為可變長參數(shù)。在命令行中,用戶可以輸入多個(gè)參數(shù),解析后會(huì)以數(shù)組形式存儲(chǔ)在對(duì)應(yīng)屬性字段中。在輸入下一個(gè)選項(xiàng)前(---開頭),用戶輸入的指令均會(huì)被視作變長參數(shù)。與普通參數(shù)一樣的是,可以通過--標(biāo)記當(dāng)前命令的結(jié)束。

program.option('-n <numbers...>', 'set numbers');
program.option('-c <chars...>', 'set chars');
node index.js -n 1 2 3 4 -c a b c
{ n: [ '1', '2', '3', '4' ], c: [ 'a', 'b', 'c' ] }

版本 

.version()方法可以設(shè)置版本,其默認(rèn)選項(xiàng)為-V--version,設(shè)置了版本后,命令行會(huì)輸出當(dāng)前的版本號(hào)。

program.version('v1.0.0')

版本選項(xiàng)也支持自定義設(shè)置選項(xiàng)名稱,可以在.version()方法里再傳遞一些參數(shù)(長選項(xiàng)名稱、描述信息),用法與.option()方法類似。

program.version('v1.0.0', '-a --aversion', 'current version');

源碼解析:

version方法其實(shí)很簡單,就是判斷用戶配置version信息時(shí)有沒有自定義信息,比如說啟動(dòng)命令這些。之后就是創(chuàng)建一個(gè)option注冊后監(jiān)聽。

version(str, flags, description) {
  if (str === undefined) return this._version;
  this._version = str;
  flags = flags || '-V, --version';
  description = description || 'output the version number';
  const versionOption = this.createOption(flags, description);
  this._versionOptionName = versionOption.attributeName();
  this.options.push(versionOption);
  this.on('option:' + versionOption.name(), () => {
    this._outputConfiguration.writeOut(`${str}\n`);
    this._exit(0, 'commander.version', str);
  });
  return this;
}

使用addOption方法

大多數(shù)情況下,選項(xiàng)均可通過.option()方法添加。但對(duì)某些不常見的用例,也可以直接構(gòu)造Option對(duì)象,對(duì)選項(xiàng)進(jìn)行更詳盡的配置。

program
  .addOption(
    new Option('-t, --timeout <delay>', 'timeout in seconds').default(
      60,
      'one minute'
    )
  )
  .addOption(
    new Option('-s, --size <type>', 'size').choices([
      'small',
      'medium',
      'large',
    ])
  );
node index.js -s small
{ timeout: 60, size: 'small' }
node index.js -s mini
error: option '-s, --size <type>' argument 'mini' is invalid. Allowed choices are small, medium, large.

源碼解析:

就是添加一個(gè)fn在傳入值的時(shí)候判斷一下值就可以。

function choices(values) {
  this.argChoices = values;
  this.parseArg = (arg, previous) => {
    if (!values.includes(arg)) {
      throw new InvalidArgumentError(
        `Allowed choices are ${values.join(', ')}.`
      );
    }
    if (this.variadic) {
      return this._concatValue(arg, previous);
    }
    return arg;
  };
  return this;
}

自定義選項(xiàng)

選項(xiàng)的參數(shù)可以通過自定義函數(shù)來處理,該函數(shù)接收兩個(gè)參數(shù),即用戶新輸入的參數(shù)值和當(dāng)前已有的參數(shù)值(即上一次調(diào)用自定義處理函數(shù)后的返回值),返回新的選項(xiàng)參數(shù)值。 也就是option中的第三個(gè)參數(shù)。第四個(gè)參數(shù)就是初始值

自定義函數(shù)適用場景包括參數(shù)類型轉(zhuǎn)換,參數(shù)暫存,或者其他自定義處理的場景。

program.option(
  '-f --float <number>',
  'process float argument',
  (value, previous) => {
    return Number(value) + previous;
  },
  2
);
node index.js -f 3
{ float: 5 }

源碼解析:

const handleOptionValue = (val, invalidValueMessage, valueSource) => {
  const oldValue = this.getOptionValue(name);
  // 當(dāng)配置了option的第三個(gè)參數(shù)
  if (val !== null && option.parseArg) {
    try {
      val = option.parseArg(
        val,
        // defaultValue為第四個(gè)參數(shù)
        oldValue === undefined ? defaultValue : oldValue
      );
    } catch (err) {
      if (err.code === "commander.invalidArgument") {
        const message = `${invalidValueMessage} ${err.message}`;
        this._displayError(err.exitCode, err.code, message);
      }
      throw err;
    }
  // 處理多參數(shù)情況
  } else if (val !== null && option.variadic) {
    val = option._concatValue(val, oldValue);
  }
  // ...
  // 查看取反部分講解
}

配置命令

通過.command().addCommand()可以配置命令,有兩種實(shí)現(xiàn)方式:為命令綁定處理函數(shù),或者將命令單獨(dú)寫成一個(gè)可執(zhí)行文件。子命令支持嵌套。

.command()的第一個(gè)參數(shù)為命令名稱。命令參數(shù)可以跟在名稱后面,也可以用.argument()單獨(dú)指定。參數(shù)可為必選的(尖括號(hào)表示)、可選的(方括號(hào)表示)或變長參數(shù)(點(diǎn)號(hào)表示,如果使用,只能是最后一個(gè)參數(shù))。

program
  .command('clone <source> [destination]')
  .description('clone a repository into a newly created directory')
  .action((source, destination) => {
    console.log('clone command called');
  });

Command對(duì)象上使用.argument()來按次序指定命令參數(shù)。該方法接受參數(shù)名稱和參數(shù)描述。參數(shù)可為必選的(尖括號(hào)表示,例如<required>)或可選的(方括號(hào)表示,例如[optional])。

program
  .command('login')
  .description('login')
  .argument('<username>', 'user')
  .argument('[password]', 'password', 'no password')
  .action((username, password) => {
    console.log(`login, ${username} - ${password}`);
  });

在參數(shù)名后加上...來聲明可變參數(shù),且只有最后一個(gè)參數(shù)支持這種用法??勺儏?shù)會(huì)以數(shù)組的形式傳遞給處理函數(shù)。

program
  .command('readFile')
  .description('read multiple file')
  .argument('<username>', 'user')
  .argument('[password]', 'password', 'no password')
  .argument('<filepath...>')
  .action((username, password, args) => {
    args.forEach((dir) => {
      console.log('rmdir %s', dir);
    });
    console.log(`username: ${username}, pass: ${password}, args: ${args}`);
  });

源碼解析:

新建一個(gè)command

command(nameAndArgs, actionOptsOrExecDesc, execOpts) {
  let desc = actionOptsOrExecDesc;
  let opts = execOpts;
  if (typeof desc === 'object' && desc !== null) {
    opts = desc;
    desc = null;
  }
  opts = opts || {};
  // 解析獲取輸入的命令及參數(shù)
  const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/);
  // 創(chuàng)建一個(gè)command
  const cmd = this.createCommand(name);
  if (desc) {
    cmd.description(desc);
    cmd._executableHandler = true;
  }
  if (opts.isDefault) this._defaultCommandName = cmd._name;
  cmd._hidden = !!(opts.noHelp || opts.hidden);
  cmd._executableFile = opts.executableFile || null;
  // 添加參數(shù)
  if (args) cmd.arguments(args);
  this.commands.push(cmd);
  cmd.parent = this;
  // 繼承屬性
  cmd.copyInheritedSettings(this);
  if (desc) return this;
  return cmd;
}

action用于注冊命令回調(diào)。

action(fn) {
  const listener = (args) => {
    const expectedArgsCount = this._args.length;
    const actionArgs = args.slice(0, expectedArgsCount);
    if (this._storeOptionsAsProperties) {
      actionArgs[expectedArgsCount] = this;
    } else {
      actionArgs[expectedArgsCount] = this.opts();
    }
    actionArgs.push(this);
    return fn.apply(this, actionArgs);
  };
  this._actionHandler = listener;
  return this;
}

接下來就是如何解析執(zhí)行命令部分了。還記得我們之前解析option時(shí)的流程嘛 

parse() -> _parseCommand(), 在parseOptions方法中,我們還剩余了處理命令部分代碼,現(xiàn)在來看一下。

源碼解析:

parseOptions(argv) {
  const operands = [];
  const unknown = [];
  let dest = operands;
  const args = argv.slice();
  while (args.length) {
    // 解析option部分
    // ...
    // 當(dāng)開啟了enablePositionalOptions后, 會(huì)將命令后邊的參數(shù)全部定義為未知參數(shù),而不是命令選項(xiàng),同時(shí)如果想要在子命令中啟動(dòng)enablePositionalOptions,需要在父命令中先開啟。
    if (
      (this._enablePositionalOptions || this._passThroughOptions) &&
      operands.length === 0 &&
      unknown.length === 0
    ) {
      if (this._findCommand(arg)) {
        operands.push(arg);
        if (args.length > 0) unknown.push(...args);
        break;
      } else if (
        arg === this._helpCommandName &&
        this._hasImplicitHelpCommand()
      ) {
        operands.push(arg);
        if (args.length > 0) operands.push(...args);
        break;
      } else if (this._defaultCommandName) {
        unknown.push(arg);
        if (args.length > 0) unknown.push(...args);
        break;
      }
    }
    if (this._passThroughOptions) {
      dest.push(arg);
      if (args.length > 0) dest.push(...args);
      break;
    }
    dest.push(arg);
  }
  return { operands, unknown };
}

沒有開啟enablePositionalOptions配置,命令及后邊的參數(shù)都會(huì)保存在operands數(shù)組中,當(dāng)開啟后,命令后邊的參數(shù)都會(huì)變成unknown。

在解析了用戶輸入的參數(shù)后繼續(xù)往下執(zhí)行_parseCommand方法

_parseCommand(operands, unknown) {
  // 解析配置
  const parsed = this.parseOptions(unknown);
  this._parseOptionsEnv(); // after cli, so parseArg not called on both cli and env
  operands = operands.concat(parsed.operands);
  unknown = parsed.unknown;
  this.args = operands.concat(unknown);
  // 匹配找到命令, 使用子進(jìn)程運(yùn)行命令
  if (operands && this._findCommand(operands[0])) {
    return this._dispatchSubcommand(operands[0], operands.slice(1), unknown);
  }
  // 沒找到命令就判斷一下是否有幫助命令, 而且用戶輸入的第一個(gè)參數(shù)是help
  if (this._hasImplicitHelpCommand() && operands[0] === this._helpCommandName) {
    // 有且只有個(gè)命令
    if (operands.length === 1) {
      this.help();
    }
    // 執(zhí)行第二條命令
    return this._dispatchSubcommand(operands[1], [], [this._helpLongFlag]);
  }
  // 幫助命令也沒有找到或者用戶輸入的第一個(gè)參數(shù)未知但是存在一個(gè)默認(rèn)名字
  if (this._defaultCommandName) {
    outputHelpIfRequested(this, unknown); // Run the help for default command from parent rather than passing to default command
    return this._dispatchSubcommand(
      this._defaultCommandName,
      operands,
      unknown
    );
  }
  // ...
  const commandEvent = `command:${this.name()}`;
  // 存在command處理器
  if (this._actionHandler) {
    checkForUnknownOptions();
    // 處理參數(shù)
    this._processArguments();
    let actionResult;
    actionResult = this._chainOrCallHooks(actionResult, 'preAction');
    actionResult = this._chainOrCall(actionResult, () =>
      this._actionHandler(this.processedArgs)
    );
    // 觸發(fā)父命令
    if (this.parent) this.parent.emit(commandEvent, operands, unknown); // legacy
    actionResult = this._chainOrCallHooks(actionResult, 'postAction');
    return actionResult;
  }
  // 觸發(fā)父監(jiān)聽器
  if (this.parent && this.parent.listenerCount(commandEvent)) {
    checkForUnknownOptions();
    this._processArguments();
    this.parent.emit(commandEvent, operands, unknown);
  // 處理參數(shù)
  } else if (operands.length) {
    if (this._findCommand('*')) {
      return this._dispatchSubcommand('*', operands, unknown);
    }
    if (this.listenerCount('command:*')) {
      this.emit('command:*', operands, unknown);
    } else if (this.commands.length) {
      this.unknownCommand();
    } else {
      checkForUnknownOptions();
      this._processArguments();
    }
  // 不存在命令
  } else if (this.commands.length) {
    checkForUnknownOptions();
    this.help({ error: true });
  } else {
    checkForUnknownOptions();
    this._processArguments();
  }
}

到此,整個(gè)解析過程就完成了。

聲明統(tǒng)一參數(shù)

命令處理函數(shù)的參數(shù),為該命令聲明的所有參數(shù),除此之外還會(huì)附加兩個(gè)額外參數(shù):一個(gè)是解析出的選項(xiàng),另一個(gè)則是該命令對(duì)象自身。

program
  .argument('<name>')
  .option('-t, --title <title>', 'title to use before name')
  .option('-d, --de')
  .action((name, options, command) => {
    console.log(name);
    console.log(options);
    console.log(command.name());
  });

幫助信息

幫助信息是 Commander 基于你的程序自動(dòng)生成的,默認(rèn)的幫助選項(xiàng)是-h,--help。

node index.js -h
Usage: index [options]
Options:
  -h, --help  display help for command

源碼解析:

commander.js提供了一個(gè)help類,但是它的方法都是靜態(tài)的,允許被覆蓋。

command類中有一個(gè)createHelp方法,通過Object.assign()方法覆蓋help的原始方法。

createHelp() {
    return Object.assign(new Help(), this.configureHelp());
}

formatHelp生成幫助文字,解析處理設(shè)置的command、optionargument

formatHelp(cmd, helper) {
  const termWidth = helper.padWidth(cmd, helper);
  const helpWidth = helper.helpWidth || 80;
  const itemIndentWidth = 2;
  const itemSeparatorWidth = 2;
  // 格式化
  function formatItem(term, description) {
    if (description) {
      const fullText = `${term.padEnd(
        termWidth + itemSeparatorWidth
      )}${description}`;
      return helper.wrap(
        fullText,
        helpWidth - itemIndentWidth,
        termWidth + itemSeparatorWidth
      );
    }
    return term;
  }
  function formatList(textArray) {
    return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
  }
  let output = [`Usage: ${helper.commandUsage(cmd)}`, ''];
  // 描述符
  const commandDescription = helper.commandDescription(cmd);
  if (commandDescription.length > 0) {
    output = output.concat([commandDescription, '']);
  }
  // 參數(shù)
  const argumentList = helper.visibleArguments(cmd).map((argument) => {
    return formatItem(
      helper.argumentTerm(argument),
      helper.argumentDescription(argument)
    );
  });
  if (argumentList.length > 0) {
    output = output.concat(['Arguments:', formatList(argumentList), '']);
  }
  // 選項(xiàng)
  const optionList = helper.visibleOptions(cmd).map((option) => {
    return formatItem(
      helper.optionTerm(option),
      helper.optionDescription(option)
    );
  });
  if (optionList.length > 0) {
    output = output.concat(['Options:', formatList(optionList), '']);
  }
  // 命令
  const commandList = helper.visibleCommands(cmd).map((cmd) => {
    return formatItem(
      helper.subcommandTerm(cmd),
      helper.subcommandDescription(cmd)
    );
  });
  if (commandList.length > 0) {
    output = output.concat(['Commands:', formatList(commandList), '']);
  }
  return output.join('\n');
}

自定義

使用addHelpText方法添加額外的幫助信息。

program.addHelpText('after', `call help`);
node index.js -h
Usage: index [options]
Options:
  -h, --help  display help for command
call help

addHelpText方法的第一個(gè)參數(shù)是添加的幫助信息展示的位置,

包括如下:

  • beforeAll:作為全局標(biāo)頭欄展示
  • before:在內(nèi)建幫助信息之前展示
  • after:在內(nèi)建幫助信息之后展示
  • afterAll:作為全局末尾欄展示

源碼解析:

在執(zhí)行addHelpText方法后,會(huì)對(duì)該事件進(jìn)行監(jiān)聽,同時(shí)執(zhí)行輸入幫助命令時(shí)會(huì)按順序執(zhí)行所有事件,從而實(shí)現(xiàn)自定義信息的展示。

addHelpText

addHelpText(position, text) {
  const allowedValues = ['beforeAll', 'before', 'after', 'afterAll'];
  // 過濾
  if (!allowedValues.includes(position)) {
    throw new Error(`Unexpected value for position to addHelpText.
Expecting one of '${allowedValues.join("', '")}'`);
  }
  const helpEvent = `${position}Help`;
  // 監(jiān)聽事件
  this.on(helpEvent, (context) => {
    let helpStr;
    if (typeof text === 'function') {
      helpStr = text({ error: context.error, command: context.command });
    } else {
      helpStr = text;
    }
    // Ignore falsy value when nothing to output.
    if (helpStr) {
      context.write(`${helpStr}\n`);
    }
  });
  return this;
}

outputHelp

outputHelp(contextOptions) {
  let deprecatedCallback;
  if (typeof contextOptions === 'function') {
    deprecatedCallback = contextOptions;
    contextOptions = undefined;
  }
  const context = this._getHelpContext(contextOptions);
  // 遍歷觸發(fā)所有祖先命令的 beforeAllHelp 事件
  getCommandAndParents(this)
    .reverse()
    // 觸發(fā) beforeAllHelp
    .forEach((command) => command.emit('beforeAllHelp', context));
  // 觸發(fā) beforeHelp
  this.emit('beforeHelp', context);
  let helpInformation = this.helpInformation(context);
  if (deprecatedCallback) {
    helpInformation = deprecatedCallback(helpInformation);
    if (
      typeof helpInformation !== 'string' &&
      !Buffer.isBuffer(helpInformation)
    ) {
      throw new Error('outputHelp callback must return a string or a Buffer');
    }
  }
  context.write(helpInformation);
  // 觸發(fā) help 指令
  this.emit(this._helpLongFlag); // deprecated
  // 觸發(fā) afterHelp
  this.emit('afterHelp', context);
  // 遍歷觸發(fā)所有祖先命令的 afterAllHelp 事件
  getCommandAndParents(this).forEach((command) =>
    // 觸發(fā) afterAllHelp
    command.emit('afterAllHelp', context)
  );
}

showHelpAfterError展示幫助信息

program.showHelpAfterError();
// 或者
program.showHelpAfterError('(add --help for additional information)');
node index.js -asd
error: unknown option '-asd'
(add --help for additional information)

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Node.js巧妙實(shí)現(xiàn)Web應(yīng)用代碼熱更新

    Node.js巧妙實(shí)現(xiàn)Web應(yīng)用代碼熱更新

    本文給大家講解的是Node.js的代碼熱更新的問題,其主要實(shí)現(xiàn)原理 是怎么對(duì) module 對(duì)象做處理,也就是手工監(jiān)聽文件修改, 然后清楚模塊緩存, 重新掛載模塊,思路清晰考慮細(xì)致, 雖然有點(diǎn)冗余代碼,但還是推薦給大家
    2015-10-10
  • 使用nvm安裝node的詳細(xì)圖文教程

    使用nvm安裝node的詳細(xì)圖文教程

    nvm是nodejs的版本管理工具,可以在一個(gè)環(huán)境中同時(shí)安裝多個(gè)nodejs 版本(和配套的 npm 版本),并隨時(shí)切換,下面這篇文章主要給大家介紹了關(guān)于使用nvm安裝node的詳細(xì)圖文教程,需要的朋友可以參考下
    2023-02-02
  • 防止Node.js中錯(cuò)誤導(dǎo)致進(jìn)程阻塞的辦法

    防止Node.js中錯(cuò)誤導(dǎo)致進(jìn)程阻塞的辦法

    Node.js我們用到非常的多了,如果我們開發(fā)不當(dāng)可能因錯(cuò)誤導(dǎo)致進(jìn)程阻塞問題,對(duì)于進(jìn)程阻塞問題一直是個(gè)頭痛的事情,今天我們一起來看一篇關(guān)于Node.js防止錯(cuò)誤導(dǎo)致的進(jìn)程阻塞示例,下面一起來看看。
    2016-08-08
  • 解決node修改后需頻繁手動(dòng)重啟的問題

    解決node修改后需頻繁手動(dòng)重啟的問題

    今天小編就為大家分享一篇解決node修改后需頻繁手動(dòng)重啟的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-05-05
  • nodeJS服務(wù)器的創(chuàng)建和重新啟動(dòng)的實(shí)現(xiàn)方法

    nodeJS服務(wù)器的創(chuàng)建和重新啟動(dòng)的實(shí)現(xiàn)方法

    今天小編就為大家分享一篇nodeJS服務(wù)器的創(chuàng)建和重新啟動(dòng)的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-05-05
  • 使用nodejs?spider爬取圖片及數(shù)據(jù)實(shí)現(xiàn)

    使用nodejs?spider爬取圖片及數(shù)據(jù)實(shí)現(xiàn)

    這篇文章主要為大家介紹了使用nodejs?spider爬取圖片及數(shù)據(jù)實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-07-07
  • nodejs之koa2請(qǐng)求示例(GET,POST)

    nodejs之koa2請(qǐng)求示例(GET,POST)

    本篇文章主要介紹了nodejs之koa2請(qǐng)求示例(GET,POST),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2018-08-08
  • 使用VS開發(fā) Node.js指南

    使用VS開發(fā) Node.js指南

    這篇文章主要介紹了使用VS開發(fā) Node.js的方法,主要是使用NTVS(Node.js Toolsfor Visual Studio)來實(shí)現(xiàn),有需要的小伙伴參考下
    2015-01-01
  • 詳解Node.Js如何處理post數(shù)據(jù)

    詳解Node.Js如何處理post數(shù)據(jù)

    這篇文章給大家介紹了如何利用Node.Js處理post數(shù)據(jù),文中通過實(shí)例和圖文介紹的很詳細(xì),有需要的小伙伴們可以參考借鑒,下面來一起看看吧。
    2016-09-09
  • 詳解nodejs 文本操作模塊-fs模塊(三)

    詳解nodejs 文本操作模塊-fs模塊(三)

    本篇文章主要介紹了nodejs 文本操作模塊-fs模塊(三),詳細(xì)的講訴了readFile,和writeFile方法,具有一定的參考價(jià)值,有興趣的可以了解一下。
    2016-12-12

最新評(píng)論