Node.js 中如何收集和解析命令行參數

2021-02-19 程式設計師成長指北
前言

  在開發 CLI(Command Line Interface)工具的業務場景下,離不開命令行參數的收集和解析。

  接下來,本文介紹如何收集和解析命令行參數。

收集命令行參數

  在 Node.js 中,可以通過 process.argv 屬性收集進程被啟動時傳入的命令行參數:

  // ./example/demo.js
  process.argv.slice(2);

  // 命令行執行如下命令
  node ./example/demo.js --name=xiaoming --age=20 man

  // 得到的結果
  [ '--name=xiaoming', '--age=20', 'man' ]

  由上述示例可以發現,Node.js 在處理命令行參數時,只是簡單地通過空格來分割字符串。

  對於這樣的參數數組,無法很方便地獲取到每個參數對應的值,所以需要再進行一次解析操作。

命令行參數風格

  在解析命令行參數之前,需要了解一些常見的命令行參數風格:

  Unix 參數風格有一個特殊的注意事項:「「-」後面緊鄰的每一個字母都表示一個參數名」

  ls -al

  上述命令用來顯示當前目錄下所有的文件、文件夾並且顯示它們的詳細信息,等同於:

  ls -a -l

  GNU 風格的參數以 「--」開頭,一般後面會跟上一個單詞或者短語,例如熟悉的 npm 安裝依賴的命令:

  npm install --save koa

對於兩個單詞的情況,在 GNU 參數風格中,會通過「-」來連接,例如 npm 安裝僅用於開發環境的依賴:

  npm install --save-dev webpack

  BSD 是加州大學伯克利分校開發的一個 Unix 版本。其與 Unix 的區別主要在於參數前面沒有 「-」,個人感覺這樣很難區別參數和參數值。

注意事項:-- 後面緊鄰空格時,表示後面的字符串不需要解析。

❞解析命令行參數
function parse(args = []) {
  // _ 屬性用來保留不需要處理的參數字符串
  const output = { _: [] };

  for (let index = 0; index < args.length; index++) {
    const arg = args[index];
    
    if (isIgnoreFollowingParameters(output, args, index, arg)) {
      break;
    }
    
    if (!isParameter(arg)) {
      output._.push(arg);
      continue;
    }

    ...
  }

  return output;
}

parse(process.argv.slice(2));

  接收到命令行參數數組之後,需要遍歷數組,處理每一個參數字符串。

  isIgnoreFollowingParameters 方法主要用來判斷單個「--」的場景,後續的參數字符串不再需要處理:

function isIgnoreFollowingParameters(output, args, index, arg) {
  if (arg !== '--') {
    return false;
  }
  output._ = output._.concat(args.slice(++index));
  return true;
}

  接下來,如果參數字符串不以「-」開頭,同樣也不需要處理,參數的形式以 Unix 和 GNU 風格為主:

function isParameter(arg) {
  return arg.startsWith('-');
}

  參數的表現形式主要分為以下幾種:

"--name=xiaoming": 參數名為 name,參數值為 xiaoming"-abc=10": 參數名為 a,參數值為 true;參數名為 b,參數值為 true;參數名為 c,參數值為 10"--save-dev": 參數名為 save-dev,參數值為 true"--age 20":參數名為 age,參數值為 20
  let hyphensIndex;
  for (hyphensIndex = 0; hyphensIndex < arg.length; hyphensIndex++) {
    if (arg.charCodeAt(hyphensIndex) !== 45) {
      break;
    }
  }

  let assignmentIndex;
  for (assignmentIndex = hyphensIndex + 1; assignmentIndex < arg.length; assignmentIndex++) {
    if (arg[assignmentIndex].charCodeAt(0) === 61) {
      break;
    }
  }

  利用 Unicode 碼點值找出連字符和等號的下標值,從而根據下標分割出參數名和參數值:

  const name = arg.substring(hyphensIndex, assignmentIndex);

  let value;
  const assignmentValue = arg.substring(++assignmentIndex);

  處理參數值時,需要考慮參數賦值的四種場景:

  if (assignmentValue) {
    value = assignmentValue; // --name=xiaoming or -abc=10
  } else if (index + 1 === args.length) {
    value = true; // --save-dev
  } else if (('' + args[index + 1]).charCodeAt(0) !== 45) {
    value = args[++index]; // --age 20
  } else {
    value = true; // 預設情況
  }

  由於 Unix 風格中每一個字母都代表一個參數,並且「手動傳遞的參數值應該賦值給最後一個參數」,所以還需針對該場景進行適配:

  // 「-」or「--」
  const arr = hyphensIndex === 2 ? [name] : name;
  for (let keyIndex = 0; keyIndex < arr.length; keyIndex++) {
    const _key = arr[keyIndex];
    const _value = keyIndex + 1 < arr.length || value;
    handleKeyValue(output, _key, _value);
  }

  最後針對參數的賦值操作,需要考慮到「多次賦值」的情況:

function handleKeyValue(output, key, value) {
  const oldValue = output[key];
  if (Array.isArray(oldValue)) {
    output[key] = oldValue.concat(value);
    return;
  }

  if (oldValue) {
    output[key] = [oldValue, value];
    return;
  }

  output[key] = value;
}

  到此,命令行參數的解析功能就完成了,上述方法執行的效果如下:

  # 命令行執行
  node ./example/step1.js --name=xiaoming --age 20 --save-dev -abc=10 -c=20  -- --ignore

  # 解析結果
  {
    _: [ '--ignore' ],
    name: 'xiaoming',
    age: '20',
    'save-dev': true,
    a: true,
    b: true,
    c: [ '10', '20' ]
  }

別名機制

  比較優秀的 CLI 工具在參數的解析上都支持參數的別名設置,例如使用 npm 安裝開發環境依賴時,你可以選擇這種完整的寫法:

  npm install --save-dev webpack

  你也可以使用下面這種別名方式:

  npm install -D webpack

  從使用上來說 -D 和 --save-dev 是兩種方式,但是從 CLI 工具的開發者來說,最終處理邏輯時只能以一個參數名為標準,所以對於一個命令行參數解析庫來說,其結果需要包含所有的情況:

  npm install --save-dev webpack

  # 解析的結果
  { 'save-dev': true, 'D': true }

  以上文的解析方法為例,需要添加額外的選項參數,加入 alias 屬性來聲明別名屬性的對應關係:

  parse(process.argv.slice(2), {
    alias: {
      'save-dev': 'S'
    }
  })

  上述方式符合正常的理解:設置參數對應的別名。但這是一個「單向查找關係」,需要轉化為:

  "alias": {
    "save-dev": ["s"],
    "s": ["save-dev"]
  }

  因為對於使用者來說,只會選擇一種方式傳遞參數。對於開發者的話需要根據任意一個別名找到其相關聯的別名:

function parse(args = [], options = {}) {
  const output = { _: [] };

  const { alias } = options;

  const hasAlias = alias !== void 666;

  if (hasAlias) {
    Object.keys(alias).forEach(key => {
      alias[key] = toArr(alias[key]);
      alias[key].forEach((item, index) => {
        (alias[item] = alias[key].concat(key)).splice(index, 1);
      })
    })
  }

  // 省略解析代碼
  ...

 if (hasAlias) {
    Object.keys(output).forEach(key => {
      const arr = alias[key] || [];
      arr.forEach(sub => output[sub] = output[key])
    })
 }

  return output;
}

  除了別名之外,還可以在參數解析之後做如下優化:

成熟的解析庫

  針對一些成熟的命令行參數解析庫可以採用基準測試查看它們的解析效率:

const nopt = require('nopt');
const mri = require('mri');
const yargs = require('yargs-parser');
const minimist = require('minimist');
const { Suite } = require('benchmark');

const bench = new Suite();
const args = ['--name=xiaoming', '-abc', '10', '--save-dev', '--age', '20'];

bench
 .add('minimist     ', () => minimist(args))
 .add('mri          ', () => mri(args))
 .add('nopt         ', () => nopt(args))
 .add('yargs-parser ', () => yargs(args))
 .on('cycle', e => console.log(String(e.target)))
 .run();

  本文的內容主要參考解析效率最高的 mri 庫的源碼,感興趣的同學可以學習其源碼實現。(順便吐槽一下:嵌套三元操作符可讀性真的很差。。)

  雖然上述基準測試中 minimist 效率並不很好,但是其覆蓋了比較全的參數輸入場景。(以上測試用例覆蓋的場景有限)

1.看到這裡了就點個在看支持下吧,你的「點讚,在看是我創作的動力。

2.關注公眾號程式設計師成長指北,回復「1」加入高級前端交流群!「在這裡有好多 前端 開發者,會討論 前端 Node 知識,互相學習」!

3.也可添加微信【ikoala520】,一起成長。

「在看轉發」是最大的支持

相關焦點

  • get、post 請求中常見 content-type 請求頭以及 nodeJs 解析請求參數
    規範把 HTTP 請求分為三個部分:狀態行、請求頭、消息主體。但是,數據發送出去,還要服務端解析成功才有意義。一般服務端語言如 php、python 等,以及它們的 framework,都內置了自動解析常見數據格式的功能。服務端通常是根據請求頭(headers)中的 Content-Type 欄位來獲知請求中的消息主體是用何種方式編碼,再對主體進行解析。
  • 如何使用 npm 管理 NodeJS 包 | Linux 中國
    今天,我們將討論如何使用 npm 管理 NodeJS 包。npm 是最大的軟體註冊中心,包含 600,000 多個包。每天,世界各地的開發人員通過 npm 共享和下載軟體包。在本指南中,我將解釋使用 npm 基礎知識,例如安裝包(本地和全局)、安裝特定版本的包、更新、刪除和管理 NodeJS 包等等。
  • 一些小眾卻有用的 Node.js 包
    yargsyargs 是一個用來處理命令行參數的包,可以幫你處理自行設置的命令行標誌和輸入的任何類型的數據,其中包括布爾值、浮點數和字符串等。這個包非常簡單明了,不需要在項目中編寫大量的樣板代碼。,然後在命令行中執行 node index.js -x 3,會看到如下消息:Usage: index.js -x [num] -y [num]Options: -x [required] -y
  • Node.js模塊化
    在模塊對象中保存了和當前模塊相關信息。在模塊對象中有一個屬性 exports,它的值是一個對象,模塊內部需要被導出的成員都應該存儲在到這 個對象中。/logger")logger("Hello")這裡有幾點注意:如果沒有加文件後綴,會按照以下後綴加載文件.js    fs模塊同步讀取文件編譯執行.json  fs模塊同步讀取文件,用JSON.parse()解析返回結果.node 這是c/c++編寫的擴展文件,通過dlopen()方法編譯其他擴展名  會以
  • Node.js(二)
    簡言之,Node.js中的js不像普通的js具有全局變量,而是以模塊(文件)為作用域,不會汙染其他文件。而用戶書寫的js就是自定義模塊,下載安裝的就是第三方模塊,自帶的js就是核心模塊。既然模塊沒有了全局作用域,那麼我們需要使用模塊就需要進行加載和導出通信規則。
  • Node.js模塊接口七大設計模式
    今天我們將討論七種Node.js模塊接口設計模式,在實際工作中,它們經常會被混合起來使用:導出一個命名空間導出一個函數導出一個高階函數導出一個構造函數導出一個單體擴展一個全局對象運用一個猴子補丁(Monkey Patch)require,exports和module.exports
  • Node.js幾種創建子進程方法
    不同參數間使用空格隔開,可用於複雜的命令。const { exec } = require('child_process')exec('cat *.js bad_file | wc -l')exec方法用於異步創建一個新的子進程,可以接受一個callback。
  • 【譯】Node.js的eventloop,timers和process.nextTick()
    事件循環機制解析當啟動 Node.js 時,它會初始化 eventloop,處理提供的輸入腳本(或者是丟入REPL,本文檔中沒有涉及到 REPL 相關知識,REPL 詳情請查看 https://nodejs.org/api/repl.html#repl_repl)這會使用 async API  calls(異步 api 調用),安排定時器
  • Node.js v17 來了,看看都有哪些新功能?
    錯誤堆棧增加 Node.js 版本堆棧跟蹤是診斷應用程式錯誤信息的重要組成部分,在 Node.js v17 版本中,如果因為一些致命的錯誤導致進程退出,在錯誤堆棧的尾部將包含 Node.js 的版本信息。
  • 讓flag支持從文件中讀取命令行參數
    常規的使用都是在命令行中啟動服務的時候一一的輸入,讓程序解析。今天給大家介紹一種可以從文件中讀取命令行參數的實現方法。下面我們通過代碼來演示下flag的常規應用。下面我們就介紹通過讓程序從配置文件中讀取的方法。常規應用中,我們看到,讀取並解析命令行參數的邏輯主要在flag.Parse中。
  • 理解 Node.js 中的 Worker Threads
    是單線程的,但卻可以通過回調、Promises 和 async/await 來儘可能將操作卸載(offloading)到系統內核一個 JS 引擎實例:執行 JavaScript 代碼的程序一個 Node.js 實例:執行 Node.js 代碼的程序換句話說,Node 執行在一個單線程上,並且同一時間,事件循環上只有一個進程、一份代碼、一次執行(代碼無法並行執行
  • Go 實戰 | 讓你的 flag 支持從文件中讀取命令行參數
    今天給大家介紹一個在項目中如何將命令行參數組織到文件中並進行解析的案例。golang標準庫提供了flag包來處理命令行參數。常規的使用都是在命令行中啟動服務的時候一一的輸入,讓程序解析。今天給大家介紹一種可以從文件中讀取命令行參數的實現方法。下面我們通過代碼來演示下flag的常規應用。
  • 深入研究 Node.js 的回調隊列
    在本文中,我們將深入研究 Node.js 中的隊列:它們是什麼,它們如何工作(通過事件循環)以及它們的類型。Node.js 中的隊列是什麼?隊列是 Node.js 中用於組織異步操作的數據結構。這些操作以不同的形式存在,包括HTTP請求、讀取或寫入文件操作、流等。
  • Node 最新 Module 導入導出規範
    的參數傳入或通過 STDIN 傳入 nodeNode.js 會將所有其他形式的輸入視為 CommonJS,例如 .js 文件們最近的父 package.json 沒有 "type" 欄位,或啟動 node 時沒有傳入 --input-type 標誌。
  • Cypress系列(40)- 命令行運行 Cypress
    前言前面也介紹過 Cypress 命令行,先來看看它的語法格式cypress <command> [options] cypress open 簡介簡介 最簡單的命令,指定的參數將自動應用於你通過測試運行器打開的項目這些參數將應用於每一次測試運行,直到關閉測試運行器為止指定的參數將會覆蓋配置文件 cypress.json 中的相同參數 可選參數列表
  • Python | 使用argparse解析命令行參數
    今天是Python專題第27篇文章,我們來聊聊Python當中的命令行參數工具argparse。命令行參數工具是我們非常常用的工具,比如當我們做實驗希望調節參數的時候,如果參數都是通過硬編碼寫在代碼當中的話,我們每次修改參數都需要修改對應的代碼和邏輯顯然這不太方便。
  • Node.js v17來了,Nodejs能崛起嗎?
    Node.js v17 版本已發布,在這個版本中提供了一些新的功能:基於 Promise 的其它核心模塊 API       錯誤堆棧尾部增加 Node.js 版本信息OpenSSL 3.0 支持v8 JavaScript 引擎更新至 9.5。
  • 教你用 Node 創建 CLI 工具
    /bin//index.js" }複製代碼#!/usr/bin/env nodeconsole.log("Hello World!!!");複製代碼如果 npm link 報錯,windows/mac 嘗試使用管理員身份輸入。在命令行執行wbiao
  • 分享4個Linux中Node.js的進程管理器
    它還支持應用程式日誌記錄,群集和負載平衡,以及許多其他有用的流程管理功能。另請參閱:2019年為開發人員提供的14個最佳NodeJS框架包管理器尤其適用於在生產環境中部署Node.js應用程式。 在本文中,我們將回顧Linux系統中Node.js應用程式管理的四個進程管理器。1.