topbit 3.1.0 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib/npargv.js CHANGED
@@ -1,354 +1,298 @@
1
1
  'use strict'
2
2
 
3
3
  /**
4
- * {
5
- * '-a' : {
6
- * name: 'a',
7
- * type: 'number|string|int|float|bool',
8
- * min: 0,
9
- * max: 100,
10
- * match: RegExp,
11
- * default : any
12
- * },
13
- *
14
- * '$2': {
15
- *
16
- * },
17
- * {
18
- * '--port=' : {
19
- * name : 'port',
20
- * type : 'int',
21
- * min: 2000,
22
- * max: 5000,
23
- * default: 3456
24
- * }
25
- * }
26
- * }
27
- *
4
+ * 验证并转换数值
28
5
  */
6
+ function validateAndConvert(name, rule, value) {
7
+ let val = value
29
8
 
30
- function setVal (a, ainfo, obj, next) {
31
-
32
- if (ainfo.type && ['int', 'float', 'number'].indexOf(ainfo.type) >= 0) {
33
- if (isNaN(next)) {
34
- return {
35
- ok: false,
36
- message: `${a} 类型错误,要求参数必须是数字类型:${next}`
37
- }
38
- }
39
-
40
- if (ainfo.type === 'int' || ainfo.type === 'number') {
41
- next = parseInt(next)
42
- } else {
43
- next = parseFloat(next)
44
- }
45
- }
46
-
47
- if (ainfo.min !== undefined) {
48
- if (next < ainfo.min) {
49
- return {
50
- ok: false,
51
- message: `${a} 数值不能低于 ${ainfo.min}`
52
- }
53
- }
54
- }
55
-
56
- if (ainfo.max !== undefined) {
57
- if (next > ainfo.max) {
58
- return {
59
- ok: false,
60
- message: `${a} 数值不能大于 ${ainfo.max}`
61
- }
9
+ // 1. 类型检查与转换
10
+ if (['int', 'float', 'number'].includes(rule.type)) {
11
+ if (isNaN(val)) {
12
+ throw new Error(`${name} 类型错误,要求参数必须是数字类型,当前值为:${val}`)
62
13
  }
63
- }
64
-
65
- if (ainfo.match && ainfo.match instanceof RegExp) {
66
- if (!ainfo.match.test(next)) {
67
- return {
68
- ok: false,
69
- message: `${a} 数值无法匹配 ${next}`
70
- }
14
+ if (rule.type === 'int') {
15
+ val = parseInt(val, 10)
16
+ } else {
17
+ val = parseFloat(val)
71
18
  }
72
19
  }
73
20
 
74
- let val = next
75
-
76
- if (ainfo.callback && typeof ainfo.callback === 'function') {
77
- val = ainfo.callback(next)
78
- if (val === undefined) val = next
79
- }
80
-
81
- obj[ ainfo.name || a ] = val
82
-
83
- return {
84
- ok: true,
85
- message: ''
86
- }
87
-
88
- }
89
-
90
- function checkAndSet (a, ainfo, obj, next) {
91
-
92
- if (typeof ainfo === 'boolean') {
93
- obj[a] = true
94
- return {
95
- ok: true,
96
- message: '',
97
- op: 'none'
98
- }
21
+ // 2. 范围检查
22
+ if (rule.min !== undefined && val < rule.min) {
23
+ throw new Error(`${name} 数值不能低于 ${rule.min}`)
99
24
  }
100
-
101
- if (ainfo.type === 'bool' || ainfo.type === 'boolean') {
102
- obj[ ainfo.name || a ] = true
103
-
104
- return {
105
- ok: true,
106
- message: '',
107
- op: 'none'
108
- }
25
+ if (rule.max !== undefined && val > rule.max) {
26
+ throw new Error(`${name} 数值不能大于 ${rule.max}`)
109
27
  }
110
28
 
111
- if (next === null) {
112
- return {
113
- ok: false,
114
- message: `${a} 必须携带参数。`
29
+ // 3. 正则匹配
30
+ if (rule.match && rule.match instanceof RegExp) {
31
+ if (!rule.match.test(String(value))) { // 确保用原始字符串测试
32
+ throw new Error(`${name} 格式不匹配`)
115
33
  }
116
34
  }
117
35
 
118
- let r = setVal(a, ainfo, obj, next)
119
-
120
- if (!r.ok && ainfo.default !== undefined) {
121
- r.ok = true
122
- }
123
-
124
- if (r.ok) {
125
- r.op = 'next'
126
-
127
- if (a[a.length - 1] === '=') {
128
- r.op = 'none'
129
- }
36
+ // 4. 回调处理
37
+ if (typeof rule.callback === 'function') {
38
+ const callbackVal = rule.callback(val)
39
+ if (callbackVal !== undefined) val = callbackVal
130
40
  }
131
41
 
132
- return r
42
+ return val
133
43
  }
134
44
 
135
- function setAutoDefault (opts, k) {
136
- switch (opts[k].type) {
45
+ /**
46
+ * 自动填充默认值配置
47
+ */
48
+ function applyAutoDefault(rule) {
49
+ if (rule.default !== undefined) return
50
+
51
+ switch (rule.type) {
137
52
  case 'number':
138
53
  case 'int':
139
54
  case 'float':
140
- opts[k].default = 0
141
- if (opts[k].min)
142
- opts[k].default = opts[k].min;
55
+ rule.default = rule.min !== undefined ? rule.min : 0
143
56
  break
144
-
145
57
  case 'string':
146
- opts[k].default = ''
58
+ rule.default = ''
147
59
  break
148
-
149
60
  case 'bool':
150
61
  case 'boolean':
151
- opts[k].default = false
62
+ rule.default = false
152
63
  break
153
64
  }
154
65
  }
155
66
 
156
- /*
157
- * opts['@autoDefault'] = true 表示自动设定默认值
158
- *
159
- * */
160
-
161
- function parseArgv (options = null, obj = null) {
162
- if (!options) options = {}
163
-
164
- if (obj === null) obj = {}
165
-
166
- let opts = {}
167
-
168
- for (let k in options) {
169
- opts[k] = options[k]
170
- }
171
-
172
- let autoDefault = false
67
+ /**
68
+ * 规范化 Schema 定义
69
+ * 将简写转换为完整对象,处理自动默认值
70
+ */
71
+ function normalizeSchema(schema, autoDefault) {
72
+ const normalized = {}
73
+
74
+ for (let key in schema) {
75
+ let rule = schema[key]
76
+
77
+ // 1. 字符串简写处理: {'-v': 'verbose'} -> {'-v': { name: 'verbose', type: 'boolean' }}
78
+ if (typeof rule === 'string' && rule.trim().length > 0) {
79
+ rule = { name: rule.trim(), type: 'boolean', default: false }
80
+ }
81
+ else if (typeof rule !== 'object' || rule === null) {
82
+ rule = { name: key, type: 'boolean' }
83
+ }
173
84
 
174
- if (opts['@autoDefault'] === undefined) opts['@autoDefault'] = true;
85
+ // 3. 自动推断 Type
86
+ if (!rule.type) {
87
+ if (key.includes('=')) {
88
+ rule.type = 'string'
89
+ } else if (rule.match || rule.callback) {
90
+ rule.type = 'string'
91
+ } else if (rule.min !== undefined || rule.max !== undefined) {
92
+ rule.type = 'int'
93
+ } else if (rule.default !== undefined) {
94
+ const defaultType = typeof rule.default
95
+ rule.type = ['number', 'boolean', 'string'].includes(defaultType) ? defaultType : 'string'
96
+ } else {
97
+ rule.type = 'boolean'
98
+ }
99
+ }
175
100
 
176
- if (opts['@autoDefault'] !== undefined) {
177
- autoDefault = !!opts['@autoDefault']
178
- delete opts['@autoDefault']
179
- }
101
+ // 4. 应用自动默认值
102
+ if (autoDefault) {
103
+ applyAutoDefault(rule)
104
+ }
180
105
 
181
- let commands = []
182
- if (opts['@command'] !== undefined) {
183
- if (typeof opts['@command'] === 'string') {
184
- opts['@command'].split(' ').filter(p => p.length > 0).forEach(a => {
185
- commands.push(a.trim())
186
- })
187
- } else if (Array.isArray(opts['@command'])) {
188
- commands = opts['@command'];
106
+ // 5. 处理别名 (Alias)
107
+ normalized[key] = rule
108
+ if (rule.alias && typeof rule.alias === 'string') {
109
+ normalized[rule.alias] = rule
189
110
  }
190
- delete opts['@command'];
191
111
  }
112
+
113
+ return normalized
114
+ }
192
115
 
193
- let defaultCommand = ''
194
- if (opts['@defaultCommand'] !== undefined && commands.indexOf(opts['@defaultCommand']) >= 0) {
195
- defaultCommand = opts['@defaultCommand'];
196
- delete opts['@defaultCommand'];
116
+ /**
117
+ * 主解析函数
118
+ *
119
+ * @param {Object} schema 参数定义
120
+ * @param {Object} options 配置项 { strict: boolean, autoDefault: boolean, commands: [], defaultCommand: string, argv: [] }
121
+ */
122
+ function parseArgv(schema = {}, options = {}) {
123
+ // --- 1. 配置初始化 ---
124
+ const config = {
125
+ strict: true, // 默认为严格模式,报错即停
126
+ autoDefault: true, // 默认自动生成 default 值
127
+ commands: options.commands && Array.isArray(options.commands) ? options.commands : [],
128
+ defaultCommand: options.defaultCommand || '', // 默认子命令
129
+ argv: process.argv, // 允许传入自定义 argv 用于测试
130
+ ...options
197
131
  }
198
132
 
133
+ if (config.commands && config.commands.length > 0 && !options.defaultCommand) {
134
+ config.defaultCommand = config.commands[0]
135
+ }
136
+
137
+ // --- 2. 预处理 ---
138
+ const rules = normalizeSchema(schema, config.autoDefault)
139
+ const result = {}
140
+ const errors = []
141
+ const unknownList = []
199
142
  let userCommand = null
200
- let commandFromInput = false
201
- if (commands.length > 0) {
202
- if (process.argv.length < 3) {
203
- if (defaultCommand) userCommand = defaultCommand;
204
- else {
205
- return {
206
- ok: false,
207
- message: '请使用子命令:' + commands.join('|'),
208
- args: obj
209
- }
210
- }
211
- } else if (commands.indexOf(process.argv[2]) < 0) {
212
- if (defaultCommand) userCommand = defaultCommand;
213
- else {
214
- return {
215
- ok: false,
216
- message: '不支持的子命令',
217
- args: obj
218
- }
219
- }
143
+
144
+ // 初始化结果对象中的默认值
145
+ for (let key in rules) {
146
+ const rule = rules[key]
147
+ const name = rule.name || key
148
+ if (rule.type === 'bool' || rule.type === 'boolean') {
149
+ if (result[name] === undefined) result[name] = rule.default !== undefined ? rule.default : false
220
150
  } else {
221
- commandFromInput = true
222
- userCommand = process.argv[2]
151
+ if (result[name] === undefined && rule.default !== undefined) result[name] = rule.default
223
152
  }
224
153
  }
225
154
 
226
- let tmp_val
227
-
228
- for (let k in opts) {
229
- if (typeof opts[k] === 'string' && opts[k].trim().length > 0) {
230
- opts[k] = {
231
- name: opts[k].trim(),
232
- type: 'boolean',
233
- default: false
234
- }
235
- }
155
+ // --- 3. 解析子命令 ---
156
+ let index = 2
157
+ if (config.argv[0].endsWith('node')) {
158
+ // 标准 node xxx.js 格式,从下标2开始
159
+ // 如果是 compiled binary 或者是 electron 等环境,可能需要自行调整 config.argv
160
+ }
236
161
 
237
- if (typeof opts[k] !== 'object' || opts[k].toString() !== '[object Object]') {
162
+ if (config.commands.length > 0) {
163
+ const inputCmd = config.argv[index]
238
164
 
239
- opts[k] = {
240
- type : 'boolean',
241
- name : k,
242
- default: false
165
+ if (!inputCmd || inputCmd.startsWith('-')) {
166
+ // 没有提供命令,或者直接开始了参数
167
+ if (config.defaultCommand) {
168
+ userCommand = config.defaultCommand
169
+ } else {
170
+ const msg = `缺少子命令,可用命令: ${config.commands.join('|')}`
171
+ if (config.strict) return { ok: false, message: msg, args: result }
172
+ errors.push(msg)
243
173
  }
244
-
245
- } else if (opts[k].type === undefined) {
246
-
247
- if (k.indexOf('=') > 0) {
248
- opts[k].type = 'string'
249
- } else if (opts[k].match || opts[k].callback) {
250
- opts[k].type = 'string'
174
+ } else if (config.commands.includes(inputCmd)) {
175
+ userCommand = inputCmd
176
+ index++ // 消耗掉命令参数
177
+ } else {
178
+ // 提供了命令但不在列表中
179
+ if (config.defaultCommand) {
180
+ userCommand = config.defaultCommand
181
+ // 注意:这里不 index++,因为当前这个 unknown command 可能是个普通参数?
182
+ // 根据惯例,如果命令不对,通常视为错误,或者回退到 defaultCommand 但把当前词作为参数解析
251
183
  } else {
252
- if (opts[k].min !== undefined || opts[k].max !== undefined) {
253
- opts[k].type = 'int'
254
- } else if (opts[k].default !== undefined) {
255
- tmp_val = typeof opts[k].default
256
- if (tmp_val === 'number' || tmp_val === 'boolean' || tmp_val === 'string') {
257
- opts[k].type = tmp_val
258
- } else {
259
- opts[k].type = 'string'
260
- }
261
- } else {
262
- opts[k].type = 'bool'
263
- }
184
+ const msg = `不支持的子命令: ${inputCmd}`
185
+ if (config.strict) return { ok: false, message: msg, args: result }
186
+ errors.push(msg)
264
187
  }
265
-
266
- }
267
-
268
- autoDefault && opts[k].default === undefined && setAutoDefault(opts, k)
269
-
270
- if (opts[k].type === 'bool' || opts[k].type === 'boolean') {
271
- obj[ opts[k].name || k ] = false
272
- } else if (opts[k].default !== undefined) {
273
- obj[ opts[k].name || k ] = opts[k].default
274
188
  }
275
-
276
189
  }
277
190
 
278
- for (let k in opts) {
279
- if (opts[k].alias && typeof opts[k].alias === 'string' && opts[k].alias !== k) {
280
- opts[opts[k].alias] = opts[k]
281
- }
282
- }
191
+ // --- 4. 遍历参数 ---
192
+ // 计算位置参数的偏移量 (如果有子命令,$1 应该是子命令后的第一个参数)
193
+ const posOffset = index - 1
283
194
 
284
- let a
285
- let next
286
- let next_end = process.argv.length - 1
287
- let r = ''
288
- let i = 2
289
- let offset = 1
290
- if (commands.length > 0 && commandFromInput) {
291
- i++
292
- offset = 2
293
- }
294
-
295
- let ind = 0
296
- let pos_key = ''
297
- let aList = []
298
-
299
- while (i < process.argv.length) {
300
-
301
- //先检测是否存在对位置的引用
302
- pos_key = '$' + `${i-offset}`
303
- if (opts[pos_key]) {
304
- r = checkAndSet(pos_key, opts[pos_key], obj, process.argv[i])
305
- if (!r.ok) {
306
- r.args = obj
307
- return r
195
+ while (index < config.argv.length) {
196
+ let rawArg = config.argv[index]
197
+
198
+ // 4.1 处理位置参数引用 ($1, $2...)
199
+ const posKey = `$${index - posOffset}`
200
+ if (rules[posKey]) {
201
+ const rule = rules[posKey]
202
+ const name = rule.name || posKey
203
+ try {
204
+ result[name] = validateAndConvert(name, rule, rawArg)
205
+ } catch (e) {
206
+ if (config.strict) return { ok: false, message: e.message, args: result }
207
+ errors.push(e.message)
308
208
  }
309
- i++
209
+ index++
310
210
  continue
311
211
  }
312
212
 
313
- a = process.argv[i]
314
-
315
- next = i < next_end ? process.argv[i+1] : null
213
+ // 4.2 解析 Key-Value (处理 --port=8080 这种情况)
214
+ let nextArg = (index + 1 < config.argv.length) ? config.argv[index + 1] : null
215
+ let key = rawArg
216
+ let valStr = null
217
+ let consumeNext = false // 是否消耗了下一个参数
218
+
219
+ const equalIndex = rawArg.indexOf('=')
220
+ if (equalIndex > 0) {
221
+ key = rawArg.substring(0, equalIndex + 1) // 保留 = 号以匹配定义
222
+ if (!rules[key]) {
223
+ key = rawArg.substring(0, equalIndex) // 尝试不带 = 匹配
224
+ }
225
+ valStr = rawArg.substring(equalIndex + 1)
226
+ }
316
227
 
317
- ind = a.indexOf('=')
228
+ // 4.3 匹配定义
229
+ if (rules[key]) {
230
+ const rule = rules[key]
231
+ const name = rule.name || key
318
232
 
319
- if (ind > 0) {
320
- a = a.substring(0, ind+1)
321
- next = process.argv[i].substring(ind+1)
322
- }
233
+ // 布尔类型不需要下一个参数值 (除非显式赋值,暂不支持 --bool=false 写法,按原逻辑处理)
234
+ if (rule.type === 'bool' || rule.type === 'boolean') {
235
+ result[name] = true
236
+ } else {
237
+ // 取值
238
+ let valToProcess = (valStr !== null) ? valStr : nextArg
239
+
240
+ // 如果是从 nextArg 取值的,标记需要跳过下一个循环
241
+ if (valStr === null) {
242
+ if (valToProcess === null) {
243
+ // 已经是最后一个了,却需要参数
244
+ const msg = `${key} 必须携带参数`
245
+ if (config.strict) return { ok: false, message: msg, args: result }
246
+ errors.push(msg)
247
+ }
248
+ consumeNext = true
249
+ }
323
250
 
324
- if (opts[a]) {
325
- r = checkAndSet(a, opts[a], obj, next)
326
- if (!r.ok) {
327
- r.args = obj
328
- return r
251
+ if (valToProcess !== null) {
252
+ try {
253
+ result[name] = validateAndConvert(name, rule, valToProcess)
254
+ } catch (e) {
255
+ if (config.strict) return { ok: false, message: e.message, args: result }
256
+ errors.push(e.message)
257
+ // 出错时,非 strict 模式下保留默认值
258
+ }
259
+ }
329
260
  }
330
261
 
331
- if (r.op === 'next') {
332
- i += 2
333
- continue
262
+ if (consumeNext) index++
263
+ } else {
264
+ // 1. 处理转义字符 (例如输入 \-name,实际意图是文件名 -name)
265
+ if (rawArg.startsWith('\\')) {
266
+ // 去掉开头的反斜杠,将其余部分作为普通参数存入 list
267
+ unknownList.push(rawArg.substring(1))
334
268
  }
335
269
 
336
- } else {
337
- if (a[0] !== '-') {
338
- a[0] !== '\\' ? aList.push(a) : aList.push(a.substring(1))
270
+ // 2. 处理普通参数 (文件名、纯字符串等,不以 - 开头)
271
+ else if (!rawArg.startsWith('-')) {
272
+ unknownList.push(rawArg)
273
+ }
274
+
275
+ // 3. 剩下的即为以 - 开头但未定义的未知 Flag
276
+ else {
277
+ //unknownList.push(rawArg)
278
+ // 这一块可以根据你的需求决定:
279
+ // 选项 A: 忽略 (保持当前逻辑)
280
+ // 选项 B: 报错 (如果 config.strict 为 true)
281
+ // 选项 C: 也放入 list (通常不建议,因为可能是用户输错的参数)
339
282
  }
340
283
  }
341
284
 
342
- i++
285
+ index++
343
286
  }
344
287
 
345
288
  return {
346
- ok: true,
347
- message: '',
348
- args: obj,
349
- list: aList,
289
+ ok: errors.length === 0,
290
+ message: errors.length > 0 ? errors[0] : '', // 向下兼容,返回第一个错误
291
+ errors: errors, // 新增:返回所有错误
292
+ args: result,
293
+ list: unknownList,
350
294
  command: userCommand
351
295
  }
352
296
  }
353
297
 
354
- module.exports = parseArgv
298
+ module.exports = parseArgv
@@ -1,4 +1,4 @@
1
- const titbit = require('../lib/titbit');
1
+ const titbit = require('../src/topbit');
2
2
 
3
3
  let app = new titbit();
4
4
 
@@ -1,4 +1,4 @@
1
- const titbit = require('../lib/titbit.js');
1
+ const titbit = require('../src/topbit.js');
2
2
 
3
3
  var app = new titbit();
4
4