From 32b9bdaab1fbc28576b17de8516164ce0360f292 Mon Sep 17 00:00:00 2001
From: Vitaly Baev <ping@baev.dev>
Date: Sun, 16 Jan 2022 17:05:44 +0300
Subject: [PATCH] Allows passing size or custom alphabet via cli as args (#334)

* Allows passing size or custom alphabet via cli as args

* Increase test coverage, make work with Node 12
---
 README.md          | 13 +++++++--
 README.ru.md       | 13 +++++++--
 bin/nanoid.cjs     | 45 +++++++++++++++++++++++++++++--
 bin/nanoid.test.js | 67 +++++++++++++++++++++++++++++++++++++++++++---
 bin/utils/index.js | 44 ++++++++++++++++++++++++++++++
 5 files changed, 172 insertions(+), 10 deletions(-)
 create mode 100644 bin/utils/index.js

diff --git a/README.md b/README.md
index 33ca42e8..814839bb 100644
--- a/README.md
+++ b/README.md
@@ -468,10 +468,19 @@ npx: installed 1 in 0.63s
 LZfXLFzPPR4NNrgjlWDxn
 ```
 
-If you want to change alphabet or ID size, you should use [`nanoid-cli`].
+Size of generated ID can be specified with `--size` (or `-s`) option:
 
-[`nanoid-cli`]: https://github.com/twhitbeck/nanoid-cli
+```sh
+$ npx nanoid --size 10
+L3til0JS4z
+```
+
+Custom alphabet can be specified with `--alphabet` (or `-a`) option (note that in this case `--size` is required):
 
+```sh
+$ npx nanoid --alphabet abc --size 15
+bccbcabaabaccab
+```
 
 ### Other Programming Languages
 
diff --git a/README.ru.md b/README.ru.md
index bc7f9800..75e1ecf4 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -456,10 +456,19 @@ npx: installed 1 in 0.63s
 LZfXLFzPPR4NNrgjlWDxn
 ```
 
-Для смены алфавита или длины ID есть отдельный проект [`nanoid-cli`].
+Длину генерируемых ID можно передать в аргументе `--size` (или `-s`):
 
-[`nanoid-cli`]: https://github.com/twhitbeck/nanoid-cli
+```sh
+$ npx nanoid --size 10
+L3til0JS4z
+```
+
+Изменить алфавит можно при помощи аргумента `--alphabet` (ли `-a`) (обратите внимание, что в этом случае `--size` обязателен):
 
+```sh
+$ npx nanoid --alphabet abc --size 15
+bccbcabaabaccab
+```
 
 ### Другие языки программирования
 
diff --git a/bin/nanoid.cjs b/bin/nanoid.cjs
index e14116f6..16ab4934 100755
--- a/bin/nanoid.cjs
+++ b/bin/nanoid.cjs
@@ -1,5 +1,46 @@
 #!/usr/bin/env node
 
-let { nanoid } = require('..')
+let { nanoid, customAlphabet } = require('..')
+let { parseArgs } = require('./utils')
 
-process.stdout.write(nanoid() + '\n')
+let parsedArgs = parseArgs(process.argv)
+
+if (parsedArgs.help) {
+  process.stdout.write(`
+	Usage
+	  $ nanoid [options]
+
+	Options
+	  -s, --size       Generated ID size
+	  -a, --alphabet   Alphabet to use
+	  -h, --help       Show this help
+
+	Examples
+	  $ nano --s=15
+	  S9sBF77U6sDB8Yg
+
+	  $ nano --size=10 --alphabet=abc
+	  bcabababca
+`)
+  process.exit()
+}
+
+let alphabet = parsedArgs.alphabet || parsedArgs.a
+let size = parsedArgs.size || parsedArgs.s ? Number(parsedArgs.size || parsedArgs.s) : undefined
+
+if (typeof size !== 'undefined' && (Number.isNaN(size) || size <= 0)) {
+  process.stderr.write('Size must be positive integer\n')
+  process.exit(1)
+}
+
+if (alphabet) {
+  if (typeof size === 'undefined') {
+    process.stderr.write('You must also specify size option, when using custom alphabet\n')
+    process.exit(1)
+  }
+  process.stdout.write(customAlphabet(alphabet, size)())
+} else {
+  process.stdout.write(nanoid(size))
+}
+
+process.stdout.write('\n')
diff --git a/bin/nanoid.test.js b/bin/nanoid.test.js
index 43753706..005af89b 100644
--- a/bin/nanoid.test.js
+++ b/bin/nanoid.test.js
@@ -1,15 +1,74 @@
-let { test } = require('uvu')
-let { is, match } = require('uvu/assert')
+let { suite } = require('uvu')
+let { is, match, equal } = require('uvu/assert')
 let { promisify } = require('util')
 let { join } = require('path')
 let child = require('child_process')
 
+let { parseArgs } = require('./utils')
+
 let exec = promisify(child.exec)
 
-test('prints unique ID', async () => {
+const nanoIdSuit = suite('nanoid')
+
+nanoIdSuit('prints unique ID', async () => {
   let { stdout, stderr } = await exec('node ' + join(__dirname, 'nanoid.cjs'))
   is(stderr, '')
   match(stdout, /^[\w-]{21}\n$/)
 })
 
-test.run()
+nanoIdSuit('uses size', async () => {
+  let { stdout, stderr } = await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --size=10')
+  is(stderr, '')
+  match(stdout, /^[\w-]{10}\n$/)
+})
+
+nanoIdSuit('uses alphabet', async () => {
+  let { stdout, stderr } = await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --alphabet=abc --size=15')
+  is(stderr, '')
+  match(stdout, /^[abc]{15}\n$/)
+})
+
+nanoIdSuit('show an error if size is not a number', async () => {
+  try {
+    await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --s abc')
+  } catch (e) {
+    match(e, /Size must be positive integer/)
+  }
+})
+
+nanoIdSuit('shows an error if size is not provided when using custom alphabet', async () => {
+  try {
+    await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --alphabet abc')
+  } catch (e) {
+    match(e, /You must also specify size option, when using custom alphabet/)
+  }
+})
+
+nanoIdSuit('requires error if size is a negative number', async () => {
+  try {
+    await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --size "-1"')
+  } catch (e) {
+    match(e, /Size must be positive integer/)
+  }
+})
+
+nanoIdSuit('displays help', async () => {
+  let { stdout, stderr } = await exec('node ' + join(__dirname, 'nanoid.cjs') + ' --help')
+  is(stderr, '')
+  match(stdout, /Usage/)
+  match(stdout, /\$ nanoid \[options]/)
+})
+
+nanoIdSuit.run()
+
+const parseArgsSuite = suite('parseArgs')
+
+parseArgsSuite('parses args', () => {
+  equal(parseArgs(['node', 'nanoid.cjs', '--help']), { help: true })
+  equal(parseArgs(['node', 'nanoid.cjs', '--help', '--size=30']), { help: true, size: '30' })
+  equal(parseArgs(['node', 'nanoid.cjs', '--help', '-s', '30']), { help: true, s: '30' })
+  equal(parseArgs(['node', 'nanoid.cjs', '--help', '-size', '30']), { help: true, size: '30' })
+  equal(parseArgs(['node', 'nanoid.cjs', '--help', '-size', '30', '-alphabet', 'abc']), { help: true, size: '30', alphabet: 'abc' })
+})
+
+parseArgsSuite.run()
diff --git a/bin/utils/index.js b/bin/utils/index.js
new file mode 100644
index 00000000..914cdad2
--- /dev/null
+++ b/bin/utils/index.js
@@ -0,0 +1,44 @@
+let cleanArgName = (arg) => arg.startsWith('--') ? arg.slice(2) : arg.slice(1)
+
+let parseArgs = (argv) => {
+  argv.splice(0, 2)
+
+  let parsedArgs = {}
+
+  let currentArg = null
+  argv.forEach((arg) => {
+    if (arg.includes('=')) {
+      if (currentArg) {
+        parsedArgs[currentArg] = true
+        currentArg = null
+      }
+      let argSplit = arg.split('=')
+      parsedArgs[cleanArgName(argSplit[0])] = argSplit[1]
+
+      return
+    }
+
+    if (arg.startsWith('-') || arg.startsWith('--')) {
+      if (currentArg) {
+        parsedArgs[currentArg] = true
+        currentArg = null
+      }
+
+      currentArg = cleanArgName(arg)
+      return
+    }
+
+    if (currentArg) {
+      parsedArgs[currentArg] = arg
+      currentArg = null
+    }
+  })
+
+  if (currentArg) {
+    parsedArgs[currentArg] = true
+  }
+
+  return parsedArgs
+}
+
+module.exports = { parseArgs }