Skip to content

Commit

Permalink
self-build argparser
Browse files Browse the repository at this point in the history
  • Loading branch information
nondanee committed Feb 10, 2019
1 parent 9dd0516 commit 0ff500a
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 55 deletions.
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,19 @@ $ docker-compose up

```
$ node app.js -h
Usage: unblockneteasemusic [options] [value ...]
Options:
-v, --version output the version number
-p, --port <port> specify server port
-u, --proxy-url <url> request through upstream proxy
-f, --force-host <host> force the netease server ip
-o, --match-order <name,...> set priority of sources
-t, --token <token> set up http basic authentication
-s, --strict enable proxy limitation
-h, --help output usage information
usage: unblockneteasemusic [-v] [-p port] [-u url] [-f host]
[-o source [source ...]] [-t token] [-s] [-h]
optional arguments:
-v, --version output the version number
-p port, --port port specify server port
-u url, --proxy-url url request through upstream proxy
-f host, --force-host host force the netease server ip
-o source [source ...], --match-order source [source ...]
set priority of sources
-t token, --token token set up http basic authentication
-s, --strict enable proxy limitation
-h, --help output usage information
```

## 使用
Expand Down
37 changes: 15 additions & 22 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
#!/usr/bin/env node

const package = require('./package.json')
const config = require('./config.json')

try{
Object.assign(config, require('commander')
.name(package.name)
.version(package.version, '-v, --version')
.usage('[options] [value ...]')
.option('-p, --port <port>', 'specify server port')
.option('-u, --proxy-url <url>', 'request through upstream proxy')
.option('-f, --force-host <host>', 'force the netease server ip')
.option('-o, --match-order <name,...>', 'set priority of sources')
.option('-t, --token <token>', 'set up http basic authentication')
.option('-s, --strict', 'enable proxy limitation')
.parse(process.argv))
}catch(error){}
const config = require('./cli.js')
.program({name: package.name, version: package.version})
.argument(['-v', '--version'], {action: 'version'})
.argument(['-p', '--port'], {metavar: 'port', help: 'specify server port'})
.argument(['-u', '--proxy-url'], {metavar: 'url', help: 'request through upstream proxy'})
.argument(['-f', '--force-host'], {metavar: 'host', help: 'force the netease server ip'})
.argument(['-o', '--match-order'], {metavar: 'source', nargs: '+', help: 'set priority of sources'})
.argument(['-t', '--token'], {metavar: 'token', help: 'set up http basic authentication'})
.argument(['-s', '--strict'], {action: 'store_true', help: 'enable proxy limitation'})
.argument(['-h', '--help'], {action: 'help'})
.parse(process.argv)

if(config.port && (isNaN(config.port) || config.port < 1 || config.port > 65535)){
console.log('Port must be a number higher than 0 and lower than 65535.')
Expand All @@ -31,7 +27,7 @@ if(config.forceHost && !/\d+\.\d+\.\d+\.\d+/.test(config.forceHost)){
}
if(config.matchOrder){
const provider = ['qq', 'xiami', 'baidu', 'kugou', 'kuwo', 'migu', 'joox']
const candidate = config.matchOrder.split(/\s*\W\s*/)
const candidate = config.matchOrder
if(candidate.some((key, index) => index != candidate.indexOf(key))){
console.log('Please check the duplication in match order.')
process.exit(1)
Expand All @@ -50,18 +46,15 @@ if(config.token && !/\S+:\S+/.test(config.token)){
const parse = require('url').parse
const hook = require('./hook')
const server = require('./server')
const port = config.port
const port = config.port || 8080

global.proxy = config.proxyUrl ? parse(config.proxyUrl) : null
global.hosts = {}, hook.target.host.forEach(host => global.hosts[host] = config.forceHost)
config.strict ? server.whitelist = ['music.163.com', 'music.126.net'] : server.blanklist = []
server.authentication = config.token || null

const dns = host =>
new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error? reject(error) : resolve(records.map(record => record.address))))

const httpdns = host =>
require('./request')('POST', 'https://music.httpdns.c.163.com/d', {}, host).then(response => response.json()).then(jsonBody => jsonBody.dns[0].ips)
const dns = host => new Promise((resolve, reject) => require('dns').lookup(host, {all: true}, (error, records) => error? reject(error) : resolve(records.map(record => record.address))))
const httpdns = host => require('./request')('POST', 'https://music.httpdns.c.163.com/d', {}, host).then(response => response.json()).then(jsonBody => jsonBody.dns[0].ips)

Promise.all([httpdns(hook.target.host[0])].concat(hook.target.host.map(host => dns(host))))
.then(result => {
Expand Down
161 changes: 161 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const cli = {
_prog: {},
_args: [],
program: (option = {}) => {
cli._prog = option
return cli
},
argument: (flag, option = {}) => {
// name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.
// dest - The name of the attribute to be added to the object returned by parse_args().

// nargs - The number of command-line arguments that should be consumed. // N, ?, *, +, REMAINDER
// action - The basic type of action to be taken when this argument is encountered at the command line. // store, store_true, store_false, append, append_const, count

// const - A constant value required by some action and nargs selections. (supporting store_const and append_const action)

// metavar - A name for the argument in usage messages.
// help - A brief description of what the argument does.

// required - Whether or not the command-line option may be omitted (optionals only).
// default - The value produced if the argument is absent from the command line.
// type - The type to which the command-line argument should be converted.
// choices - A container of the allowable values for the argument.

flag = Array.isArray(flag) ? flag : [flag]
option.flag = flag
option.positional = !flag[0].startsWith('-')
option.action = option.action || 'store'
option.dest = option.dest || flag.slice(-1)[0].toLowerCase().replace(/^-+/, '').replace(/-[a-z]/g, character => character.slice(1).toUpperCase())
option.help = option.help || {'help': 'output usage information', 'version': 'output the version number'}[option.action]
cli._args.push(option)
return cli
},
parse: argv => {
let optionals = {}, positionals = cli._args.map((option, index) => option.positional ? index : null).filter(index => index !== null)
cli._args.forEach((option, index) => {if(!option.positional){option.flag.forEach(flag => optionals[flag] = index)}})

cli._prog.name = cli._prog.name || require('path').parse(argv[1]).base
let args = argv.slice(2)
.reduce((result, part) => /^-[^-]/.test(part) ? result.concat(part.slice(1).split('').map(string => '-' + string)) : result.concat(part), [])

let pointer = 0
while(pointer < args.length) {
let value = args[pointer]
let picked = null
let index = value.startsWith('-') ? optionals[value] : positionals.shift()
if(index == undefined) value.startsWith('-') ? error(`no such option: ${value}`) : error(`extra arguments found: ${value}`)
if(value.startsWith('-')) pointer += 1
let action = cli._args[index].action

let step = args.slice(pointer).findIndex(part => part in optionals)
let next = step === -1 ? args.length : step + pointer

if(['help', 'version'].includes(action)){
if(action === 'help') help()
else if(action === 'version') version()
}
else if(['store_true', 'store_false'].includes(action)){
picked = action === 'store_true'
}
else{
picked = args.slice(pointer, next)
if(picked.length === 0){
cli._args[index].positional ?
error(`the following arguments are required: ${value}`) :
error(`arguments: ${value} expected at least one argument`)
}
if(cli._args[index].nargs != '+'){
picked = picked[0]
pointer += 1
}
else{
pointer = next
}
}
cli[cli._args[index].dest] = picked
}
if(positionals.length){
error(`the following arguments are required: ${positionals.map(index => cli._args[index].flag[0]).join(', ')}`)
}
// cli._args.forEach(option => console.log(option.dest, cli[option.dest]))
return cli
}
}

const pad = length => (new Array(length + 1)).join(' ')

const usage = () => {
let options = cli._args.map(option => {
let flag = option.flag[0]
let name = option.metavar || option.dest
if(option.positional){
if(option.nargs === '+')
return `${name} [${name} ...]`
else
return `${name}`
}
else{
if(['store_true', 'store_false', 'help', 'version'].includes(option.action))
return `[${flag}]`
else if(option.nargs === '+')
return `[${flag} ${name} [${name} ...]]`
else
return `[${flag} ${name}]`
}
}).map(name => ' ' + name)
let maximum = 80
let prefix = `usage: ${cli._prog.name}`
let lines = [prefix]

options.forEach(option => {
lines[lines.length - 1].length + option.length < maximum ?
lines[lines.length - 1] += option :
lines.push(pad(prefix.length) + option)
})
console.log(lines.join('\n'))
}

const help = () => {
usage()
let positionals = cli._args.filter(option => option.positional)
.map(option => [option.metavar || option.dest, option.help])
let optionals = cli._args.filter(option => !option.positional)
.map(option => {
let flags = option.flag
let name = option.metavar || option.dest
let use = ''
if(['store_true', 'store_false', 'help', 'version'].includes(option.action))
use = flags.map(flag => `${flag}`).join(', ')
else if(option.nargs === '+')
use = flags.map(flag => `${flag} ${name} [${name} ...]`).join(', ')
else
use = flags.map(flag => `${flag} ${name}`).join(', ')
return [use, option.help]
})
let align = Math.max.apply(null, positionals.concat(optionals).map(option => option[0].length))
align = align > 26 ? 26 : align
const publish = option => {
option[0].length > align ?
console.log(` ${option[0]}\n${pad(align + 4)}${option[1]}`) :
console.log(` ${option[0]}${pad(align - option[0].length)} ${option[1]}`)
}
if(positionals.length) console.log('\npositional arguments:')
positionals.forEach(publish)
if(optionals.length) console.log('\noptional arguments:')
optionals.forEach(publish)
process.exit()
}

const version = () => {
console.log(cli._prog.version)
process.exit()
}

const error = message => {
usage()
console.log(cli._prog.name + ':', 'error:', message)
process.exit(1)
}

module.exports = cli
7 changes: 0 additions & 7 deletions config.json

This file was deleted.

10 changes: 1 addition & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,5 @@
},
"author": "nondanee",
"license": "MIT",
"dependencies": {
"commander": "^2.18.0"
}
"dependencies": {}
}
3 changes: 1 addition & 2 deletions request.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ const request = (method, url, headers, body) => {
return request(method, url.resolve(response.headers.location), headers, body)
else
return Object.assign(response, {url: url, body: raw => read(response, raw), json: () => json(response), jsonp: () => jsonp(response)})
}
)
})
}

const read = (connect, raw) => new Promise((resolve, reject) => {
Expand Down

0 comments on commit 0ff500a

Please sign in to comment.