Browse Source

node 服务命令行参数解析

zhuth 6 years ago
parent
commit
f450465127
3 changed files with 464 additions and 1 deletions
  1. 15 0
      server/Readme.md
  2. 434 0
      server/command.js
  3. 15 1
      server/server.js

+ 15 - 0
server/Readme.md

@@ -0,0 +1,15 @@
+Node 接口服务
+```
+server
+-- data // 解析条件参数并返回数据
+---- index.js // 映射URL与解析方法
+-- command.js // Nodejs执行命令解析模块
+-- getData.js // 业务分发模块
+-- server.js // 服务启动
+```
+
+启动服务指令:
+```cmd
+node ./server.js --port 24002
+// node ./server.js [ -p | --port ] <port number>
+```

+ 434 - 0
server/command.js

@@ -0,0 +1,434 @@
+exports = module.exports = new Command();
+
+exports.Command = Command;
+
+exports.Option = Option;
+
+/**
+ * 选项参数
+ * @param flags 选项标识
+ * @param description 描述信息
+ * @constructor 选项参数构造器
+ */
+function Option(flags, description) {
+    this.flags = flags; // 选项标识
+    this.required = ~flags.indexOf('<'); // 必须,包含<>
+    this.optional = ~flags.indexOf('['); // 可选,包含[]
+    this.bool = !~flags.indexOf('-no-'); // 禁用,包含-no-
+    flags = flags.split(/[ ,|]+/);
+    if (flags.length > 1 && !/^[[<]/.test(flags[1]))
+        this.short = flags.shift(); // 短选项
+    this.long = flags.shift();// 长选项
+    this.description = description || ''; // 描述信息
+}
+
+/**
+ * 参数名称,以长选项为名字
+ * @returns {XML|string}
+ */
+Option.prototype.name = function () {
+    return this.long
+        .replace('--', '')
+        .replace('no-', '');
+};
+
+/**
+ * 判断是短选项还是长选项
+ * @param arg
+ * @returns {boolean}
+ */
+Option.prototype.is = function (arg) {
+    return arg === this.short || arg === this.long;
+};
+
+/**
+ * 命令行
+ * @param name 命令行名称
+ * @constructor 命令行构造器
+ */
+function Command(name) {
+    this.options = []; // 选项集合
+    this._allowUnknownOption = false; // 是否允许未知参数
+    this._args = []; // 参数集合
+    this._name = name || ''; // 名称
+}
+
+Command.prototype.name = function (str) {
+    this._name = str;
+    return this;
+};
+
+/**
+ * 版本号
+ * @param str 版本号
+ * @param flags 选项
+ * @returns {*}
+ * @api public
+ */
+Command.prototype.version = function (str, flags) {
+    if (0 === arguments.length)
+        return this._version;
+    this._version = str;
+    flags = flags || '-V, --version';
+    this.option(flags, 'output the version number');
+
+    return this;
+};
+
+/**
+ * 使用说明
+ * @param str
+ * @returns {*}
+ * @api public
+ */
+Command.prototype.usage = function (str) {
+    var args = this._args.map(function (arg) {
+        return humanReadableArgName(arg);
+    });
+
+    var usage = '[options]'
+        + (this._args.length ? ' ' + args.join(' ') : '');
+
+    if (0 === arguments.length)
+        return this._usage || usage;
+
+    this._usage = str;
+
+    return this;
+};
+
+/**
+ * 命令行选项赋值
+ * @param flags 选项参数
+ * @param description 描述
+ * @param fn
+ * @param defaultValue
+ * @returns {Command}
+ * @api public
+ */
+Command.prototype.option = function (flags, description, fn, defaultValue) {
+    var self = this
+        , option = new Option(flags, description)
+        , oname = option.name()
+        , name = camelcase(oname);
+
+    // 参数为三个
+    if (typeof fn !== 'function') {
+        if (fn instanceof RegExp) {
+            var regex = fn;
+            fn = function (val, def) {
+                var m = regex.exec(val);
+                return m ? m[0] : def;
+            }
+        } else {
+            defaultValue = fn;
+            fn = null;
+        }
+    }
+
+    // 为禁用--no-*,可选[optional],必选<required>设置默认值
+    if (false === option.bool || option.optional || option.required) {
+        if (false === option.bool)
+            defaultValue = true; // 默认为true
+        if (undefined !== defaultValue)
+            self[name] = defaultValue;
+    }
+
+    // 注册
+    this.options.push(option);
+
+    return this;
+};
+
+/**
+ * 解析参数
+ * @param argv 参数
+ * @returns {Command}
+ * @api public
+ */
+Command.prototype.parse = function (argv) {
+    // 存储原始参数
+    this.rawArgs = argv;
+    // 猜名字
+    this._name = this._name || argv[0];
+    // 解析参数
+    var parsed = this.parseOptions(this.normalize(argv));
+
+    var args = this.args = parsed.args;
+
+    var result = this.parseArgs(this.args, parsed.unknown);
+
+    return result;
+};
+
+/**
+ * 允许未知选项
+ * @param arg
+ * @returns {Command}
+ * @api public
+ */
+Command.prototype.allowUnknownOption = function (arg) {
+    this._allowUnknownOption = arguments.length === 0 || arg;
+    return this;
+};
+
+/**
+ * 规范化参数,主要处理多个短选项如:-xvf和长选项:--options=xxx
+ * @param args
+ * @returns {Array}
+ * @api private
+ */
+Command.prototype.normalize = function (args) {
+    var ret = []
+        , arg
+        , lastOpt
+        , index;
+
+    for (var i = 0, len = args.length; i < len; ++i) {
+        arg = args[i];
+        if (i > 0) {
+            lastOpt = this.optionFor(args[i - 1]);
+        }
+        if (arg === '--') {
+            ret = ret.concat(args.slice(i));
+            break;
+        } else if (lastOpt && lastOpt.required) {
+            ret.push(arg);
+        } else if (arg.length > 1 && '-' === arg[0] && '-' !== arg[1]) {
+            arg.slice(1).split('').forEach(function (c) {
+                ret.push('-' + c);
+            });
+        } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
+            ret.push(arg.slice(0, index), arg.slice(index + 1));
+        } else {
+            ret.push(arg);
+        }
+    }
+    return ret;
+};
+
+/**
+ * 解析参数
+ * @param argv
+ * @returns {{args: Array, unknown: Array}}
+ * @api private
+ */
+Command.prototype.parseOptions = function (argv) {
+    var args = []
+        , len = argv.length
+        , literal
+        , option
+        , arg;
+
+    var unknownOptions = [];
+
+    // 解析选项
+    for (var i = 0; i < len; ++i) {
+        arg = argv[i];
+
+        // 参数后为'-- xxx'时表示为文字而非参数
+        if ('--' === arg) {
+            literal = true;
+            continue;
+        }
+        if (literal) {
+            args.push(arg);
+            continue;
+        }
+
+        // 找到匹配的选项
+        option = this.optionFor(arg);
+
+        // 定义
+        if (option) {
+            if (option.required) { // 选项必须
+                arg = argv[++i];
+                if (undefined === arg || null === arg)
+                    return this.optionMissingArgument(option);
+                this[option.name()] = arg;
+            } else if (option.optional) { // 选项可选
+                arg = argv[i + 1];
+                if (undefined === arg || null === arg || ('-' === arg[0] && '-' !== arg)) {
+                    arg = null;
+                } else {
+                    ++i;
+                }
+                this[option.name()] = arg;
+            } else {
+                if ('version' !== option.name()) {
+                    this[option.name()] = true;
+                } else {
+                    this.outputVersion();
+                    this.exit();
+                }
+            }
+            args.push(arg);
+            continue;
+        }
+
+        // 未知选项
+        if (arg.length > 1 && '-' === arg[0]) {
+            unknownOptions.push(arg);
+            if (argv[i + 1] && '-' !== argv[i + 1][0]) {
+                unknownOptions.push(argv[++i]);
+            }
+            continue;
+        }
+
+        args.push(arg);
+    }
+
+    return {args: args, unknown: unknownOptions};
+};
+
+/**
+ * 参数匹配选项
+ * @param arg
+ * @returns {*}
+ * @api private
+ */
+Command.prototype.optionFor = function (arg) {
+    for (var i = 0, len = this.options.length; i < len; ++i) {
+        if (this.options[i].is(arg)) {
+            return this.options[i];
+        }
+    }
+};
+
+/**
+ * 解析参数
+ * @param args
+ * @param unknown
+ * @returns {Command}
+ */
+Command.prototype.parseArgs = function (args, unknown) {
+    var name;
+    if (args.length) {
+        name = args[0];
+    } else {
+
+        outputHelpIfNecessary(this, unknown);
+
+        if (unknown.length > 0) {
+            this.unknownOption(unknown[0]);
+        }
+    }
+    return this;
+};
+
+/**
+ * 未知选项
+ * @param flag
+ */
+Command.prototype.unknownOption = function (flag) {
+    if (this._allowUnknownOption)
+        return;
+    console.error();
+    console.error("error: unknown option `%s'", flag);
+    console.error();
+    this.exit();
+};
+
+Command.prototype.optionMissingArgument = function (option, flag) {
+    console.error();
+    if (flag) {
+        console.error("error: option `%s' argument missing, got `%s'", option.flags, flag);
+    } else {
+        console.error("error: option `%s' argument missing", option.flags);
+    }
+    console.error();
+    this.exit();
+};
+
+Command.prototype.outputVersion = function () {
+    console.log(this._version);
+};
+
+Command.prototype.outputHelp = function (cb) {
+    if (!cb) {
+        cb = function (passthru) {
+            return passthru;
+        }
+    }
+    console.log(cb(this.helpInformation()));
+};
+
+Command.prototype.helpInformation = function () {
+    var desc = [];
+    if (this._description) {
+        desc = [
+            '  ' + this._description
+            , ''
+        ];
+    }
+
+    var cmdName = this._name;
+    if (this._alias) {
+        cmdName = cmdName + '|' + this._alias;
+    }
+    var usage = [
+        ''
+        , '  Usage: ' + cmdName + ' ' + this.usage()
+        , ''
+    ];
+
+    var options = [
+        '  Options:'
+        , ''
+        , '' + this.optionHelp().replace(/^/gm, '    ')
+        , ''
+        , ''
+    ];
+
+    return usage
+        .concat(desc)
+        .concat(options)
+        .join('\n');
+};
+
+Command.prototype.optionHelp = function () {
+    var width = this.largestOptionLength();
+
+    return [pad('-h, --help', width) + '  ' + 'output usage information']
+        .concat(this.options.map(function (option) {
+            return pad(option.flags, width) + '  ' + option.description;
+        }))
+        .join('\n');
+};
+Command.prototype.largestOptionLength = function () {
+    return this.options.reduce(function (max, option) {
+        return Math.max(max, option.flags.length);
+    }, 0);
+};
+
+Command.prototype.exit = function () {
+    process.exit();
+};
+
+function camelcase(flag) {
+    return flag.split('-').reduce(function (str, word) {
+        return str + word[0].toUpperCase() + word.slice(1);
+    });
+}
+
+function outputHelpIfNecessary(cmd, options) {
+    options = options || [];
+    for (var i = 0; i < options.length; i++) {
+        if (options[i] === '--help' || options[i] === '-h') {
+            cmd.outputHelp();
+            cmd.exit();
+        }
+    }
+}
+
+function humanReadableArgName(arg) {
+    var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
+    return arg.required
+        ? '<' + nameOutput + '>'
+        : '[' + nameOutput + ']'
+}
+
+function pad(str, width) {
+    var len = Math.max(0, width - str.length);
+    return str + new Array(len + 1).join(' ');
+}

+ 15 - 1
server/server.js

@@ -1,7 +1,20 @@
 var http = require("http");
 var url = require("url");
 var getData = require("./getData");
+var command = require("./command");
 
+/**
+ * 参数
+ * @type {Command}
+ */
+var commandParams = command
+    .version('0.0.1')
+    .option('-p, --port <number>', 'change server port', 3000)
+    .parse(process.argv.slice(2));
+    
+var port = commandParams.port;
+
+console.log('Server starting...');
 function onRequest(request, response) {
     // 默认情况下,如果url路径中有中文,则会对中文进行URI编码,所以服务端要想获取中文需要对url进行URI解码
     // console.log(encodeURI(request.url));
@@ -22,4 +35,5 @@ function onRequest(request, response) {
     });
     getData(request, response);
 }
-http.createServer(onRequest).listen(3000);
+http.createServer(onRequest).listen(port);
+console.log('Server started on port ' + port);