From 09c5f4cd1f916f1b2a20248960f605a5dc5d6b0d Mon Sep 17 00:00:00 2001 From: gagik Date: Thu, 27 Nov 2025 12:55:48 +0100 Subject: [PATCH 01/12] chore: move parseCliArgs to args-parser --- package-lock.json | 6 +- packages/arg-parser/package.json | 3 +- packages/arg-parser/src/arg-parser.spec.ts | 948 ++++++++++++++++++++ packages/arg-parser/src/arg-parser.ts | 246 ++++++ packages/arg-parser/src/index.ts | 2 + packages/cli-repl/package.json | 3 +- packages/cli-repl/src/arg-parser.spec.ts | 958 +-------------------- packages/cli-repl/src/arg-parser.ts | 243 +----- packages/cli-repl/src/run.ts | 5 +- 9 files changed, 1244 insertions(+), 1170 deletions(-) create mode 100644 packages/arg-parser/src/arg-parser.spec.ts create mode 100644 packages/arg-parser/src/arg-parser.ts diff --git a/package-lock.json b/package-lock.json index 3740344715..0cc5af91c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36715,7 +36715,8 @@ "dependencies": { "@mongosh/errors": "2.4.4", "@mongosh/i18n": "^2.19.0", - "mongodb-connection-string-url": "^3.0.2" + "mongodb-connection-string-url": "^3.0.2", + "yargs-parser": "^20.2.4" }, "devDependencies": { "@mongodb-js/devtools-connect": "^3.9.4", @@ -37790,8 +37791,7 @@ "pretty-repl": "^4.0.1", "semver": "^7.5.4", "strip-ansi": "^6.0.0", - "text-table": "^0.2.0", - "yargs-parser": "^20.2.4" + "text-table": "^0.2.0" }, "bin": { "mongosh": "bin/mongosh.js" diff --git a/packages/arg-parser/package.json b/packages/arg-parser/package.json index cce4f28c20..975017967b 100644 --- a/packages/arg-parser/package.json +++ b/packages/arg-parser/package.json @@ -37,7 +37,8 @@ "dependencies": { "@mongosh/errors": "2.4.4", "@mongosh/i18n": "^2.19.0", - "mongodb-connection-string-url": "^3.0.2" + "mongodb-connection-string-url": "^3.0.2", + "yargs-parser": "^20.2.4" }, "devDependencies": { "@mongodb-js/devtools-connect": "^3.9.4", diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts new file mode 100644 index 0000000000..c9ad3edf55 --- /dev/null +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -0,0 +1,948 @@ +import { MongoshUnimplementedError } from '@mongosh/errors'; +import { expect } from 'chai'; +import { getLocale, parseCliArgs, UnknownCliArgumentError } from './arg-parser'; + +describe('arg-parser', function () { + describe('.getLocale', function () { + context('when --locale is provided', function () { + it('returns the locale', function () { + expect(getLocale(['--locale', 'de_DE'], {})).to.equal('de_DE'); + }); + }); + + context('when --locale is not provided', function () { + context('when env.LANG is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANG: 'de_DE.UTF-8' })).to.equal('de_DE'); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANG: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LANGUAGE is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANGUAGE: 'de_DE.UTF-8' })).to.equal( + 'de_DE' + ); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LANGUAGE: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LC_ALL is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_ALL: 'de_DE.UTF-8' })).to.equal('de_DE'); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_ALL: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + + context('when env.LC_MESSAGES is set', function () { + context('when it contains the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_MESSAGES: 'de_DE.UTF-8' })).to.equal( + 'de_DE' + ); + }); + }); + + context('when it does not contain the encoding', function () { + it('returns the locale', function () { + expect(getLocale([], { LC_MESSAGES: 'de_DE' })).to.equal('de_DE'); + }); + }); + }); + }); + }); + + describe('.parse', function () { + const baseArgv = ['node', 'mongosh']; + context('when providing only a URI', function () { + const uri = 'mongodb://domain.com:20000'; + const argv = [...baseArgv, uri]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + }); + + context('when providing a URI + options', function () { + const uri = 'mongodb://domain.com:20000'; + + context('when providing general options', function () { + context('when providing --ipv6', function () { + const argv = [...baseArgv, uri, '--ipv6']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the ipv6 value in the object', function () { + expect(parseCliArgs(argv).ipv6).to.equal(true); + }); + }); + + context('when providing -h', function () { + const argv = [...baseArgv, uri, '-h']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the help value in the object', function () { + expect(parseCliArgs(argv).help).to.equal(true); + }); + }); + + context('when providing --help', function () { + const argv = [...baseArgv, uri, '--help']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the help value in the object', function () { + expect(parseCliArgs(argv).help).to.equal(true); + }); + }); + + context('when providing --version', function () { + const argv = [...baseArgv, uri, '--version']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the version value in the object', function () { + expect(parseCliArgs(argv).version).to.equal(true); + }); + }); + + context('when providing --verbose', function () { + const argv = [...baseArgv, uri, '--verbose']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the verbose value in the object', function () { + expect(parseCliArgs(argv).verbose).to.equal(true); + }); + }); + + context('when providing --shell', function () { + const argv = [...baseArgv, uri, '--shell']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the shell value in the object', function () { + expect(parseCliArgs(argv).shell).to.equal(true); + }); + }); + + context('when providing --nodb', function () { + const argv = [...baseArgv, uri, '--nodb']; + + it('does not return the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); + expect(parseCliArgs(argv).fileNames).to.deep.equal([uri]); + }); + + it('sets the nodb value in the object', function () { + expect(parseCliArgs(argv).nodb).to.equal(true); + }); + }); + + context('when providing --norc', function () { + const argv = [...baseArgv, uri, '--norc']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the norc value in the object', function () { + expect(parseCliArgs(argv).norc).to.equal(true); + }); + }); + + context('when providing --quiet', function () { + const argv = [...baseArgv, uri, '--quiet']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the quiet value in the object', function () { + expect(parseCliArgs(argv).quiet).to.equal(true); + }); + }); + + context('when providing --eval (single value)', function () { + const argv = [...baseArgv, uri, '--eval', '1+1']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the eval value in the object', function () { + expect(parseCliArgs(argv).eval).to.deep.equal(['1+1']); + }); + }); + + context('when providing --eval (multiple values)', function () { + const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the eval value in the object', function () { + expect(parseCliArgs(argv).eval).to.deep.equal(['1+1', '2+2']); + }); + }); + + context('when providing --retryWrites', function () { + const argv = [...baseArgv, uri, '--retryWrites']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the retryWrites value in the object', function () { + expect(parseCliArgs(argv).retryWrites).to.equal(true); + }); + }); + + context('when providing an unknown parameter', function () { + const argv = [...baseArgv, uri, '--what']; + + it('raises an error', function () { + try { + parseCliArgs(argv); + } catch (err: any) { + if (err instanceof UnknownCliArgumentError) { + expect(err.argument).equals('--what'); + return; + } else { + expect.fail('Did not throw an unknown cli error'); + } + } + expect.fail('parsing unknown parameter did not throw'); + }); + }); + }); + + context('when providing authentication options', function () { + context('when providing -u', function () { + const argv = [...baseArgv, uri, '-u', 'richard']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the username in the object', function () { + expect(parseCliArgs(argv).username).to.equal('richard'); + }); + }); + + context('when providing --username', function () { + const argv = [...baseArgv, uri, '--username', 'richard']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the username in the object', function () { + expect(parseCliArgs(argv).username).to.equal('richard'); + }); + }); + + context('when providing -p', function () { + const argv = [...baseArgv, uri, '-p', 'pw']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the password in the object', function () { + expect(parseCliArgs(argv).password).to.equal('pw'); + }); + }); + + context('when providing --password', function () { + const argv = [...baseArgv, uri, '--password', 'pw']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the password in the object', function () { + expect(parseCliArgs(argv).password).to.equal('pw'); + }); + }); + + context('when providing --authenticationDatabase', function () { + const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the authenticationDatabase in the object', function () { + expect(parseCliArgs(argv).authenticationDatabase).to.equal('db'); + }); + }); + + context('when providing --authenticationMechanism', function () { + const argv = [ + ...baseArgv, + uri, + '--authenticationMechanism', + 'SCRAM-SHA-256', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the authenticationMechanism in the object', function () { + expect(parseCliArgs(argv).authenticationMechanism).to.equal( + 'SCRAM-SHA-256' + ); + }); + }); + + context('when providing --gssapiServiceName', function () { + const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the gssapiServiceName in the object', function () { + expect(parseCliArgs(argv).gssapiServiceName).to.equal('mongosh'); + }); + }); + + context('when providing --gssapiHostName', function () { + const argv = [...baseArgv, uri, '--gssapiHostName', 'example.com']; + + it('throws an error since it is not supported', function () { + try { + parseCliArgs(argv); + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + 'Argument --gssapiHostName is not supported in mongosh' + ); + return; + } + expect.fail('Expected error'); + }); + + // it('returns the URI in the object', () => { + // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + // }); + + // it('sets the gssapiHostName in the object', () => { + // expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com'); + // }); + }); + + context('when providing --sspiHostnameCanonicalization', function () { + const argv = [ + ...baseArgv, + uri, + '--sspiHostnameCanonicalization', + 'forward', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the gssapiHostName in the object', function () { + expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal( + 'forward' + ); + }); + }); + + context('when providing --sspiRealmOverride', function () { + const argv = [ + ...baseArgv, + uri, + '--sspiRealmOverride', + 'example2.com', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the gssapiHostName in the object', function () { + expect(parseCliArgs(argv).sspiRealmOverride).to.equal( + 'example2.com' + ); + }); + }); + + context('when providing --awsIamSessionToken', function () { + const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the awsIamSessionToken in the object', function () { + expect(parseCliArgs(argv).awsIamSessionToken).to.equal('tok'); + }); + }); + }); + + context('when providing TLS options', function () { + context('when providing --tls', function () { + const argv = [...baseArgv, uri, '--tls']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tls in the object', function () { + expect(parseCliArgs(argv).tls).to.equal(true); + }); + }); + + context('when providing -tls (single dash)', function () { + const argv = [...baseArgv, uri, '-tls']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tls in the object', function () { + expect(parseCliArgs(argv).tls).to.equal(true); + }); + }); + + context('when providing --tlsCertificateKeyFile', function () { + const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFile in the object', function () { + expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); + }); + }); + + context( + 'when providing -tlsCertificateKeyFile (single dash)', + function () { + const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFile in the object', function () { + expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); + }); + } + ); + + context('when providing --tlsCertificateKeyFilePassword', function () { + const argv = [ + ...baseArgv, + uri, + '--tlsCertificateKeyFilePassword', + 'test', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCertificateKeyFilePassword in the object', function () { + expect(parseCliArgs(argv).tlsCertificateKeyFilePassword).to.equal( + 'test' + ); + }); + }); + + context('when providing --tlsCAFile', function () { + const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCAFile in the object', function () { + expect(parseCliArgs(argv).tlsCAFile).to.equal('test'); + }); + }); + + context('when providing --tlsCRLFile', function () { + const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCRLFile in the object', function () { + expect(parseCliArgs(argv).tlsCRLFile).to.equal('test'); + }); + }); + + context('when providing --tlsAllowInvalidHostnames', function () { + const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsAllowInvalidHostnames in the object', function () { + expect(parseCliArgs(argv).tlsAllowInvalidHostnames).to.equal(true); + }); + }); + + context('when providing --tlsAllowInvalidCertificates', function () { + const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsAllowInvalidCertificates in the object', function () { + expect(parseCliArgs(argv).tlsAllowInvalidCertificates).to.equal( + true + ); + }); + }); + + context('when providing --sslFIPSMode', function () { + const argv = [...baseArgv, uri, '--sslFIPSMode']; + + it('throws an error since it is not supported', function () { + try { + parseCliArgs(argv); + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + 'Argument --sslFIPSMode is not supported in mongosh' + ); + return; + } + expect.fail('Expected error'); + }); + + // it('returns the URI in the object', () => { + // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + // }); + + // it('sets the tlsFIPSMode in the object', () => { + // expect(parseCliArgs(argv).tlsFIPSMode).to.equal(true); + // }); + }); + + context('when providing --tlsCertificateSelector', function () { + const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsCertificateSelector in the object', function () { + expect(parseCliArgs(argv).tlsCertificateSelector).to.equal('test'); + }); + }); + + context('when providing --tlsDisabledProtocols', function () { + const argv = [ + ...baseArgv, + uri, + '--tlsDisabledProtocols', + 'TLS1_0,TLS2_0', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the tlsDisabledProtocols in the object', function () { + expect(parseCliArgs(argv).tlsDisabledProtocols).to.equal( + 'TLS1_0,TLS2_0' + ); + }); + }); + }); + + context('when providing FLE options', function () { + context('when providing --awsAccessKeyId', function () { + const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the awsAccessKeyId in the object', function () { + expect(parseCliArgs(argv).awsAccessKeyId).to.equal('foo'); + }); + }); + + context('when providing --awsSecretAccessKey', function () { + const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the awsSecretAccessKey in the object', function () { + expect(parseCliArgs(argv).awsSecretAccessKey).to.equal('foo'); + }); + }); + + context('when providing --awsSessionToken', function () { + const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the awsSessionToken in the object', function () { + expect(parseCliArgs(argv).awsSessionToken).to.equal('foo'); + }); + }); + + context('when providing --keyVaultNamespace', function () { + const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the keyVaultNamespace in the object', function () { + expect(parseCliArgs(argv).keyVaultNamespace).to.equal('foo.bar'); + }); + }); + + context('when providing --kmsURL', function () { + const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the kmsURL in the object', function () { + expect(parseCliArgs(argv).kmsURL).to.equal('example.com'); + }); + }); + }); + + context('when providing versioned API options', function () { + context('when providing --apiVersion', function () { + const argv = [...baseArgv, uri, '--apiVersion', '1']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect(parseCliArgs(argv).apiVersion).to.equal('1'); + }); + }); + + context('when providing --apiDeprecationErrors', function () { + const argv = [...baseArgv, uri, '--apiDeprecationErrors']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect(parseCliArgs(argv).apiDeprecationErrors).to.equal(true); + }); + }); + + context('when providing --apiStrict', function () { + const argv = [...baseArgv, uri, '--apiStrict']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the apiVersion in the object', function () { + expect(parseCliArgs(argv).apiStrict).to.equal(true); + }); + }); + }); + + context('when providing filenames after an URI', function () { + context('when the filenames end in .js', function () { + const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); + }); + }); + + context('when the filenames end in .mongodb', function () { + const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); + }); + }); + + context('when the filenames end in other extensions', function () { + const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + }); + }); + + context('when filenames are specified using -f', function () { + const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + }); + }); + + context('when filenames are specified using -f/--file', function () { + const argv = [ + ...baseArgv, + uri, + '-f', + 'test1.txt', + '--file', + 'test2.txt', + ]; + + it('returns the URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + }); + }); + }); + + context('when providing filenames without an URI', function () { + context('when the filenames end in .js', function () { + const argv = [...baseArgv, 'test1.js', 'test2.js']; + + it('returns no URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); + }); + }); + + context('when the filenames end in .mongodb', function () { + const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; + + it('returns no URI in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); + }); + + it('sets the filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); + expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); + }); + }); + + context('when the filenames end in other extensions', function () { + const argv = [...baseArgv, 'test1.txt', 'test2.txt']; + + it('returns the first filename as an URI', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal( + 'test1.txt' + ); + }); + + it('uses the remainder as filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); + }); + }); + + context('when the first argument is an URI ending in .js', function () { + const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; + + it('returns the first filename as an URI', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal( + 'mongodb://domain.foo.js' + ); + }); + + it('uses the remainder as filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); + }); + }); + + context( + 'when the first argument is an URI ending in .js but --file is used', + function () { + const argv = [ + ...baseArgv, + '--file', + 'mongodb://domain.foo.js', + 'mongodb://domain.bar.js', + ]; + + it('returns the first filename as an URI', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal( + 'mongodb://domain.bar.js' + ); + }); + + it('uses the remainder as filenames', function () { + expect(parseCliArgs(argv).fileNames?.[0]).to.equal( + 'mongodb://domain.foo.js' + ); + }); + } + ); + }); + }); + + context('when providing no URI', function () { + context('when providing a DB address', function () { + context('when only a db name is provided', function () { + const db = 'foo'; + const argv = [...baseArgv, db]; + + it('sets the db in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); + }); + }); + + context('when a db address is provided without a scheme', function () { + const db = '192.168.0.5:9999/foo'; + const argv = [...baseArgv, db]; + + it('sets the db in the object', function () { + expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); + }); + }); + }); + + context('when providing no DB address', function () { + context('when providing a host', function () { + const argv = [...baseArgv, '--host', 'example.com']; + + it('sets the host value in the object', function () { + expect(parseCliArgs(argv).host).to.equal('example.com'); + }); + }); + + context('when providing a port', function () { + const argv = [...baseArgv, '--port', '20000']; + + it('sets the port value in the object', function () { + expect(parseCliArgs(argv).port).to.equal('20000'); + }); + }); + }); + }); + + context('when providing a deprecated argument', function () { + for (const { deprecated, replacement, value } of [ + { deprecated: 'ssl', replacement: 'tls' }, + { + deprecated: 'sslAllowInvalidCertificates', + replacement: 'tlsAllowInvalidCertificates', + }, + { + deprecated: 'sslAllowInvalidCertificates', + replacement: 'tlsAllowInvalidCertificates', + }, + { + deprecated: 'sslAllowInvalidHostnames', + replacement: 'tlsAllowInvalidHostnames', + }, + // { deprecated: 'sslFIPSMode', replacement: 'tlsFIPSMode' }, <<-- FIPS is currently not supported right now + { + deprecated: 'sslPEMKeyFile', + replacement: 'tlsCertificateKeyFile', + value: 'pemKeyFile', + }, + { + deprecated: 'sslPEMKeyPassword', + replacement: 'tlsCertificateKeyFilePassword', + value: 'pemKeyPass', + }, + { deprecated: 'sslCAFile', replacement: 'tlsCAFile', value: 'caFile' }, + // { deprecated: 'sslCertificateSelector', replacement: 'tlsCertificateSelector', value: 'certSelector' }, <<-- Certificate selector not supported right now + { + deprecated: 'sslCRLFile', + replacement: 'tlsCRLFile', + value: 'crlFile', + }, + { + deprecated: 'sslDisabledProtocols', + replacement: 'tlsDisabledProtocols', + value: 'disabledProtos', + }, + ] as const) { + it(`replaces --${deprecated} with --${replacement}`, function () { + const argv = [...baseArgv, `--${deprecated}`]; + if (value) { + argv.push(value); + } + + const args = parseCliArgs(argv); + expect(args).to.not.have.property(deprecated); + expect(args[replacement]).to.equal(value ?? true); + }); + } + }); + }); +}); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts new file mode 100644 index 0000000000..13ae3cfac7 --- /dev/null +++ b/packages/arg-parser/src/arg-parser.ts @@ -0,0 +1,246 @@ +import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; +import i18n from '@mongosh/i18n'; +import parser from 'yargs-parser'; +import type { CliOptions } from './cli-options'; + +/** + * The yargs-parser options configuration. + */ +const OPTIONS = { + string: [ + 'apiVersion', + 'authenticationDatabase', + 'authenticationMechanism', + 'awsAccessKeyId', + 'awsIamSessionToken', + 'awsSecretAccessKey', + 'awsSessionToken', + 'awsIamSessionToken', + 'browser', + 'csfleLibraryPath', + 'cryptSharedLibPath', + 'db', + 'gssapiHostName', + 'gssapiServiceName', + 'sspiHostnameCanonicalization', + 'sspiRealmOverride', + 'jsContext', + 'host', + 'keyVaultNamespace', + 'kmsURL', + 'locale', + 'oidcFlows', + 'oidcRedirectUri', + 'password', + 'port', + 'sslPEMKeyFile', + 'sslPEMKeyPassword', + 'sslCAFile', + 'sslCertificateSelector', + 'sslCRLFile', + 'sslDisabledProtocols', + 'tlsCAFile', + 'tlsCertificateKeyFile', + 'tlsCertificateKeyFilePassword', + 'tlsCertificateSelector', + 'tlsCRLFile', + 'tlsDisabledProtocols', + 'username', + ], + boolean: [ + 'apiDeprecationErrors', + 'apiStrict', + 'buildInfo', + 'exposeAsyncRewriter', + 'help', + 'ipv6', + 'nodb', + 'norc', + 'oidcTrustedEndpoint', + 'oidcIdTokenAsAccessToken', + 'oidcNoNonce', + 'perfTests', + 'quiet', + 'retryWrites', + 'shell', + 'smokeTests', + 'skipStartupWarnings', + 'ssl', + 'sslAllowInvalidCertificates', + 'sslAllowInvalidHostnames', + 'sslFIPSMode', + 'tls', + 'tlsAllowInvalidCertificates', + 'tlsAllowInvalidHostnames', + 'tlsFIPSMode', + 'tlsUseSystemCA', + 'verbose', + 'version', + ], + array: ['eval', 'file'], + alias: { + h: 'help', + p: 'password', + u: 'username', + f: 'file', + 'build-info': 'buildInfo', + json: 'json', // List explicitly here since it can be a boolean or a string + browser: 'browser', // ditto + oidcDumpTokens: 'oidcDumpTokens', // ditto + oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time + oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto + }, + configuration: { + 'camel-case-expansion': false, + 'unknown-options-as-args': true, + 'parse-positional-numbers': false, + 'parse-numbers': false, + 'greedy-arrays': false, + 'short-option-groups': false, + }, +}; + +/** + * Maps deprecated arguments to their new counterparts. + */ +const DEPRECATED_ARGS_WITH_REPLACEMENT: Record = { + ssl: 'tls', + sslAllowInvalidCertificates: 'tlsAllowInvalidCertificates', + sslAllowInvalidHostnames: 'tlsAllowInvalidHostnames', + sslFIPSMode: 'tlsFIPSMode', + sslPEMKeyFile: 'tlsCertificateKeyFile', + sslPEMKeyPassword: 'tlsCertificateKeyFilePassword', + sslCAFile: 'tlsCAFile', + sslCertificateSelector: 'tlsCertificateSelector', + sslCRLFile: 'tlsCRLFile', + sslDisabledProtocols: 'tlsDisabledProtocols', +}; + +/** + * If an unsupported argument is given an error will be thrown. + */ +const UNSUPPORTED_ARGS: Readonly = ['sslFIPSMode', 'gssapiHostName']; + +/** + * Determine the locale of the shell. + * + * @param {string[]} args - The arguments. + * + * @returns {string} The locale. + */ +export function getLocale(args: string[], env: any): string { + const localeIndex = args.indexOf('--locale'); + if (localeIndex > -1) { + return args[localeIndex + 1]; + } + const lang = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES; + return lang ? lang.split('.')[0] : lang; +} + +function isConnectionSpecifier(arg?: string): boolean { + return ( + typeof arg === 'string' && + (arg.startsWith('mongodb://') || + arg.startsWith('mongodb+srv://') || + !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) + ); +} + +/** + * Parses arguments into a JS object. + * + * @param args - The CLI arguments. + * + * @returns The arguments as cli options. + */ +export function parseCliArgs(args: string[]): + | CliOptions & { + smokeTests: boolean; + perfTests: boolean; + buildInfo: boolean; + _argParseWarnings: string[]; + } { + const programArgs = args.slice(2); + i18n.setLocale(getLocale(programArgs, process.env)); + + const parsed = parser(programArgs, OPTIONS) as unknown as CliOptions & { + smokeTests: boolean; + perfTests: boolean; + buildInfo: boolean; + _argParseWarnings: string[]; + _?: string[]; + file?: string[]; + }; + const positionalArguments = parsed._ ?? []; + for (const arg of positionalArguments) { + if (arg.startsWith('-')) { + throw new UnknownCliArgumentError(arg); + } + } + + if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { + parsed.connectionSpecifier = positionalArguments.shift(); + } + parsed.fileNames = [...(parsed.file ?? []), ...positionalArguments]; + + // All positional arguments are either in connectionSpecifier or fileNames, + // and should only be accessed that way now. + delete parsed._; + + parsed._argParseWarnings = verifyCliArguments(parsed); + + return parsed; +} + +export function verifyCliArguments(args: any /* CliOptions */): string[] { + for (const unsupported of UNSUPPORTED_ARGS) { + if (unsupported in args) { + throw new MongoshUnimplementedError( + `Argument --${unsupported} is not supported in mongosh`, + CommonErrors.InvalidArgument + ); + } + } + + if (![undefined, true, false, 'relaxed', 'canonical'].includes(args.json)) { + throw new MongoshUnimplementedError( + '--json can only have the values relaxed or canonical', + CommonErrors.InvalidArgument + ); + } + + if ( + ![undefined, true, false, 'redacted', 'include-secrets'].includes( + args.oidcDumpTokens + ) + ) { + throw new MongoshUnimplementedError( + '--oidcDumpTokens can only have the values redacted or include-secrets', + CommonErrors.InvalidArgument + ); + } + + const messages = []; + for (const deprecated in DEPRECATED_ARGS_WITH_REPLACEMENT) { + if (deprecated in args) { + const replacement = DEPRECATED_ARGS_WITH_REPLACEMENT[deprecated]; + messages.push( + `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` + ); + + args[replacement] = args[deprecated]; + delete args[deprecated]; + } + } + return messages; +} + +export class UnknownCliArgumentError extends Error { + /** The argument that was not parsed. */ + readonly argument: string; + constructor(argument: string) { + super(`Unknown argument: ${argument}`); + this.name = 'UnknownParserArgumentError'; + this.argument = argument; + } +} diff --git a/packages/arg-parser/src/index.ts b/packages/arg-parser/src/index.ts index fd999886fd..f0fc68775c 100644 --- a/packages/arg-parser/src/index.ts +++ b/packages/arg-parser/src/index.ts @@ -19,3 +19,5 @@ export function generateConnectionInfoFromCliArgs( const connectionString = generateUri(options); return mapCliToDriver(options, { connectionString, driverOptions: {} }); } + +export { parseCliArgs, UnknownCliArgumentError } from './arg-parser'; diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index 6745eae931..a3c57b26c4 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -93,8 +93,7 @@ "semver": "^7.5.4", "strip-ansi": "^6.0.0", "text-table": "^0.2.0", - "glibc-version": "^1.0.0", - "yargs-parser": "^20.2.4" + "glibc-version": "^1.0.0" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", diff --git a/packages/cli-repl/src/arg-parser.spec.ts b/packages/cli-repl/src/arg-parser.spec.ts index ab09eff6ae..70c326a6d3 100644 --- a/packages/cli-repl/src/arg-parser.spec.ts +++ b/packages/cli-repl/src/arg-parser.spec.ts @@ -1,946 +1,40 @@ -import { MongoshUnimplementedError } from '@mongosh/errors'; import { expect } from 'chai'; +import { parseMongoshCliArgs } from './arg-parser'; import stripAnsi from 'strip-ansi'; -import { getLocale, parseCliArgs } from './arg-parser'; -describe('arg-parser', function () { - describe('.getLocale', function () { - context('when --locale is provided', function () { - it('returns the locale', function () { - expect(getLocale(['--locale', 'de_DE'], {})).to.equal('de_DE'); - }); - }); - - context('when --locale is not provided', function () { - context('when env.LANG is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANG: 'de_DE.UTF-8' })).to.equal('de_DE'); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANG: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LANGUAGE is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANGUAGE: 'de_DE.UTF-8' })).to.equal( - 'de_DE' - ); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LANGUAGE: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LC_ALL is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_ALL: 'de_DE.UTF-8' })).to.equal('de_DE'); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_ALL: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - - context('when env.LC_MESSAGES is set', function () { - context('when it contains the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_MESSAGES: 'de_DE.UTF-8' })).to.equal( - 'de_DE' - ); - }); - }); - - context('when it does not contain the encoding', function () { - it('returns the locale', function () { - expect(getLocale([], { LC_MESSAGES: 'de_DE' })).to.equal('de_DE'); - }); - }); - }); - }); - }); - - describe('.parse', function () { - const baseArgv = ['node', 'mongosh']; - context('when providing only a URI', function () { - const uri = 'mongodb://domain.com:20000'; - const argv = [...baseArgv, uri]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - }); - - context('when providing a URI + options', function () { - const uri = 'mongodb://domain.com:20000'; - - context('when providing general options', function () { - context('when providing --ipv6', function () { - const argv = [...baseArgv, uri, '--ipv6']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the ipv6 value in the object', function () { - expect(parseCliArgs(argv).ipv6).to.equal(true); - }); - }); - - context('when providing -h', function () { - const argv = [...baseArgv, uri, '-h']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); - }); - }); - - context('when providing --help', function () { - const argv = [...baseArgv, uri, '--help']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); - }); - }); - - context('when providing --version', function () { - const argv = [...baseArgv, uri, '--version']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the version value in the object', function () { - expect(parseCliArgs(argv).version).to.equal(true); - }); - }); - - context('when providing --verbose', function () { - const argv = [...baseArgv, uri, '--verbose']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the verbose value in the object', function () { - expect(parseCliArgs(argv).verbose).to.equal(true); - }); - }); - - context('when providing --shell', function () { - const argv = [...baseArgv, uri, '--shell']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the shell value in the object', function () { - expect(parseCliArgs(argv).shell).to.equal(true); - }); - }); - - context('when providing --nodb', function () { - const argv = [...baseArgv, uri, '--nodb']; - - it('does not return the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - expect(parseCliArgs(argv).fileNames).to.deep.equal([uri]); - }); - - it('sets the nodb value in the object', function () { - expect(parseCliArgs(argv).nodb).to.equal(true); - }); - }); - - context('when providing --norc', function () { - const argv = [...baseArgv, uri, '--norc']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the norc value in the object', function () { - expect(parseCliArgs(argv).norc).to.equal(true); - }); - }); - - context('when providing --quiet', function () { - const argv = [...baseArgv, uri, '--quiet']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the quiet value in the object', function () { - expect(parseCliArgs(argv).quiet).to.equal(true); - }); - }); - - context('when providing --eval (single value)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1']); - }); - }); - - context('when providing --eval (multiple values)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1', '2+2']); - }); - }); - - context('when providing --retryWrites', function () { - const argv = [...baseArgv, uri, '--retryWrites']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the retryWrites value in the object', function () { - expect(parseCliArgs(argv).retryWrites).to.equal(true); - }); - }); - - context('when providing an unknown parameter', function () { - const argv = [...baseArgv, uri, '--what']; - - it('raises an error', function () { - try { - parseCliArgs(argv); - } catch (err: any) { - return expect(stripAnsi(err.message)).to.contain( - 'Error parsing command line: unrecognized option: --what' - ); - } - expect.fail('parsing unknown parameter did not throw'); - }); - }); - }); - - context('when providing authentication options', function () { - context('when providing -u', function () { - const argv = [...baseArgv, uri, '-u', 'richard']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); - }); - }); - - context('when providing --username', function () { - const argv = [...baseArgv, uri, '--username', 'richard']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); - }); - }); - - context('when providing -p', function () { - const argv = [...baseArgv, uri, '-p', 'pw']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); - }); - }); - - context('when providing --password', function () { - const argv = [...baseArgv, uri, '--password', 'pw']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); - }); - }); - - context('when providing --authenticationDatabase', function () { - const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the authenticationDatabase in the object', function () { - expect(parseCliArgs(argv).authenticationDatabase).to.equal('db'); - }); - }); - - context('when providing --authenticationMechanism', function () { - const argv = [ - ...baseArgv, - uri, - '--authenticationMechanism', - 'SCRAM-SHA-256', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the authenticationMechanism in the object', function () { - expect(parseCliArgs(argv).authenticationMechanism).to.equal( - 'SCRAM-SHA-256' - ); - }); - }); - - context('when providing --gssapiServiceName', function () { - const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiServiceName in the object', function () { - expect(parseCliArgs(argv).gssapiServiceName).to.equal('mongosh'); - }); - }); - - context('when providing --gssapiHostName', function () { - const argv = [...baseArgv, uri, '--gssapiHostName', 'example.com']; - - it('throws an error since it is not supported', function () { - try { - parseCliArgs(argv); - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --gssapiHostName is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); - }); - - // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - // }); - - // it('sets the gssapiHostName in the object', () => { - // expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com'); - // }); - }); - - context('when providing --sspiHostnameCanonicalization', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiHostnameCanonicalization', - 'forward', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal( - 'forward' - ); - }); - }); - - context('when providing --sspiRealmOverride', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiRealmOverride', - 'example2.com', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiRealmOverride).to.equal( - 'example2.com' - ); - }); - }); - - context('when providing --awsIamSessionToken', function () { - const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsIamSessionToken in the object', function () { - expect(parseCliArgs(argv).awsIamSessionToken).to.equal('tok'); - }); - }); - }); - - context('when providing TLS options', function () { - context('when providing --tls', function () { - const argv = [...baseArgv, uri, '--tls']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); - }); - }); - - context('when providing -tls (single dash)', function () { - const argv = [...baseArgv, uri, '-tls']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); - }); - }); - - context('when providing --tlsCertificateKeyFile', function () { - const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); - }); - }); - - context( - 'when providing -tlsCertificateKeyFile (single dash)', - function () { - const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); - }); - } - ); - - context('when providing --tlsCertificateKeyFilePassword', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsCertificateKeyFilePassword', - 'test', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateKeyFilePassword in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFilePassword).to.equal( - 'test' - ); - }); - }); - - context('when providing --tlsCAFile', function () { - const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCAFile in the object', function () { - expect(parseCliArgs(argv).tlsCAFile).to.equal('test'); - }); - }); - - context('when providing --tlsCRLFile', function () { - const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCRLFile in the object', function () { - expect(parseCliArgs(argv).tlsCRLFile).to.equal('test'); - }); - }); - - context('when providing --tlsAllowInvalidHostnames', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsAllowInvalidHostnames in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidHostnames).to.equal(true); - }); - }); - - context('when providing --tlsAllowInvalidCertificates', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsAllowInvalidCertificates in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidCertificates).to.equal( - true - ); - }); - }); - - context('when providing --sslFIPSMode', function () { - const argv = [...baseArgv, uri, '--sslFIPSMode']; - - it('throws an error since it is not supported', function () { - try { - parseCliArgs(argv); - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --sslFIPSMode is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); - }); - - // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - // }); - - // it('sets the tlsFIPSMode in the object', () => { - // expect(parseCliArgs(argv).tlsFIPSMode).to.equal(true); - // }); - }); - - context('when providing --tlsCertificateSelector', function () { - const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsCertificateSelector in the object', function () { - expect(parseCliArgs(argv).tlsCertificateSelector).to.equal('test'); - }); - }); - - context('when providing --tlsDisabledProtocols', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsDisabledProtocols', - 'TLS1_0,TLS2_0', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the tlsDisabledProtocols in the object', function () { - expect(parseCliArgs(argv).tlsDisabledProtocols).to.equal( - 'TLS1_0,TLS2_0' - ); - }); - }); - }); - - context('when providing FLE options', function () { - context('when providing --awsAccessKeyId', function () { - const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsAccessKeyId in the object', function () { - expect(parseCliArgs(argv).awsAccessKeyId).to.equal('foo'); - }); - }); - - context('when providing --awsSecretAccessKey', function () { - const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsSecretAccessKey in the object', function () { - expect(parseCliArgs(argv).awsSecretAccessKey).to.equal('foo'); - }); - }); - - context('when providing --awsSessionToken', function () { - const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the awsSessionToken in the object', function () { - expect(parseCliArgs(argv).awsSessionToken).to.equal('foo'); - }); - }); - - context('when providing --keyVaultNamespace', function () { - const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the keyVaultNamespace in the object', function () { - expect(parseCliArgs(argv).keyVaultNamespace).to.equal('foo.bar'); - }); - }); - - context('when providing --kmsURL', function () { - const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the kmsURL in the object', function () { - expect(parseCliArgs(argv).kmsURL).to.equal('example.com'); - }); - }); - }); - - context('when providing versioned API options', function () { - context('when providing --apiVersion', function () { - const argv = [...baseArgv, uri, '--apiVersion', '1']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiVersion).to.equal('1'); - }); - }); - - context('when providing --apiDeprecationErrors', function () { - const argv = [...baseArgv, uri, '--apiDeprecationErrors']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiDeprecationErrors).to.equal(true); - }); - }); - - context('when providing --apiStrict', function () { - const argv = [...baseArgv, uri, '--apiStrict']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiStrict).to.equal(true); - }); - }); - }); - - context('when providing filenames after an URI', function () { - context('when the filenames end in .js', function () { - const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); - }); - }); - - context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); - }); - }); - - context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - - context('when filenames are specified using -f', function () { - const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - - context('when filenames are specified using -f/--file', function () { - const argv = [ - ...baseArgv, - uri, - '-f', - 'test1.txt', - '--file', - 'test2.txt', - ]; - - it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); - }); - }); - }); - - context('when providing filenames without an URI', function () { - context('when the filenames end in .js', function () { - const argv = [...baseArgv, 'test1.js', 'test2.js']; - - it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); - }); - }); - - context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; - - it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - }); - - it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); - }); - }); - - context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, 'test1.txt', 'test2.txt']; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'test1.txt' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); - }); - }); - - context('when the first argument is an URI ending in .js', function () { - const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.foo.js' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); - }); - }); - - context( - 'when the first argument is an URI ending in .js but --file is used', - function () { - const argv = [ - ...baseArgv, - '--file', - 'mongodb://domain.foo.js', - 'mongodb://domain.bar.js', - ]; - - it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.bar.js' - ); - }); - - it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal( - 'mongodb://domain.foo.js' - ); - }); - } +describe('parseMongoshCliArgs', function () { + const baseArgv = ['node', 'mongosh']; + const uri = 'mongodb://domain.com:2020'; + context('when providing an unknown parameter', function () { + const argv = [...baseArgv, uri, '--what']; + + it('raises an error', function () { + try { + parseMongoshCliArgs(argv); + } catch (err: any) { + return expect(stripAnsi(err.message)).to.contain( + 'Error parsing command line: unrecognized option: --what' ); - }); + } + expect.fail('parsing unknown parameter did not throw'); }); - context('when providing no URI', function () { - context('when providing a DB address', function () { - context('when only a db name is provided', function () { - const db = 'foo'; - const argv = [...baseArgv, db]; - - it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); - }); - }); - - context('when a db address is provided without a scheme', function () { - const db = '192.168.0.5:9999/foo'; - const argv = [...baseArgv, db]; + context('provides the usual functionality of arg-parser', function () { + it('sets passed fields', function () { + const argv = [...baseArgv, uri, '--tls', '--port', '1234']; - it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); - }); - }); + const args = parseMongoshCliArgs(argv); + expect(args['tls']).equals(true); + expect(args['port']).equals('1234'); }); - context('when providing no DB address', function () { - context('when providing a host', function () { - const argv = [...baseArgv, '--host', 'example.com']; + it(`replaces --sslPEMKeyFile with --tlsCertificateKeyFile`, function () { + const argv = [...baseArgv, `--sslPEMKeyFile`, `test`]; - it('sets the host value in the object', function () { - expect(parseCliArgs(argv).host).to.equal('example.com'); - }); - }); - - context('when providing a port', function () { - const argv = [...baseArgv, '--port', '20000']; - - it('sets the port value in the object', function () { - expect(parseCliArgs(argv).port).to.equal('20000'); - }); - }); + const args = parseMongoshCliArgs(argv); + expect(args).to.not.have.property('sslPEMKeyFile'); + expect(args['tlsCertificateKeyFile']).to.equal('test'); }); }); - - context('when providing a deprecated argument', function () { - for (const { deprecated, replacement, value } of [ - { deprecated: 'ssl', replacement: 'tls' }, - { - deprecated: 'sslAllowInvalidCertificates', - replacement: 'tlsAllowInvalidCertificates', - }, - { - deprecated: 'sslAllowInvalidCertificates', - replacement: 'tlsAllowInvalidCertificates', - }, - { - deprecated: 'sslAllowInvalidHostnames', - replacement: 'tlsAllowInvalidHostnames', - }, - // { deprecated: 'sslFIPSMode', replacement: 'tlsFIPSMode' }, <<-- FIPS is currently not supported right now - { - deprecated: 'sslPEMKeyFile', - replacement: 'tlsCertificateKeyFile', - value: 'pemKeyFile', - }, - { - deprecated: 'sslPEMKeyPassword', - replacement: 'tlsCertificateKeyFilePassword', - value: 'pemKeyPass', - }, - { deprecated: 'sslCAFile', replacement: 'tlsCAFile', value: 'caFile' }, - // { deprecated: 'sslCertificateSelector', replacement: 'tlsCertificateSelector', value: 'certSelector' }, <<-- Certificate selector not supported right now - { - deprecated: 'sslCRLFile', - replacement: 'tlsCRLFile', - value: 'crlFile', - }, - { - deprecated: 'sslDisabledProtocols', - replacement: 'tlsDisabledProtocols', - value: 'disabledProtos', - }, - ] as const) { - it(`replaces --${deprecated} with --${replacement}`, function () { - const argv = [...baseArgv, `--${deprecated}`]; - if (value) { - argv.push(value); - } - - const args = parseCliArgs(argv); - expect(args).to.not.have.property(deprecated); - expect(args[replacement]).to.equal(value ?? true); - }); - } - }); }); }); diff --git a/packages/cli-repl/src/arg-parser.ts b/packages/cli-repl/src/arg-parser.ts index 932fae1d46..6051c6c1cd 100644 --- a/packages/cli-repl/src/arg-parser.ts +++ b/packages/cli-repl/src/arg-parser.ts @@ -1,7 +1,5 @@ -import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; import i18n from '@mongosh/i18n'; -import type { CliOptions } from '@mongosh/arg-parser'; -import parser from 'yargs-parser'; +import { parseCliArgs, UnknownCliArgumentError } from '@mongosh/arg-parser'; import { colorizeForStderr as clr } from './clr'; import { USAGE } from './constants'; @@ -10,236 +8,21 @@ import { USAGE } from './constants'; */ const UNKNOWN = 'cli-repl.arg-parser.unknown-option'; -/** - * The yargs-parser options configuration. - */ -const OPTIONS = { - string: [ - 'apiVersion', - 'authenticationDatabase', - 'authenticationMechanism', - 'awsAccessKeyId', - 'awsIamSessionToken', - 'awsSecretAccessKey', - 'awsSessionToken', - 'awsIamSessionToken', - 'browser', - 'csfleLibraryPath', - 'cryptSharedLibPath', - 'db', - 'gssapiHostName', - 'gssapiServiceName', - 'sspiHostnameCanonicalization', - 'sspiRealmOverride', - 'jsContext', - 'host', - 'keyVaultNamespace', - 'kmsURL', - 'locale', - 'oidcFlows', - 'oidcRedirectUri', - 'password', - 'port', - 'sslPEMKeyFile', - 'sslPEMKeyPassword', - 'sslCAFile', - 'sslCertificateSelector', - 'sslCRLFile', - 'sslDisabledProtocols', - 'tlsCAFile', - 'tlsCertificateKeyFile', - 'tlsCertificateKeyFilePassword', - 'tlsCertificateSelector', - 'tlsCRLFile', - 'tlsDisabledProtocols', - 'username', - ], - boolean: [ - 'apiDeprecationErrors', - 'apiStrict', - 'buildInfo', - 'exposeAsyncRewriter', - 'help', - 'ipv6', - 'nodb', - 'norc', - 'oidcTrustedEndpoint', - 'oidcIdTokenAsAccessToken', - 'oidcNoNonce', - 'perfTests', - 'quiet', - 'retryWrites', - 'shell', - 'smokeTests', - 'skipStartupWarnings', - 'ssl', - 'sslAllowInvalidCertificates', - 'sslAllowInvalidHostnames', - 'sslFIPSMode', - 'tls', - 'tlsAllowInvalidCertificates', - 'tlsAllowInvalidHostnames', - 'tlsFIPSMode', - 'tlsUseSystemCA', - 'verbose', - 'version', - ], - array: ['eval', 'file'], - alias: { - h: 'help', - p: 'password', - u: 'username', - f: 'file', - 'build-info': 'buildInfo', - json: 'json', // List explicitly here since it can be a boolean or a string - browser: 'browser', // ditto - oidcDumpTokens: 'oidcDumpTokens', // ditto - oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time - oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto - }, - configuration: { - 'camel-case-expansion': false, - 'unknown-options-as-args': true, - 'parse-positional-numbers': false, - 'parse-numbers': false, - 'greedy-arrays': false, - 'short-option-groups': false, - }, -}; - -/** - * Maps deprecated arguments to their new counterparts. - */ -const DEPRECATED_ARGS_WITH_REPLACEMENT: Record = { - ssl: 'tls', - sslAllowInvalidCertificates: 'tlsAllowInvalidCertificates', - sslAllowInvalidHostnames: 'tlsAllowInvalidHostnames', - sslFIPSMode: 'tlsFIPSMode', - sslPEMKeyFile: 'tlsCertificateKeyFile', - sslPEMKeyPassword: 'tlsCertificateKeyFilePassword', - sslCAFile: 'tlsCAFile', - sslCertificateSelector: 'tlsCertificateSelector', - sslCRLFile: 'tlsCRLFile', - sslDisabledProtocols: 'tlsDisabledProtocols', -}; - -/** - * If an unsupported argument is given an error will be thrown. - */ -const UNSUPPORTED_ARGS: Readonly = ['sslFIPSMode', 'gssapiHostName']; - -/** - * Determine the locale of the shell. - * - * @param {string[]} args - The arguments. - * - * @returns {string} The locale. - */ -export function getLocale(args: string[], env: any): string { - const localeIndex = args.indexOf('--locale'); - if (localeIndex > -1) { - return args[localeIndex + 1]; - } - const lang = env.LANG || env.LANGUAGE || env.LC_ALL || env.LC_MESSAGES; - return lang ? lang.split('.')[0] : lang; -} - -function isConnectionSpecifier(arg?: string): boolean { - return ( - typeof arg === 'string' && - (arg.startsWith('mongodb://') || - arg.startsWith('mongodb+srv://') || - !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) - ); -} - -/** - * Parses arguments into a JS object. - * - * @param args - The CLI arguments. - * - * @returns The arguments as cli options. - */ -export function parseCliArgs(args: string[]): CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - _argParseWarnings: string[]; -} { - const programArgs = args.slice(2); - i18n.setLocale(getLocale(programArgs, process.env)); - - const parsed = parser(programArgs, OPTIONS) as unknown as CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - _argParseWarnings: string[]; - _?: string[]; - file?: string[]; - }; - const positionalArguments = parsed._ ?? []; - for (const arg of positionalArguments) { - if (arg.startsWith('-')) { +export function parseMongoshCliArgs( + args: string[] +): ReturnType { + try { + return parseCliArgs(args); + } catch (error) { + if (error instanceof UnknownCliArgumentError) { throw new Error( - ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr(String(arg), 'bold')} + ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr( + String(error.argument), + 'bold' + )} ${USAGE}` ); } + throw error; } - - if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { - parsed.connectionSpecifier = positionalArguments.shift(); - } - parsed.fileNames = [...(parsed.file ?? []), ...positionalArguments]; - - // All positional arguments are either in connectionSpecifier or fileNames, - // and should only be accessed that way now. - delete parsed._; - - parsed._argParseWarnings = verifyCliArguments(parsed); - - return parsed; -} - -export function verifyCliArguments(args: any /* CliOptions */): string[] { - for (const unsupported of UNSUPPORTED_ARGS) { - if (unsupported in args) { - throw new MongoshUnimplementedError( - `Argument --${unsupported} is not supported in mongosh`, - CommonErrors.InvalidArgument - ); - } - } - - if (![undefined, true, false, 'relaxed', 'canonical'].includes(args.json)) { - throw new MongoshUnimplementedError( - '--json can only have the values relaxed or canonical', - CommonErrors.InvalidArgument - ); - } - - if ( - ![undefined, true, false, 'redacted', 'include-secrets'].includes( - args.oidcDumpTokens - ) - ) { - throw new MongoshUnimplementedError( - '--oidcDumpTokens can only have the values redacted or include-secrets', - CommonErrors.InvalidArgument - ); - } - - const messages = []; - for (const deprecated in DEPRECATED_ARGS_WITH_REPLACEMENT) { - if (deprecated in args) { - const replacement = DEPRECATED_ARGS_WITH_REPLACEMENT[deprecated]; - messages.push( - `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` - ); - - args[replacement] = args[deprecated]; - delete args[deprecated]; - } - } - return messages; } diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 7e18e526bf..670388d6ef 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -15,7 +15,7 @@ enableFipsIfRequested(); import { markTime } from './startup-timing'; import { CliRepl } from './cli-repl'; -import { parseCliArgs } from './arg-parser'; +import { parseMongoshCliArgs } from './arg-parser'; import { runSmokeTests } from './smoke-tests'; import { USAGE } from './constants'; import { baseBuildInfo, buildInfo } from './build-info'; @@ -33,6 +33,7 @@ import v8 from 'v8'; import { TimingCategories } from '@mongosh/types'; import './webpack-self-inspection'; import { systemCA } from '@mongodb-js/devtools-proxy-support'; +import clr from './clr'; // TS does not yet have type definitions for v8.startupSnapshot if ((v8 as any)?.startupSnapshot?.isBuildingSnapshot?.()) { @@ -85,7 +86,7 @@ async function main() { try { (net as any)?.setDefaultAutoSelectFamily?.(true); - const options = parseCliArgs(process.argv); + const options = parseMongoshCliArgs(process.argv); for (const warning of options._argParseWarnings) { console.warn(warning); } From 674866f597a66b32c4df6d73ba805480b109d31d Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Thu, 27 Nov 2025 13:13:33 +0100 Subject: [PATCH 02/12] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/arg-parser/src/arg-parser.ts | 2 +- packages/cli-repl/src/arg-parser.spec.ts | 2 +- packages/cli-repl/src/run.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index 13ae3cfac7..3d323aa466 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -240,7 +240,7 @@ export class UnknownCliArgumentError extends Error { readonly argument: string; constructor(argument: string) { super(`Unknown argument: ${argument}`); - this.name = 'UnknownParserArgumentError'; + this.name = 'UnknownCliArgumentError'; this.argument = argument; } } diff --git a/packages/cli-repl/src/arg-parser.spec.ts b/packages/cli-repl/src/arg-parser.spec.ts index 70c326a6d3..916dc64f16 100644 --- a/packages/cli-repl/src/arg-parser.spec.ts +++ b/packages/cli-repl/src/arg-parser.spec.ts @@ -19,7 +19,7 @@ describe('parseMongoshCliArgs', function () { expect.fail('parsing unknown parameter did not throw'); }); - context('provides the usual functionality of arg-parser', function () { + context('parses standard arguments correctly', function () { it('sets passed fields', function () { const argv = [...baseArgv, uri, '--tls', '--port', '1234']; diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 670388d6ef..7c8cb83f7f 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -33,7 +33,7 @@ import v8 from 'v8'; import { TimingCategories } from '@mongosh/types'; import './webpack-self-inspection'; import { systemCA } from '@mongodb-js/devtools-proxy-support'; -import clr from './clr'; + // TS does not yet have type definitions for v8.startupSnapshot if ((v8 as any)?.startupSnapshot?.isBuildingSnapshot?.()) { From 132a48d4b8a089658c387b9a78ec12399d33d2d9 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 28 Nov 2025 14:00:16 +0100 Subject: [PATCH 03/12] chore: try nodenext --- configs/tsconfig-mongosh/tsconfig.common.json | 4 +- package-lock.json | 66 +++++++++++++++---- packages/arg-parser/package.json | 11 ++++ packages/arg-parser/src/index.ts | 2 - .../browser-repl/src/components/editor.tsx | 6 +- packages/build/package.json | 2 +- packages/build/src/download-center/config.ts | 2 +- packages/cli-repl/src/arg-parser.ts | 5 +- 8 files changed, 76 insertions(+), 22 deletions(-) diff --git a/configs/tsconfig-mongosh/tsconfig.common.json b/configs/tsconfig-mongosh/tsconfig.common.json index ecca337371..b1e364d62e 100644 --- a/configs/tsconfig-mongosh/tsconfig.common.json +++ b/configs/tsconfig-mongosh/tsconfig.common.json @@ -14,7 +14,7 @@ "removeComments": true, "target": "es2018", "lib": ["es2019"], - "module": "commonjs", - "moduleResolution": "node" + "module": "nodenext", + "moduleResolution": "nodenext" } } diff --git a/package-lock.json b/package-lock.json index 0cc5af91c8..0f51857135 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7107,17 +7107,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "node_modules/@mongodb-js/dl-center": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.3.0.tgz", - "integrity": "sha512-5fsbPhmok5uyTdr3G3wf8YUWJm/TQnBHBeqRQV4CsSW15MguAv8YEx8cF8YXB20G01izkHT72xkqb/Ry4SiHcg==", - "license": "Apache-2.0", - "dependencies": { - "ajv": "^6.12.5", - "aws-sdk": "^2.1441.0", - "node-fetch": "^2.6.7" - } - }, "node_modules/@mongodb-js/eslint-config-devtools": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/@mongodb-js/eslint-config-devtools/-/eslint-config-devtools-0.9.9.tgz", @@ -37433,7 +37422,7 @@ "license": "Apache-2.0", "dependencies": { "@mongodb-js/devtools-github-repo": "^1.4.2", - "@mongodb-js/dl-center": "^1.3.0", + "@mongodb-js/dl-center": "^1.4.4", "@mongodb-js/mongodb-downloader": "^0.3.7", "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", @@ -37486,6 +37475,59 @@ "node": ">= 16" } }, + "packages/build/node_modules/@mongodb-js/dl-center": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/dl-center/-/dl-center-1.4.4.tgz", + "integrity": "sha512-xhJd0ja7Nf7HSUcYkXQSfgQUEFNdKaRd9NTjc09aMzXa2+OM5dA0AeBSiQyRP6DGVcgFHc52/J0mBG3FzM8kMA==", + "license": "Apache-2.0", + "dependencies": { + "ajv": "^6.12.5", + "aws-sdk": "^2.1441.0", + "node-fetch": "^2.7.0" + } + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "packages/build/node_modules/@mongodb-js/dl-center/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "packages/build/node_modules/@mongodb-js/signing-utils": { "version": "0.3.7", "resolved": "https://registry.npmjs.org/@mongodb-js/signing-utils/-/signing-utils-0.3.7.tgz", diff --git a/packages/arg-parser/package.json b/packages/arg-parser/package.json index 975017967b..86be73655b 100644 --- a/packages/arg-parser/package.json +++ b/packages/arg-parser/package.json @@ -3,6 +3,17 @@ "version": "3.22.2", "description": "MongoDB Shell CLI Argument List Parser Package", "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "exports": { + ".": { + "default": "./lib/index.js", + "types": "./lib/index.d.ts" + }, + "./arg-parser": { + "default": "./lib/arg-parser.js", + "types": "./lib/arg-parser.d.ts" + } + }, "repository": { "type": "git", "url": "git://github.com/mongodb-js/mongosh.git" diff --git a/packages/arg-parser/src/index.ts b/packages/arg-parser/src/index.ts index f0fc68775c..fd999886fd 100644 --- a/packages/arg-parser/src/index.ts +++ b/packages/arg-parser/src/index.ts @@ -19,5 +19,3 @@ export function generateConnectionInfoFromCliArgs( const connectionString = generateUri(options); return mapCliToDriver(options, { connectionString, driverOptions: {} }); } - -export { parseCliArgs, UnknownCliArgumentError } from './arg-parser'; diff --git a/packages/browser-repl/src/components/editor.tsx b/packages/browser-repl/src/components/editor.tsx index 4e3302c11c..614f99621e 100644 --- a/packages/browser-repl/src/components/editor.tsx +++ b/packages/browser-repl/src/components/editor.tsx @@ -67,7 +67,7 @@ export function createCommands( }, { key: 'ArrowUp', - run: (context) => { + run: (context: any) => { const selection = context.state.selection.main; if (!selection.empty) { return false; @@ -88,7 +88,7 @@ export function createCommands( }, { key: 'ArrowDown', - run: (context) => { + run: (context: any) => { const selection = context.state.selection.main; if (!selection.empty) { return false; @@ -146,7 +146,7 @@ export class Editor extends Component { constructor(props: EditorProps) { super(props); - this.autocompleter = (context) => { + this.autocompleter = (context: any) => { if (!this.props.autocompleter?.getCompletions) { return null; } diff --git a/packages/build/package.json b/packages/build/package.json index c75fe33701..c75f480165 100644 --- a/packages/build/package.json +++ b/packages/build/package.json @@ -65,7 +65,7 @@ }, "dependencies": { "@mongodb-js/devtools-github-repo": "^1.4.2", - "@mongodb-js/dl-center": "^1.3.0", + "@mongodb-js/dl-center": "^1.4.4", "@mongodb-js/mongodb-downloader": "^0.3.7", "@mongodb-js/monorepo-tools": "^1.1.16", "@mongodb-js/signing-utils": "^0.3.7", diff --git a/packages/build/src/download-center/config.ts b/packages/build/src/download-center/config.ts index 9774e14040..fd6948c44d 100644 --- a/packages/build/src/download-center/config.ts +++ b/packages/build/src/download-center/config.ts @@ -6,7 +6,7 @@ import { major as majorVersion } from 'semver'; import type { DownloadCenterConfig, PlatformWithPackages, -} from '@mongodb-js/dl-center/dist/download-center-config'; +} from '@mongodb-js/dl-center'; import { ARTIFACTS_BUCKET, JSON_FEED_ARTIFACT_KEY, diff --git a/packages/cli-repl/src/arg-parser.ts b/packages/cli-repl/src/arg-parser.ts index 6051c6c1cd..4ff9762a4a 100644 --- a/packages/cli-repl/src/arg-parser.ts +++ b/packages/cli-repl/src/arg-parser.ts @@ -1,5 +1,8 @@ import i18n from '@mongosh/i18n'; -import { parseCliArgs, UnknownCliArgumentError } from '@mongosh/arg-parser'; +import { + parseCliArgs, + UnknownCliArgumentError, +} from '@mongosh/arg-parser/arg-parser'; import { colorizeForStderr as clr } from './clr'; import { USAGE } from './constants'; From fafa0ed1f314269fcce99ab94f43eb795d7b9044 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 28 Nov 2025 14:20:50 +0100 Subject: [PATCH 04/12] chore: run reformat, fix build --- packages/build/src/download-center/config.spec.ts | 2 +- packages/cli-repl/src/run.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/build/src/download-center/config.spec.ts b/packages/build/src/download-center/config.spec.ts index 4b0285758a..43d70cca91 100644 --- a/packages/build/src/download-center/config.spec.ts +++ b/packages/build/src/download-center/config.spec.ts @@ -1,4 +1,4 @@ -import type { DownloadCenterConfig } from '@mongodb-js/dl-center/dist/download-center-config'; +import type { DownloadCenterConfig } from '@mongodb-js/dl-center'; import { type PackageInformationProvider } from '../packaging'; import { expect } from 'chai'; import sinon from 'sinon'; diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 7c8cb83f7f..612bf42ee1 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -34,7 +34,6 @@ import { TimingCategories } from '@mongosh/types'; import './webpack-self-inspection'; import { systemCA } from '@mongodb-js/devtools-proxy-support'; - // TS does not yet have type definitions for v8.startupSnapshot if ((v8 as any)?.startupSnapshot?.isBuildingSnapshot?.()) { // Import a few nested deps of dependencies that cannot be included in the From 0d5aeb54d1ebf22acdbc7a139a64fffc6c03ab6f Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 28 Nov 2025 14:54:22 +0100 Subject: [PATCH 05/12] chore: fix check --- package-lock.json | 8 ++++---- packages/arg-parser/package.json | 1 + packages/cli-repl/package.json | 4 +--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0f51857135..4284ed126c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13335,9 +13335,9 @@ } }, "node_modules/@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, "license": "MIT" }, @@ -36712,6 +36712,7 @@ "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", + "@types/yargs-parser": "^21.0.3", "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", @@ -37849,7 +37850,6 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^15.0.0", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", diff --git a/packages/arg-parser/package.json b/packages/arg-parser/package.json index 86be73655b..bd90281eba 100644 --- a/packages/arg-parser/package.json +++ b/packages/arg-parser/package.json @@ -56,6 +56,7 @@ "@mongodb-js/eslint-config-mongosh": "^1.0.0", "@mongodb-js/prettier-config-devtools": "^1.0.1", "@mongodb-js/tsconfig-mongosh": "^1.0.0", + "@types/yargs-parser": "^21.0.3", "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index a3c57b26c4..020b9217bd 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -92,8 +92,7 @@ "pretty-repl": "^4.0.1", "semver": "^7.5.4", "strip-ansi": "^6.0.0", - "text-table": "^0.2.0", - "glibc-version": "^1.0.0" + "text-table": "^0.2.0" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", @@ -106,7 +105,6 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^15.0.0", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", From dac9f7205dc7f89d407d925d7e4964017e33ba37 Mon Sep 17 00:00:00 2001 From: gagik Date: Fri, 28 Nov 2025 16:18:29 +0100 Subject: [PATCH 06/12] chore: use zod schema for arg parser --- package-lock.json | 44 +- packages/arg-parser/package.json | 6 +- packages/arg-parser/src/arg-parser.spec.ts | 766 +++++++++++++++++---- packages/arg-parser/src/arg-parser.ts | 506 ++++++++++---- packages/cli-repl/package.json | 4 +- packages/snippet-manager/package.json | 4 +- 6 files changed, 1034 insertions(+), 296 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4284ed126c..d790595413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36689,9 +36689,9 @@ } }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -36716,10 +36716,43 @@ "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "strip-ansi": "^7.1.2" }, "engines": { "node": ">=14.15.1" + }, + "peerDependencies": { + "zod": "^3.25.76" + } + }, + "packages/arg-parser/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "packages/arg-parser/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "packages/async-rewriter2": { @@ -37850,6 +37883,7 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", + "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", @@ -38459,7 +38493,7 @@ "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", "tar": "^6.1.15", - "zod": "^3.24.1" + "zod": "^3.25.76" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", diff --git a/packages/arg-parser/package.json b/packages/arg-parser/package.json index bd90281eba..032c2326a9 100644 --- a/packages/arg-parser/package.json +++ b/packages/arg-parser/package.json @@ -51,6 +51,9 @@ "mongodb-connection-string-url": "^3.0.2", "yargs-parser": "^20.2.4" }, + "peerDependencies": { + "zod": "^3.25.76" + }, "devDependencies": { "@mongodb-js/devtools-connect": "^3.9.4", "@mongodb-js/eslint-config-mongosh": "^1.0.0", @@ -60,6 +63,7 @@ "depcheck": "^1.4.7", "eslint": "^7.25.0", "mongodb": "^6.19.0", - "prettier": "^2.8.8" + "prettier": "^2.8.8", + "strip-ansi": "^7.1.2" } } diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts index c9ad3edf55..1923823b7f 100644 --- a/packages/arg-parser/src/arg-parser.spec.ts +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -1,6 +1,16 @@ import { MongoshUnimplementedError } from '@mongosh/errors'; import { expect } from 'chai'; -import { getLocale, parseCliArgs, UnknownCliArgumentError } from './arg-parser'; +import stripAnsi from 'strip-ansi'; +import { + CliOptionsSchema, + coerceIfBoolean, + generateYargsOptionsFromSchema, + getLocale, + parseCliArgs, + parseMongoshCliOptionsArgs, + UnknownCliArgumentError, +} from './arg-parser'; +import { z } from 'zod/v4'; describe('arg-parser', function () { describe('.getLocale', function () { @@ -80,7 +90,9 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); }); @@ -92,11 +104,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--ipv6']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the ipv6 value in the object', function () { - expect(parseCliArgs(argv).ipv6).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.ipv6).to.equal( + true + ); }); }); @@ -104,11 +120,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-h']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( + true + ); }); }); @@ -116,11 +136,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--help']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the help value in the object', function () { - expect(parseCliArgs(argv).help).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( + true + ); }); }); @@ -128,11 +152,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--version']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the version value in the object', function () { - expect(parseCliArgs(argv).version).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.version).to.equal( + true + ); }); }); @@ -140,11 +168,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--verbose']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the verbose value in the object', function () { - expect(parseCliArgs(argv).verbose).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.verbose).to.equal( + true + ); }); }); @@ -152,11 +184,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--shell']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the shell value in the object', function () { - expect(parseCliArgs(argv).shell).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.shell).to.equal( + true + ); }); }); @@ -164,12 +200,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--nodb']; it('does not return the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); - expect(parseCliArgs(argv).fileNames).to.deep.equal([uri]); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames + ).to.deep.equal([uri]); }); it('sets the nodb value in the object', function () { - expect(parseCliArgs(argv).nodb).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.nodb).to.equal( + true + ); }); }); @@ -177,11 +219,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--norc']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the norc value in the object', function () { - expect(parseCliArgs(argv).norc).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.norc).to.equal( + true + ); }); }); @@ -189,11 +235,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--quiet']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the quiet value in the object', function () { - expect(parseCliArgs(argv).quiet).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.quiet).to.equal( + true + ); }); }); @@ -201,11 +251,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--eval', '1+1']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1']); + expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( + ['1+1'] + ); }); }); @@ -213,11 +267,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the eval value in the object', function () { - expect(parseCliArgs(argv).eval).to.deep.equal(['1+1', '2+2']); + expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( + ['1+1', '2+2'] + ); }); }); @@ -225,11 +283,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--retryWrites']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the retryWrites value in the object', function () { - expect(parseCliArgs(argv).retryWrites).to.equal(true); + expect( + parseMongoshCliOptionsArgs(argv).options.retryWrites + ).to.equal(true); }); }); @@ -238,14 +300,15 @@ describe('arg-parser', function () { it('raises an error', function () { try { - parseCliArgs(argv); + parseMongoshCliOptionsArgs(argv).options; } catch (err: any) { if (err instanceof UnknownCliArgumentError) { - expect(err.argument).equals('--what'); + expect(stripAnsi(err.message)).to.equal( + 'Unknown argument: --what' + ); return; - } else { - expect.fail('Did not throw an unknown cli error'); } + expect.fail('Expected UnknownCliArgumentError'); } expect.fail('parsing unknown parameter did not throw'); }); @@ -257,11 +320,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-u', 'richard']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); + expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( + 'richard' + ); }); }); @@ -269,11 +336,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--username', 'richard']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the username in the object', function () { - expect(parseCliArgs(argv).username).to.equal('richard'); + expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( + 'richard' + ); }); }); @@ -281,11 +352,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-p', 'pw']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); + expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( + 'pw' + ); }); }); @@ -293,11 +368,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--password', 'pw']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the password in the object', function () { - expect(parseCliArgs(argv).password).to.equal('pw'); + expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( + 'pw' + ); }); }); @@ -305,11 +384,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the authenticationDatabase in the object', function () { - expect(parseCliArgs(argv).authenticationDatabase).to.equal('db'); + expect( + parseMongoshCliOptionsArgs(argv).options.authenticationDatabase + ).to.equal('db'); }); }); @@ -322,13 +405,15 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the authenticationMechanism in the object', function () { - expect(parseCliArgs(argv).authenticationMechanism).to.equal( - 'SCRAM-SHA-256' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.authenticationMechanism + ).to.equal('SCRAM-SHA-256'); }); }); @@ -336,11 +421,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the gssapiServiceName in the object', function () { - expect(parseCliArgs(argv).gssapiServiceName).to.equal('mongosh'); + expect( + parseMongoshCliOptionsArgs(argv).options.gssapiServiceName + ).to.equal('mongosh'); }); }); @@ -349,7 +438,7 @@ describe('arg-parser', function () { it('throws an error since it is not supported', function () { try { - parseCliArgs(argv); + parseMongoshCliOptionsArgs(argv).options; } catch (e: any) { expect(e).to.be.instanceOf(MongoshUnimplementedError); expect(e.message).to.include( @@ -361,11 +450,11 @@ describe('arg-parser', function () { }); // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); // }); // it('sets the gssapiHostName in the object', () => { - // expect(parseCliArgs(argv).gssapiHostName).to.equal('example.com'); + // expect(parseMongoshCliOptionsArgs(argv).options.gssapiHostName).to.equal('example.com'); // }); }); @@ -378,13 +467,16 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiHostnameCanonicalization).to.equal( - 'forward' - ); + expect( + parseMongoshCliOptionsArgs(argv).options + .sspiHostnameCanonicalization + ).to.equal('forward'); }); }); @@ -397,13 +489,15 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the gssapiHostName in the object', function () { - expect(parseCliArgs(argv).sspiRealmOverride).to.equal( - 'example2.com' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.sspiRealmOverride + ).to.equal('example2.com'); }); }); @@ -411,11 +505,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the awsIamSessionToken in the object', function () { - expect(parseCliArgs(argv).awsIamSessionToken).to.equal('tok'); + expect( + parseMongoshCliOptionsArgs(argv).options.awsIamSessionToken + ).to.equal('tok'); }); }); }); @@ -425,11 +523,13 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tls']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); }); }); @@ -437,11 +537,13 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-tls']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tls in the object', function () { - expect(parseCliArgs(argv).tls).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); }); }); @@ -449,11 +551,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + ).to.equal('test'); }); }); @@ -463,11 +569,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCertificateKeyFile in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFile).to.equal('test'); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + ).to.equal('test'); }); } ); @@ -481,13 +591,16 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCertificateKeyFilePassword in the object', function () { - expect(parseCliArgs(argv).tlsCertificateKeyFilePassword).to.equal( - 'test' - ); + expect( + parseMongoshCliOptionsArgs(argv).options + .tlsCertificateKeyFilePassword + ).to.equal('test'); }); }); @@ -495,11 +608,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCAFile in the object', function () { - expect(parseCliArgs(argv).tlsCAFile).to.equal('test'); + expect(parseMongoshCliOptionsArgs(argv).options.tlsCAFile).to.equal( + 'test' + ); }); }); @@ -507,11 +624,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCRLFile in the object', function () { - expect(parseCliArgs(argv).tlsCRLFile).to.equal('test'); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCRLFile + ).to.equal('test'); }); }); @@ -519,11 +640,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsAllowInvalidHostnames in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidHostnames).to.equal(true); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsAllowInvalidHostnames + ).to.equal(true); }); }); @@ -531,13 +656,16 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsAllowInvalidCertificates in the object', function () { - expect(parseCliArgs(argv).tlsAllowInvalidCertificates).to.equal( - true - ); + expect( + parseMongoshCliOptionsArgs(argv).options + .tlsAllowInvalidCertificates + ).to.equal(true); }); }); @@ -546,7 +674,7 @@ describe('arg-parser', function () { it('throws an error since it is not supported', function () { try { - parseCliArgs(argv); + parseMongoshCliOptionsArgs(argv).options; } catch (e: any) { expect(e).to.be.instanceOf(MongoshUnimplementedError); expect(e.message).to.include( @@ -558,11 +686,11 @@ describe('arg-parser', function () { }); // it('returns the URI in the object', () => { - // expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); // }); // it('sets the tlsFIPSMode in the object', () => { - // expect(parseCliArgs(argv).tlsFIPSMode).to.equal(true); + // expect(parseMongoshCliOptionsArgs(argv).options.tlsFIPSMode).to.equal(true); // }); }); @@ -570,11 +698,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsCertificateSelector in the object', function () { - expect(parseCliArgs(argv).tlsCertificateSelector).to.equal('test'); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsCertificateSelector + ).to.equal('test'); }); }); @@ -587,13 +719,15 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the tlsDisabledProtocols in the object', function () { - expect(parseCliArgs(argv).tlsDisabledProtocols).to.equal( - 'TLS1_0,TLS2_0' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.tlsDisabledProtocols + ).to.equal('TLS1_0,TLS2_0'); }); }); }); @@ -603,11 +737,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the awsAccessKeyId in the object', function () { - expect(parseCliArgs(argv).awsAccessKeyId).to.equal('foo'); + expect( + parseMongoshCliOptionsArgs(argv).options.awsAccessKeyId + ).to.equal('foo'); }); }); @@ -615,11 +753,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the awsSecretAccessKey in the object', function () { - expect(parseCliArgs(argv).awsSecretAccessKey).to.equal('foo'); + expect( + parseMongoshCliOptionsArgs(argv).options.awsSecretAccessKey + ).to.equal('foo'); }); }); @@ -627,11 +769,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the awsSessionToken in the object', function () { - expect(parseCliArgs(argv).awsSessionToken).to.equal('foo'); + expect( + parseMongoshCliOptionsArgs(argv).options.awsSessionToken + ).to.equal('foo'); }); }); @@ -639,11 +785,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the keyVaultNamespace in the object', function () { - expect(parseCliArgs(argv).keyVaultNamespace).to.equal('foo.bar'); + expect( + parseMongoshCliOptionsArgs(argv).options.keyVaultNamespace + ).to.equal('foo.bar'); }); }); @@ -651,11 +801,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the kmsURL in the object', function () { - expect(parseCliArgs(argv).kmsURL).to.equal('example.com'); + expect(parseMongoshCliOptionsArgs(argv).options.kmsURL).to.equal( + 'example.com' + ); }); }); }); @@ -665,11 +819,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--apiVersion', '1']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiVersion).to.equal('1'); + expect( + parseMongoshCliOptionsArgs(argv).options.apiVersion + ).to.equal('1'); }); }); @@ -677,11 +835,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--apiDeprecationErrors']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiDeprecationErrors).to.equal(true); + expect( + parseMongoshCliOptionsArgs(argv).options.apiDeprecationErrors + ).to.equal(true); }); }); @@ -689,11 +851,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '--apiStrict']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the apiVersion in the object', function () { - expect(parseCliArgs(argv).apiStrict).to.equal(true); + expect(parseMongoshCliOptionsArgs(argv).options.apiStrict).to.equal( + true + ); }); }); }); @@ -703,12 +869,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.js'); }); }); @@ -716,12 +888,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.mongodb'); }); }); @@ -729,12 +907,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); }); }); @@ -742,12 +926,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); }); }); @@ -762,12 +952,18 @@ describe('arg-parser', function () { ]; it('returns the URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(uri); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(uri); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.txt'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.txt'); }); }); }); @@ -777,12 +973,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, 'test1.js', 'test2.js']; it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.js'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.js'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.js'); }); }); @@ -790,12 +992,18 @@ describe('arg-parser', function () { const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; it('returns no URI in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(undefined); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(undefined); }); it('sets the filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test1.mongodb'); - expect(parseCliArgs(argv).fileNames?.[1]).to.equal('test2.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test1.mongodb'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + ).to.equal('test2.mongodb'); }); }); @@ -803,13 +1011,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, 'test1.txt', 'test2.txt']; it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'test1.txt' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('test1.txt'); }); it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test2.txt'); }); }); @@ -817,13 +1027,15 @@ describe('arg-parser', function () { const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.foo.js' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('mongodb://domain.foo.js'); }); it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal('test2.txt'); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('test2.txt'); }); }); @@ -838,15 +1050,15 @@ describe('arg-parser', function () { ]; it('returns the first filename as an URI', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal( - 'mongodb://domain.bar.js' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal('mongodb://domain.bar.js'); }); it('uses the remainder as filenames', function () { - expect(parseCliArgs(argv).fileNames?.[0]).to.equal( - 'mongodb://domain.foo.js' - ); + expect( + parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + ).to.equal('mongodb://domain.foo.js'); }); } ); @@ -860,7 +1072,9 @@ describe('arg-parser', function () { const argv = [...baseArgv, db]; it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(db); }); }); @@ -869,7 +1083,9 @@ describe('arg-parser', function () { const argv = [...baseArgv, db]; it('sets the db in the object', function () { - expect(parseCliArgs(argv).connectionSpecifier).to.equal(db); + expect( + parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + ).to.equal(db); }); }); }); @@ -879,7 +1095,9 @@ describe('arg-parser', function () { const argv = [...baseArgv, '--host', 'example.com']; it('sets the host value in the object', function () { - expect(parseCliArgs(argv).host).to.equal('example.com'); + expect(parseMongoshCliOptionsArgs(argv).options.host).to.equal( + 'example.com' + ); }); }); @@ -887,7 +1105,9 @@ describe('arg-parser', function () { const argv = [...baseArgv, '--port', '20000']; it('sets the port value in the object', function () { - expect(parseCliArgs(argv).port).to.equal('20000'); + expect(parseMongoshCliOptionsArgs(argv).options.port).to.equal( + '20000' + ); }); }); }); @@ -938,11 +1158,263 @@ describe('arg-parser', function () { argv.push(value); } - const args = parseCliArgs(argv); + const args = parseMongoshCliOptionsArgs(argv).options; expect(args).to.not.have.property(deprecated); expect(args[replacement]).to.equal(value ?? true); }); } }); }); + + describe('union type fields', function () { + for (const { argument, values, strict } of [ + { argument: 'json', values: ['relaxed', 'canonical'] }, + { argument: 'oidcDumpTokens', values: ['redacted', 'include-secrets'] }, + { argument: 'browser', values: ['test'], strict: false }, + ] as const) { + const baseArgv = ['node', 'mongosh', 'mongodb://domain.com:20000']; + describe(`with ${argument}`, function () { + context('with boolean', function () { + it(`get set to true with --${argument}`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}`]) + .options[argument] + ).to.equal(true); + }); + + it(`sets to true with --${argument}=true`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=true`]) + .options[argument] + ).to.equal(true); + }); + + it(`sets to false with --${argument}=false`, function () { + expect( + parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=false`]) + .options[argument] + ).to.equal(false); + }); + }); + + for (const value of values) { + context('with string value', function () { + // This matches the legacy behavior pre-Zod schema migration. + it(`does not work with "--${argument} ${value}"`, function () { + expect( + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument} ${value}`, + ]).options[argument] + ).to.be.undefined; + }); + + it(`works "--${argument}=${value}"`, function () { + expect( + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument}=${value}`, + ]).options[argument] + ).to.equal(value); + }); + }); + } + + if (strict) { + it('throws an error with invalid value', function () { + try { + parseMongoshCliOptionsArgs([ + ...baseArgv, + `--${argument}`, + 'invalid', + ]); + } catch (e: any) { + expect(e).to.be.instanceOf(MongoshUnimplementedError); + expect(e.message).to.include( + `--${argument} can only have the values ${values.join(', ')}` + ); + return; + } + expect.fail('Expected error'); + }); + } + }); + } + }); + + const testSchema = z.object({ + name: z.string(), + age: z.number(), + isAdmin: z.boolean(), + roles: z.array(z.string()), + }); + + describe('generateYargsOptions', function () { + it('generates from arbitrary schema', function () { + const options = generateYargsOptionsFromSchema({ + schema: testSchema, + configuration: { + 'combine-arrays': true, + }, + }); + + expect(options).to.deep.equal({ + string: ['name'], + number: ['age'], + boolean: ['isAdmin'], + array: ['roles'], + coerce: {}, + alias: {}, + configuration: { + 'combine-arrays': true, + }, + }); + }); + + it('generates the expected options for Cli Options', function () { + const options = generateYargsOptionsFromSchema({ + schema: CliOptionsSchema, + }); + + const expected = { + string: [ + 'apiVersion', + 'authenticationDatabase', + 'authenticationMechanism', + 'awsAccessKeyId', + 'awsIamSessionToken', + 'awsSecretAccessKey', + 'awsSessionToken', + 'csfleLibraryPath', + 'cryptSharedLibPath', + 'db', + 'gssapiHostName', + 'gssapiServiceName', + 'sspiHostnameCanonicalization', + 'sspiRealmOverride', + 'jsContext', + 'host', + 'keyVaultNamespace', + 'kmsURL', + 'locale', + 'oidcFlows', + 'oidcRedirectUri', + 'password', + 'port', + 'sslPEMKeyFile', + 'sslPEMKeyPassword', + 'sslCAFile', + 'sslCertificateSelector', + 'sslCRLFile', + 'sslDisabledProtocols', + 'tlsCAFile', + 'tlsCertificateKeyFile', + 'tlsCertificateKeyFilePassword', + 'tlsCertificateSelector', + 'tlsCRLFile', + 'tlsDisabledProtocols', + 'username', + ], + boolean: [ + 'apiDeprecationErrors', + 'apiStrict', + 'buildInfo', + 'exposeAsyncRewriter', + 'help', + 'ipv6', + 'nodb', + 'norc', + 'oidcTrustedEndpoint', + 'oidcIdTokenAsAccessToken', + 'oidcNoNonce', + 'perfTests', + 'quiet', + 'retryWrites', + 'shell', + 'smokeTests', + 'skipStartupWarnings', + 'ssl', + 'sslAllowInvalidCertificates', + 'sslAllowInvalidHostnames', + 'sslFIPSMode', + 'tls', + 'tlsAllowInvalidCertificates', + 'tlsAllowInvalidHostnames', + 'tlsFIPSMode', + 'tlsUseSystemCA', + 'verbose', + 'version', + ], + array: ['eval', 'file'], + coerce: { + json: coerceIfBoolean, + oidcDumpTokens: coerceIfBoolean, + browser: coerceIfBoolean, + }, + alias: { + h: 'help', + p: 'password', + u: 'username', + f: 'file', + 'build-info': 'buildInfo', + oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time + oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto + }, + configuration: { + 'camel-case-expansion': false, + 'unknown-options-as-args': true, + 'parse-positional-numbers': false, + 'parse-numbers': false, + 'greedy-arrays': false, + 'short-option-groups': false, + }, + }; + + // Compare arrays without caring about order + expect(options.string?.sort()).to.deep.equal(expected.string.sort()); + expect(options.boolean?.sort()).to.deep.equal(expected.boolean.sort()); + expect(options.array?.sort()).to.deep.equal(expected.array.sort()); + + // Compare non-array properties normally + expect(options.alias).to.deep.equal(expected.alias); + expect(options.configuration).to.deep.equal(expected.configuration); + }); + }); + + describe('parseCliArgs', function () { + it('parses the expected options for Cli Options', function () { + const options = parseCliArgs({ + args: ['--port', '20000', '--ssl', '1', '--unknownField', '1'], + schema: CliOptionsSchema, + }); + + expect(options).to.deep.equal({ + _: ['1', '--unknownField', '1'], + port: '20000', + ssl: true, + }); + }); + }); + + it('parses extended schema', function () { + const options = parseCliArgs({ + args: [ + '--port', + '20000', + '--extendedField', + '90', + '--unknownField', + '100', + ], + schema: CliOptionsSchema.extend({ + extendedField: z.number(), + }), + }); + + expect(options).to.deep.equal({ + _: ['--unknownField', '100'], + port: '20000', + extendedField: 90, + }); + }); }); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index 3d323aa466..5d1f237361 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -1,96 +1,200 @@ import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; import i18n from '@mongosh/i18n'; +import type { CliOptions } from '@mongosh/arg-parser'; import parser from 'yargs-parser'; -import type { CliOptions } from './cli-options'; +import { z } from 'zod/v4'; +import type { Options as YargsOptions } from 'yargs-parser'; /** - * The yargs-parser options configuration. + * Custom registry for CLI options metadata */ -const OPTIONS = { - string: [ - 'apiVersion', - 'authenticationDatabase', - 'authenticationMechanism', - 'awsAccessKeyId', - 'awsIamSessionToken', - 'awsSecretAccessKey', - 'awsSessionToken', - 'awsIamSessionToken', - 'browser', - 'csfleLibraryPath', - 'cryptSharedLibPath', - 'db', - 'gssapiHostName', - 'gssapiServiceName', - 'sspiHostnameCanonicalization', - 'sspiRealmOverride', - 'jsContext', - 'host', - 'keyVaultNamespace', - 'kmsURL', - 'locale', - 'oidcFlows', - 'oidcRedirectUri', - 'password', - 'port', - 'sslPEMKeyFile', - 'sslPEMKeyPassword', - 'sslCAFile', - 'sslCertificateSelector', - 'sslCRLFile', - 'sslDisabledProtocols', - 'tlsCAFile', - 'tlsCertificateKeyFile', - 'tlsCertificateKeyFilePassword', - 'tlsCertificateSelector', - 'tlsCRLFile', - 'tlsDisabledProtocols', - 'username', - ], - boolean: [ - 'apiDeprecationErrors', - 'apiStrict', - 'buildInfo', - 'exposeAsyncRewriter', - 'help', - 'ipv6', - 'nodb', - 'norc', - 'oidcTrustedEndpoint', - 'oidcIdTokenAsAccessToken', - 'oidcNoNonce', - 'perfTests', - 'quiet', - 'retryWrites', - 'shell', - 'smokeTests', - 'skipStartupWarnings', - 'ssl', - 'sslAllowInvalidCertificates', - 'sslAllowInvalidHostnames', - 'sslFIPSMode', - 'tls', - 'tlsAllowInvalidCertificates', - 'tlsAllowInvalidHostnames', - 'tlsFIPSMode', - 'tlsUseSystemCA', - 'verbose', - 'version', - ], - array: ['eval', 'file'], - alias: { - h: 'help', - p: 'password', - u: 'username', - f: 'file', - 'build-info': 'buildInfo', - json: 'json', // List explicitly here since it can be a boolean or a string - browser: 'browser', // ditto - oidcDumpTokens: 'oidcDumpTokens', // ditto - oidcRedirectUrl: 'oidcRedirectUri', // I'd get this wrong about 50% of the time - oidcIDTokenAsAccessToken: 'oidcIdTokenAsAccessToken', // ditto - }, - configuration: { +export const cliOptionsRegistry = z.registry(); + +/** + * CLI options schema with metadata attached via registry + */ +export const CliOptionsSchema = z + .object({ + // String options + apiVersion: z.string().optional(), + authenticationDatabase: z.string().optional(), + authenticationMechanism: z.string().optional(), + awsAccessKeyId: z.string().optional(), + awsIamSessionToken: z.string().optional(), + awsSecretAccessKey: z.string().optional(), + awsSessionToken: z.string().optional(), + csfleLibraryPath: z.string().optional(), + cryptSharedLibPath: z.string().optional(), + db: z.string().optional(), + gssapiHostName: z + .string() + .optional() + .register(cliOptionsRegistry, { unsupported: true }), + gssapiServiceName: z.string().optional(), + sspiHostnameCanonicalization: z.string().optional(), + sspiRealmOverride: z.string().optional(), + jsContext: z.string().optional(), + host: z.string().optional(), + keyVaultNamespace: z.string().optional(), + kmsURL: z.string().optional(), + locale: z.string().optional(), + oidcFlows: z.string().optional(), + oidcRedirectUri: z + .string() + .optional() + .register(cliOptionsRegistry, { + alias: ['oidcRedirectUrl'], + }), + password: z + .string() + .optional() + .register(cliOptionsRegistry, { alias: ['p'] }), + port: z.string().optional(), + username: z + .string() + .optional() + .register(cliOptionsRegistry, { alias: ['u'] }), + + // Deprecated SSL options (now TLS) + sslPEMKeyFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateKeyFile', + }), + sslPEMKeyPassword: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateKeyFilePassword', + }), + sslCAFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCAFile', + }), + sslCertificateSelector: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCertificateSelector', + }), + sslCRLFile: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsCRLFile', + }), + sslDisabledProtocols: z.string().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsDisabledProtocols', + }), + + // TLS options + tlsCAFile: z.string().optional(), + tlsCertificateKeyFile: z.string().optional(), + tlsCertificateKeyFilePassword: z.string().optional(), + tlsCertificateSelector: z.string().optional(), + tlsCRLFile: z.string().optional(), + tlsDisabledProtocols: z.string().optional(), + + // Boolean options + apiDeprecationErrors: z.boolean().optional(), + apiStrict: z.boolean().optional(), + buildInfo: z + .boolean() + .optional() + .register(cliOptionsRegistry, { alias: ['build-info'] }), + exposeAsyncRewriter: z.boolean().optional(), + help: z + .boolean() + .optional() + .register(cliOptionsRegistry, { alias: ['h'] }), + ipv6: z.boolean().optional(), + nodb: z.boolean().optional(), + norc: z.boolean().optional(), + oidcTrustedEndpoint: z.boolean().optional(), + oidcIdTokenAsAccessToken: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + alias: ['oidcIDTokenAsAccessToken'], + }), + oidcNoNonce: z.boolean().optional(), + perfTests: z.boolean().optional(), + quiet: z.boolean().optional(), + retryWrites: z.boolean().optional(), + shell: z.boolean().optional(), + smokeTests: z.boolean().optional(), + skipStartupWarnings: z.boolean().optional(), + verbose: z.boolean().optional(), + version: z.boolean().optional(), + + // Deprecated SSL boolean options + ssl: z + .boolean() + .optional() + .register(cliOptionsRegistry, { deprecationReplacement: 'tls' }), + sslAllowInvalidCertificates: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + deprecationReplacement: 'tlsAllowInvalidCertificates', + }), + sslAllowInvalidHostnames: z + .boolean() + .optional() + .register(cliOptionsRegistry, { + deprecationReplacement: 'tlsAllowInvalidHostnames', + }), + sslFIPSMode: z.boolean().optional().register(cliOptionsRegistry, { + deprecationReplacement: 'tlsFIPSMode', + unsupported: true, + }), + + // TLS boolean options + tls: z.boolean().optional(), + tlsAllowInvalidCertificates: z.boolean().optional(), + tlsAllowInvalidHostnames: z.boolean().optional(), + tlsFIPSMode: z.boolean().optional(), + tlsUseSystemCA: z.boolean().optional(), + + // Array options + eval: z.array(z.string()).optional(), + file: z + .array(z.string()) + .optional() + .register(cliOptionsRegistry, { alias: ['f'] }), + + // Options that can be boolean or string + json: z.union([z.boolean(), z.enum(['relaxed', 'canonical'])]).optional(), + oidcDumpTokens: z + .union([z.boolean(), z.enum(['redacted', 'include-secrets'])]) + .optional(), + browser: z.union([z.boolean(), z.string()]).optional(), + }) + .loose(); + +/** + * Metadata that can be used to define the yargs-parser configuration for a field. + */ +export type YargsOptionsMetadata = { + alias?: string[]; +}; + +/** + * Type for option metadata + */ +export type CliOptionsRegistryMetadata = YargsOptionsMetadata & { + deprecationReplacement?: keyof CliOptions; + unsupported?: boolean; +}; + +/** + * Extract metadata for a field using the custom registry + */ +const getCliOptionsMetadata = ( + fieldName: string +): CliOptionsRegistryMetadata | undefined => { + const fieldSchema = + CliOptionsSchema.shape[fieldName as keyof typeof CliOptionsSchema.shape]; + if (!fieldSchema) { + return undefined; + } + return cliOptionsRegistry.get(fieldSchema); +}; + +/** + * Generate yargs-parser configuration from schema + */ +export function generateYargsOptionsFromSchema({ + schema = CliOptionsSchema, + configuration = { 'camel-case-expansion': false, 'unknown-options-as-args': true, 'parse-positional-numbers': false, @@ -98,28 +202,112 @@ const OPTIONS = { 'greedy-arrays': false, 'short-option-groups': false, }, -}; +}: { + schema?: z.ZodObject; + configuration?: YargsOptions['configuration']; +}): YargsOptions { + const options = { + string: [], + boolean: [], + array: [], + alias: >{}, + coerce: unknown>>{}, + number: [], + } satisfies Required< + Pick< + YargsOptions, + 'string' | 'boolean' | 'array' | 'alias' | 'coerce' | 'number' + > + >; + + for (const [fieldName, fieldSchema] of Object.entries(schema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + + // Unwrap optional type + let unwrappedType = fieldSchema; + if (fieldSchema instanceof z.ZodOptional) { + unwrappedType = fieldSchema.unwrap(); + } + + // Determine type + if (unwrappedType instanceof z.ZodArray) { + options.array.push(fieldName); + } else if (unwrappedType instanceof z.ZodBoolean) { + options.boolean.push(fieldName); + } else if (unwrappedType instanceof z.ZodString) { + options.string.push(fieldName); + } else if (unwrappedType instanceof z.ZodNumber) { + options.number.push(fieldName); + } else if (unwrappedType instanceof z.ZodUnion) { + // Handle union types (like json, browser, oidcDumpTokens) + // Check if the union includes boolean + const unionOptions = ( + unwrappedType as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]> + ).options; + const hasBoolean = unionOptions.some( + (opt) => opt instanceof z.ZodBoolean + ); + const hasString = unionOptions.some( + (opt) => opt instanceof z.ZodString || opt instanceof z.ZodEnum + ); + + if (hasString && !hasBoolean) { + options.string.push(fieldName); + } + + if (hasBoolean && hasString) { + // When a field has both boolean and string, we add a coerce function to the field. + // This allows to get a value in both -- and --= for boolean and string. + options.coerce[fieldName] = coerceIfBoolean; + } + } else { + throw new Error(`Unknown field type: ${unwrappedType.constructor.name}`); + } + + // Add aliases + if (meta?.alias) { + for (const a of meta.alias) { + options.alias[a] = fieldName; + } + } + } + + return { + ...options, + configuration, + }; +} /** - * Maps deprecated arguments to their new counterparts. + * Maps deprecated arguments to their new counterparts, derived from schema metadata. */ -const DEPRECATED_ARGS_WITH_REPLACEMENT: Record = { - ssl: 'tls', - sslAllowInvalidCertificates: 'tlsAllowInvalidCertificates', - sslAllowInvalidHostnames: 'tlsAllowInvalidHostnames', - sslFIPSMode: 'tlsFIPSMode', - sslPEMKeyFile: 'tlsCertificateKeyFile', - sslPEMKeyPassword: 'tlsCertificateKeyFilePassword', - sslCAFile: 'tlsCAFile', - sslCertificateSelector: 'tlsCertificateSelector', - sslCRLFile: 'tlsCRLFile', - sslDisabledProtocols: 'tlsDisabledProtocols', -}; +function getDeprecatedArgsWithReplacement(): Record< + keyof z.infer, + keyof CliOptions +> { + const deprecated: Record = {}; + for (const fieldName of Object.keys(CliOptionsSchema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + if (meta?.deprecationReplacement) { + deprecated[fieldName] = meta.deprecationReplacement; + } + } + return deprecated; +} /** - * If an unsupported argument is given an error will be thrown. + * Get list of unsupported arguments, derived from schema metadata. */ -const UNSUPPORTED_ARGS: Readonly = ['sslFIPSMode', 'gssapiHostName']; +function getUnsupportedArgs(schema: z.ZodObject): string[] { + const unsupported: string[] = []; + for (const fieldName of Object.keys(schema.shape)) { + const meta = getCliOptionsMetadata(fieldName); + if (meta?.unsupported) { + unsupported.push(fieldName); + } + } + return unsupported; +} /** * Determine the locale of the shell. @@ -146,54 +334,77 @@ function isConnectionSpecifier(arg?: string): boolean { ); } +export function parseCliArgs({ + args, + schema, + parserConfiguration, +}: { + args: string[]; + schema?: z.ZodObject; + parserConfiguration?: YargsOptions['configuration']; +}): T & parser.Arguments { + const options = generateYargsOptionsFromSchema({ + schema, + configuration: parserConfiguration, + }); + + return parser(args, options) as unknown as T & parser.Arguments; +} + /** - * Parses arguments into a JS object. + * Parses mongosh-specific arguments into a JS object. * * @param args - The CLI arguments. * * @returns The arguments as cli options. */ -export function parseCliArgs(args: string[]): - | CliOptions & { +export function parseMongoshCliOptionsArgs(args: string[]): { + options: CliOptions; + warnings: string[]; +} { + const programArgs = args.slice(2); + i18n.setLocale(getLocale(programArgs, process.env)); + + const parsed = parseCliArgs< + CliOptions & { smokeTests: boolean; perfTests: boolean; buildInfo: boolean; - _argParseWarnings: string[]; - } { - const programArgs = args.slice(2); - i18n.setLocale(getLocale(programArgs, process.env)); + file?: string[]; + } + >({ args: programArgs, schema: CliOptionsSchema }); - const parsed = parser(programArgs, OPTIONS) as unknown as CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - _argParseWarnings: string[]; - _?: string[]; - file?: string[]; - }; const positionalArguments = parsed._ ?? []; for (const arg of positionalArguments) { - if (arg.startsWith('-')) { + if (typeof arg === 'string' && arg.startsWith('-')) { throw new UnknownCliArgumentError(arg); } } - if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { - parsed.connectionSpecifier = positionalArguments.shift(); + if (typeof positionalArguments[0] === 'string') { + if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { + parsed.connectionSpecifier = positionalArguments.shift() as string; + } } - parsed.fileNames = [...(parsed.file ?? []), ...positionalArguments]; - - // All positional arguments are either in connectionSpecifier or fileNames, - // and should only be accessed that way now. - delete parsed._; - parsed._argParseWarnings = verifyCliArguments(parsed); + // Remove the _ property from the parsed object + const { _: _exclude, ...parsedCliOptions } = parsed; - return parsed; + return { + options: { + ...parsedCliOptions, + fileNames: [ + ...(parsedCliOptions.file ?? []), + ...(positionalArguments as string[]), + ], + }, + warnings: verifyCliArguments(parsed), + }; } -export function verifyCliArguments(args: any /* CliOptions */): string[] { - for (const unsupported of UNSUPPORTED_ARGS) { +function verifyCliArguments(args: CliOptions): string[] { + const unsupportedArgs = getUnsupportedArgs(CliOptionsSchema); + for (const unsupported of unsupportedArgs) { if (unsupported in args) { throw new MongoshUnimplementedError( `Argument --${unsupported} is not supported in mongosh`, @@ -202,18 +413,17 @@ export function verifyCliArguments(args: any /* CliOptions */): string[] { } } - if (![undefined, true, false, 'relaxed', 'canonical'].includes(args.json)) { + const jsonValidation = CliOptionsSchema.shape.json.safeParse(args.json); + if (!jsonValidation.success) { throw new MongoshUnimplementedError( '--json can only have the values relaxed or canonical', CommonErrors.InvalidArgument ); } - if ( - ![undefined, true, false, 'redacted', 'include-secrets'].includes( - args.oidcDumpTokens - ) - ) { + const oidcDumpTokensValidation = + CliOptionsSchema.shape.oidcDumpTokens.safeParse(args.oidcDumpTokens); + if (!oidcDumpTokensValidation.success) { throw new MongoshUnimplementedError( '--oidcDumpTokens can only have the values redacted or include-secrets', CommonErrors.InvalidArgument @@ -221,20 +431,36 @@ export function verifyCliArguments(args: any /* CliOptions */): string[] { } const messages = []; - for (const deprecated in DEPRECATED_ARGS_WITH_REPLACEMENT) { + const deprecatedArgs = getDeprecatedArgsWithReplacement(); + for (const deprecated of Object.keys(deprecatedArgs)) { if (deprecated in args) { - const replacement = DEPRECATED_ARGS_WITH_REPLACEMENT[deprecated]; + const replacement = deprecatedArgs[deprecated]; messages.push( `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` ); - args[replacement] = args[deprecated]; - delete args[deprecated]; + // This is a complicated type scenario. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (args as any)[replacement] = args[deprecated as keyof CliOptions]; + delete args[deprecated as keyof CliOptions]; } } return messages; } +export function coerceIfBoolean(value: unknown) { + if (typeof value === 'string') { + if (value === 'true') { + return true; + } + if (value === 'false') { + return false; + } + return value; + } + return value; +} + export class UnknownCliArgumentError extends Error { /** The argument that was not parsed. */ readonly argument: string; diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index 020b9217bd..55df5e39f3 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -92,7 +92,8 @@ "pretty-repl": "^4.0.1", "semver": "^7.5.4", "strip-ansi": "^6.0.0", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "glibc-version": "^1.0.0" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", @@ -105,6 +106,7 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", + "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", diff --git a/packages/snippet-manager/package.json b/packages/snippet-manager/package.json index 4107966917..9db7e8faaa 100644 --- a/packages/snippet-manager/package.json +++ b/packages/snippet-manager/package.json @@ -42,8 +42,8 @@ "bson": "^6.10.4", "cross-spawn": "^7.0.5", "escape-string-regexp": "^4.0.0", - "zod": "^3.24.1", - "tar": "^6.1.15" + "tar": "^6.1.15", + "zod": "^3.25.76" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", From 5cf92e8dd4df43a5c14805ef4b4edcf66c0074fd Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 1 Dec 2025 16:27:51 +0100 Subject: [PATCH 07/12] chore: separate mongosh logic, process and validate args separately --- package-lock.json | 1 - packages/arg-parser/.depcheckrc | 2 + packages/arg-parser/src/arg-metadata.ts | 84 ++ packages/arg-parser/src/arg-parser.spec.ts | 727 ++++++++++-------- packages/arg-parser/src/arg-parser.ts | 453 +++-------- packages/arg-parser/src/cli-options.ts | 278 +++++-- packages/arg-parser/tsconfig.json | 1 + packages/cli-repl/package.json | 4 +- packages/cli-repl/src/arg-parser.spec.ts | 40 - packages/cli-repl/src/arg-parser.ts | 31 - .../cli-repl/src/parse-mongosh-args.spec.ts | 64 ++ packages/cli-repl/src/parse-mongosh-args.ts | 54 ++ packages/cli-repl/src/run.ts | 6 +- 13 files changed, 947 insertions(+), 798 deletions(-) create mode 100644 packages/arg-parser/src/arg-metadata.ts delete mode 100644 packages/cli-repl/src/arg-parser.spec.ts delete mode 100644 packages/cli-repl/src/arg-parser.ts create mode 100644 packages/cli-repl/src/parse-mongosh-args.spec.ts create mode 100644 packages/cli-repl/src/parse-mongosh-args.ts diff --git a/package-lock.json b/package-lock.json index d790595413..c40821d91e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37883,7 +37883,6 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", diff --git a/packages/arg-parser/.depcheckrc b/packages/arg-parser/.depcheckrc index 1f8add4c25..dc0bc2df82 100644 --- a/packages/arg-parser/.depcheckrc +++ b/packages/arg-parser/.depcheckrc @@ -10,5 +10,7 @@ ignores: - eslint-config-mongodb-js # needed as a peer dependency of @mongodb-js/devtools-connect - mongodb + # only used in arg-parser export; should be removed once switched to knip + - yargs-parser ignore-patterns: - .eslintrc.js \ No newline at end of file diff --git a/packages/arg-parser/src/arg-metadata.ts b/packages/arg-parser/src/arg-metadata.ts new file mode 100644 index 0000000000..a0caa9f3ad --- /dev/null +++ b/packages/arg-parser/src/arg-metadata.ts @@ -0,0 +1,84 @@ +import z from 'zod/v4'; + +/** + * Registry for argument options metadata + */ +export const argMetadata = z.registry(); + +/** + * Metadata that can be used to define field's parsing behavior + */ +export type ArgumentMetadata = { + /** If set, sets this field as deprecated and replaces this field with the set field. */ + deprecationReplacement?: string; + /** If set, gets replaced with a differet field name (without deprecation) */ + replacement?: string; + /** Whether this argument is unsupported. Always throws an error if set to true. */ + unsupported?: boolean; + /** Aliases for this argument. */ + alias?: string[]; +}; + +/** + * Extract metadata for a field using the custom registry + */ +export function getArgumentMetadata( + schema: z.ZodObject, + fieldName: string +): ArgumentMetadata | undefined { + const fieldSchema = schema.shape[fieldName as keyof typeof schema.shape]; + if (!fieldSchema) { + return undefined; + } + return argMetadata.get(fieldSchema); +} + +/** + * Maps deprecated arguments to their new counterparts, derived from schema metadata. + */ +export function getDeprecatedArgsWithReplacement( + schema: z.ZodObject +): Record, T> { + const deprecated: Record = {}; + for (const fieldName of Object.keys(schema.shape)) { + const meta = getArgumentMetadata(schema, fieldName); + if (meta?.deprecationReplacement) { + deprecated[fieldName] = meta.deprecationReplacement as T; + } + } + return deprecated; +} + +/** + * Get list of unsupported arguments, derived from schema metadata. + */ +export function getUnsupportedArgs(schema: z.ZodObject): string[] { + const unsupported: string[] = []; + for (const fieldName of Object.keys(schema.shape)) { + const meta = getArgumentMetadata(schema, fieldName); + if (meta?.unsupported) { + unsupported.push(fieldName); + } + } + return unsupported; +} + +export class UnknownCliArgumentError extends Error { + /** The argument that was not parsed. */ + readonly argument: string; + constructor(argument: string) { + super(`Unknown argument: ${argument}`); + this.name = 'UnknownCliArgumentError'; + this.argument = argument; + } +} + +export class UnsupportedCliArgumentError extends Error { + /** The argument that was not supported. */ + readonly argument: string; + constructor(argument: string) { + super(`Unsupported argument: ${argument}`); + this.name = 'UnsupportedCliArgumentError'; + this.argument = argument; + } +} diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts index 1923823b7f..25762d4d89 100644 --- a/packages/arg-parser/src/arg-parser.spec.ts +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -2,13 +2,16 @@ import { MongoshUnimplementedError } from '@mongosh/errors'; import { expect } from 'chai'; import stripAnsi from 'strip-ansi'; import { + argMetadata, CliOptionsSchema, coerceIfBoolean, + coerceIfFalse, generateYargsOptionsFromSchema, getLocale, - parseCliArgs, - parseMongoshCliOptionsArgs, + parseArgs, + parseArgsWithCliOptions, UnknownCliArgumentError, + UnsupportedCliArgumentError, } from './arg-parser'; import { z } from 'zod/v4'; @@ -84,14 +87,13 @@ describe('arg-parser', function () { }); describe('.parse', function () { - const baseArgv = ['node', 'mongosh']; context('when providing only a URI', function () { const uri = 'mongodb://domain.com:20000'; - const argv = [...baseArgv, uri]; + const argv = [uri]; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); }); @@ -101,206 +103,206 @@ describe('arg-parser', function () { context('when providing general options', function () { context('when providing --ipv6', function () { - const argv = [...baseArgv, uri, '--ipv6']; + const argv = [uri, '--ipv6']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the ipv6 value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.ipv6).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.ipv6 + ).to.equal(true); }); }); context('when providing -h', function () { - const argv = [...baseArgv, uri, '-h']; + const argv = [uri, '-h']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the help value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.help + ).to.equal(true); }); }); context('when providing --help', function () { - const argv = [...baseArgv, uri, '--help']; + const argv = [uri, '--help']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the help value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.help).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.help + ).to.equal(true); }); }); context('when providing --version', function () { - const argv = [...baseArgv, uri, '--version']; + const argv = [uri, '--version']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the version value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.version).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.version + ).to.equal(true); }); }); context('when providing --verbose', function () { - const argv = [...baseArgv, uri, '--verbose']; + const argv = [uri, '--verbose']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the verbose value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.verbose).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.verbose + ).to.equal(true); }); }); context('when providing --shell', function () { - const argv = [...baseArgv, uri, '--shell']; + const argv = [uri, '--shell']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the shell value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.shell).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.shell + ).to.equal(true); }); }); context('when providing --nodb', function () { - const argv = [...baseArgv, uri, '--nodb']; + const argv = [uri, '--nodb']; it('does not return the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(undefined); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames + parseArgsWithCliOptions({ args: argv }).parsed.fileNames ).to.deep.equal([uri]); }); it('sets the nodb value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.nodb).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.nodb + ).to.equal(true); }); }); context('when providing --norc', function () { - const argv = [...baseArgv, uri, '--norc']; + const argv = [uri, '--norc']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the norc value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.norc).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.norc + ).to.equal(true); }); }); context('when providing --quiet', function () { - const argv = [...baseArgv, uri, '--quiet']; + const argv = [uri, '--quiet']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the quiet value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.quiet).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.quiet + ).to.equal(true); }); }); context('when providing --eval (single value)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1']; + const argv = [uri, '--eval', '1+1']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the eval value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( - ['1+1'] - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.eval + ).to.deep.equal(['1+1']); }); }); context('when providing --eval (multiple values)', function () { - const argv = [...baseArgv, uri, '--eval', '1+1', '--eval', '2+2']; + const argv = [uri, '--eval', '1+1', '--eval', '2+2']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the eval value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.eval).to.deep.equal( - ['1+1', '2+2'] - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.eval + ).to.deep.equal(['1+1', '2+2']); }); }); context('when providing --retryWrites', function () { - const argv = [...baseArgv, uri, '--retryWrites']; + const argv = [uri, '--retryWrites']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the retryWrites value in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.retryWrites + parseArgsWithCliOptions({ args: argv }).parsed.retryWrites ).to.equal(true); }); }); context('when providing an unknown parameter', function () { - const argv = [...baseArgv, uri, '--what']; + const argv = [uri, '--what']; it('raises an error', function () { try { - parseMongoshCliOptionsArgs(argv).options; + parseArgsWithCliOptions({ args: argv }).parsed; } catch (err: any) { if (err instanceof UnknownCliArgumentError) { expect(stripAnsi(err.message)).to.equal( @@ -317,202 +319,177 @@ describe('arg-parser', function () { context('when providing authentication options', function () { context('when providing -u', function () { - const argv = [...baseArgv, uri, '-u', 'richard']; + const argv = [uri, '-u', 'richard']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the username in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( - 'richard' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.username + ).to.equal('richard'); }); }); context('when providing --username', function () { - const argv = [...baseArgv, uri, '--username', 'richard']; + const argv = [uri, '--username', 'richard']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the username in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.username).to.equal( - 'richard' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.username + ).to.equal('richard'); }); }); context('when providing -p', function () { - const argv = [...baseArgv, uri, '-p', 'pw']; + const argv = [uri, '-p', 'pw']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the password in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( - 'pw' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.password + ).to.equal('pw'); }); }); context('when providing --password', function () { - const argv = [...baseArgv, uri, '--password', 'pw']; + const argv = [uri, '--password', 'pw']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the password in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.password).to.equal( - 'pw' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.password + ).to.equal('pw'); }); }); context('when providing --authenticationDatabase', function () { - const argv = [...baseArgv, uri, '--authenticationDatabase', 'db']; + const argv = [uri, '--authenticationDatabase', 'db']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the authenticationDatabase in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.authenticationDatabase + parseArgsWithCliOptions({ args: argv }).parsed + .authenticationDatabase ).to.equal('db'); }); }); context('when providing --authenticationMechanism', function () { - const argv = [ - ...baseArgv, - uri, - '--authenticationMechanism', - 'SCRAM-SHA-256', - ]; + const argv = [uri, '--authenticationMechanism', 'SCRAM-SHA-256']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the authenticationMechanism in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.authenticationMechanism + parseArgsWithCliOptions({ args: argv }).parsed + .authenticationMechanism ).to.equal('SCRAM-SHA-256'); }); }); context('when providing --gssapiServiceName', function () { - const argv = [...baseArgv, uri, '--gssapiServiceName', 'mongosh']; + const argv = [uri, '--gssapiServiceName', 'mongosh']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the gssapiServiceName in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.gssapiServiceName + parseArgsWithCliOptions({ args: argv }).parsed.gssapiServiceName ).to.equal('mongosh'); }); }); context('when providing --gssapiHostName', function () { - const argv = [...baseArgv, uri, '--gssapiHostName', 'example.com']; + const argv = [uri, '--gssapiHostName', 'example.com']; it('throws an error since it is not supported', function () { - try { - parseMongoshCliOptionsArgs(argv).options; - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --gssapiHostName is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); + expect( + () => parseArgsWithCliOptions({ args: argv }).parsed + ).to.throw( + UnsupportedCliArgumentError, + 'Unsupported argument: gssapiHostName' + ); }); - - // it('returns the URI in the object', () => { - // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); - // }); - - // it('sets the gssapiHostName in the object', () => { - // expect(parseMongoshCliOptionsArgs(argv).options.gssapiHostName).to.equal('example.com'); - // }); }); context('when providing --sspiHostnameCanonicalization', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiHostnameCanonicalization', - 'forward', - ]; + const argv = [uri, '--sspiHostnameCanonicalization', 'forward']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the gssapiHostName in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options + parseArgsWithCliOptions({ args: argv }).parsed .sspiHostnameCanonicalization ).to.equal('forward'); }); }); context('when providing --sspiRealmOverride', function () { - const argv = [ - ...baseArgv, - uri, - '--sspiRealmOverride', - 'example2.com', - ]; + const argv = [uri, '--sspiRealmOverride', 'example2.com']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the gssapiHostName in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.sspiRealmOverride + parseArgsWithCliOptions({ args: argv }).parsed.sspiRealmOverride ).to.equal('example2.com'); }); }); context('when providing --awsIamSessionToken', function () { - const argv = [...baseArgv, uri, '--awsIamSessionToken', 'tok']; + const argv = [uri, '--awsIamSessionToken', 'tok']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the awsIamSessionToken in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.awsIamSessionToken + parseArgsWithCliOptions({ args: argv }).parsed.awsIamSessionToken ).to.equal('tok'); }); }); @@ -520,45 +497,50 @@ describe('arg-parser', function () { context('when providing TLS options', function () { context('when providing --tls', function () { - const argv = [...baseArgv, uri, '--tls']; + const argv = [uri, '--tls']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tls in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); + expect(parseArgsWithCliOptions({ args: argv }).parsed.tls).to.equal( + true + ); }); }); context('when providing -tls (single dash)', function () { - const argv = [...baseArgv, uri, '-tls']; + const argv = [uri, '-tls']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tls in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.tls).to.equal(true); + expect(parseArgsWithCliOptions({ args: argv }).parsed.tls).to.equal( + true + ); }); }); context('when providing --tlsCertificateKeyFile', function () { - const argv = [...baseArgv, uri, '--tlsCertificateKeyFile', 'test']; + const argv = [uri, '--tlsCertificateKeyFile', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsCertificateKeyFile in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + parseArgsWithCliOptions({ args: argv }).parsed + .tlsCertificateKeyFile ).to.equal('test'); }); }); @@ -566,167 +548,158 @@ describe('arg-parser', function () { context( 'when providing -tlsCertificateKeyFile (single dash)', function () { - const argv = [...baseArgv, uri, '-tlsCertificateKeyFile', 'test']; + const argv = [uri, '-tlsCertificateKeyFile', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed + .connectionSpecifier ).to.equal(uri); }); it('sets the tlsCertificateKeyFile in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsCertificateKeyFile + parseArgsWithCliOptions({ args: argv }).parsed + .tlsCertificateKeyFile ).to.equal('test'); }); } ); context('when providing --tlsCertificateKeyFilePassword', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsCertificateKeyFilePassword', - 'test', - ]; + const argv = [uri, '--tlsCertificateKeyFilePassword', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsCertificateKeyFilePassword in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options + parseArgsWithCliOptions({ args: argv }).parsed .tlsCertificateKeyFilePassword ).to.equal('test'); }); }); context('when providing --tlsCAFile', function () { - const argv = [...baseArgv, uri, '--tlsCAFile', 'test']; + const argv = [uri, '--tlsCAFile', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsCAFile in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.tlsCAFile).to.equal( - 'test' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.tlsCAFile + ).to.equal('test'); }); }); context('when providing --tlsCRLFile', function () { - const argv = [...baseArgv, uri, '--tlsCRLFile', 'test']; + const argv = [uri, '--tlsCRLFile', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsCRLFile in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsCRLFile + parseArgsWithCliOptions({ args: argv }).parsed.tlsCRLFile ).to.equal('test'); }); }); context('when providing --tlsAllowInvalidHostnames', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidHostnames']; + const argv = [uri, '--tlsAllowInvalidHostnames']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsAllowInvalidHostnames in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsAllowInvalidHostnames + parseArgsWithCliOptions({ args: argv }).parsed + .tlsAllowInvalidHostnames ).to.equal(true); }); }); context('when providing --tlsAllowInvalidCertificates', function () { - const argv = [...baseArgv, uri, '--tlsAllowInvalidCertificates']; + const argv = [uri, '--tlsAllowInvalidCertificates']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsAllowInvalidCertificates in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options + parseArgsWithCliOptions({ args: argv }).parsed .tlsAllowInvalidCertificates ).to.equal(true); }); }); context('when providing --sslFIPSMode', function () { - const argv = [...baseArgv, uri, '--sslFIPSMode']; + const argv = [uri, '--sslFIPSMode']; it('throws an error since it is not supported', function () { - try { - parseMongoshCliOptionsArgs(argv).options; - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - 'Argument --sslFIPSMode is not supported in mongosh' - ); - return; - } - expect.fail('Expected error'); + expect( + () => parseArgsWithCliOptions({ args: argv }).parsed + ).to.throw( + UnsupportedCliArgumentError, + 'Unsupported argument: sslFIPSMode' + ); }); // it('returns the URI in the object', () => { - // expect(parseMongoshCliOptionsArgs(argv).options.connectionSpecifier).to.equal(uri); + // expect(parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier).to.equal(uri); // }); // it('sets the tlsFIPSMode in the object', () => { - // expect(parseMongoshCliOptionsArgs(argv).options.tlsFIPSMode).to.equal(true); + // expect(parseArgsWithCliOptions({ args: argv }).parsed.tlsFIPSMode).to.equal(true); // }); }); context('when providing --tlsCertificateSelector', function () { - const argv = [...baseArgv, uri, '--tlsCertificateSelector', 'test']; + const argv = [uri, '--tlsCertificateSelector', 'test']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsCertificateSelector in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsCertificateSelector + parseArgsWithCliOptions({ args: argv }).parsed + .tlsCertificateSelector ).to.equal('test'); }); }); context('when providing --tlsDisabledProtocols', function () { - const argv = [ - ...baseArgv, - uri, - '--tlsDisabledProtocols', - 'TLS1_0,TLS2_0', - ]; + const argv = [uri, '--tlsDisabledProtocols', 'TLS1_0,TLS2_0']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the tlsDisabledProtocols in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.tlsDisabledProtocols + parseArgsWithCliOptions({ args: argv }).parsed + .tlsDisabledProtocols ).to.equal('TLS1_0,TLS2_0'); }); }); @@ -734,235 +707,229 @@ describe('arg-parser', function () { context('when providing FLE options', function () { context('when providing --awsAccessKeyId', function () { - const argv = [...baseArgv, uri, '--awsAccessKeyId', 'foo']; + const argv = [uri, '--awsAccessKeyId', 'foo']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the awsAccessKeyId in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.awsAccessKeyId + parseArgsWithCliOptions({ args: argv }).parsed.awsAccessKeyId ).to.equal('foo'); }); }); context('when providing --awsSecretAccessKey', function () { - const argv = [...baseArgv, uri, '--awsSecretAccessKey', 'foo']; + const argv = [uri, '--awsSecretAccessKey', 'foo']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the awsSecretAccessKey in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.awsSecretAccessKey + parseArgsWithCliOptions({ args: argv }).parsed.awsSecretAccessKey ).to.equal('foo'); }); }); context('when providing --awsSessionToken', function () { - const argv = [...baseArgv, uri, '--awsSessionToken', 'foo']; + const argv = [uri, '--awsSessionToken', 'foo']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the awsSessionToken in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.awsSessionToken + parseArgsWithCliOptions({ args: argv }).parsed.awsSessionToken ).to.equal('foo'); }); }); context('when providing --keyVaultNamespace', function () { - const argv = [...baseArgv, uri, '--keyVaultNamespace', 'foo.bar']; + const argv = [uri, '--keyVaultNamespace', 'foo.bar']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the keyVaultNamespace in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.keyVaultNamespace + parseArgsWithCliOptions({ args: argv }).parsed.keyVaultNamespace ).to.equal('foo.bar'); }); }); context('when providing --kmsURL', function () { - const argv = [...baseArgv, uri, '--kmsURL', 'example.com']; + const argv = [uri, '--kmsURL', 'example.com']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the kmsURL in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.kmsURL).to.equal( - 'example.com' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.kmsURL + ).to.equal('example.com'); }); }); }); context('when providing versioned API options', function () { context('when providing --apiVersion', function () { - const argv = [...baseArgv, uri, '--apiVersion', '1']; + const argv = [uri, '--apiVersion', '1']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the apiVersion in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.apiVersion + parseArgsWithCliOptions({ args: argv }).parsed.apiVersion ).to.equal('1'); }); }); context('when providing --apiDeprecationErrors', function () { - const argv = [...baseArgv, uri, '--apiDeprecationErrors']; + const argv = [uri, '--apiDeprecationErrors']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the apiVersion in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.apiDeprecationErrors + parseArgsWithCliOptions({ args: argv }).parsed + .apiDeprecationErrors ).to.equal(true); }); }); context('when providing --apiStrict', function () { - const argv = [...baseArgv, uri, '--apiStrict']; + const argv = [uri, '--apiStrict']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the apiVersion in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.apiStrict).to.equal( - true - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.apiStrict + ).to.equal(true); }); }); }); context('when providing filenames after an URI', function () { context('when the filenames end in .js', function () { - const argv = [...baseArgv, uri, 'test1.js', 'test2.js']; + const argv = [uri, 'test1.js', 'test2.js']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.js'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.js'); }); }); context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, uri, 'test1.mongodb', 'test2.mongodb']; + const argv = [uri, 'test1.mongodb', 'test2.mongodb']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.mongodb'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.mongodb'); }); }); context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, uri, 'test1.txt', 'test2.txt']; + const argv = [uri, 'test1.txt', 'test2.txt']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.txt'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.txt'); }); }); context('when filenames are specified using -f', function () { - const argv = [...baseArgv, uri, '-f', 'test1.txt', '-f', 'test2.txt']; + const argv = [uri, '-f', 'test1.txt', '-f', 'test2.txt']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.txt'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.txt'); }); }); context('when filenames are specified using -f/--file', function () { - const argv = [ - ...baseArgv, - uri, - '-f', - 'test1.txt', - '--file', - 'test2.txt', - ]; + const argv = [uri, '-f', 'test1.txt', '--file', 'test2.txt']; it('returns the URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(uri); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.txt'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.txt'); }); }); @@ -970,71 +937,71 @@ describe('arg-parser', function () { context('when providing filenames without an URI', function () { context('when the filenames end in .js', function () { - const argv = [...baseArgv, 'test1.js', 'test2.js']; + const argv = ['test1.js', 'test2.js']; it('returns no URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(undefined); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.js'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.js'); }); }); context('when the filenames end in .mongodb', function () { - const argv = [...baseArgv, 'test1.mongodb', 'test2.mongodb']; + const argv = ['test1.mongodb', 'test2.mongodb']; it('returns no URI in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(undefined); }); it('sets the filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test1.mongodb'); expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[1] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[1] ).to.equal('test2.mongodb'); }); }); context('when the filenames end in other extensions', function () { - const argv = [...baseArgv, 'test1.txt', 'test2.txt']; + const argv = ['test1.txt', 'test2.txt']; it('returns the first filename as an URI', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal('test1.txt'); }); it('uses the remainder as filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test2.txt'); }); }); context('when the first argument is an URI ending in .js', function () { - const argv = [...baseArgv, 'mongodb://domain.foo.js', 'test2.txt']; + const argv = ['mongodb://domain.foo.js', 'test2.txt']; it('returns the first filename as an URI', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal('mongodb://domain.foo.js'); }); it('uses the remainder as filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('test2.txt'); }); }); @@ -1043,7 +1010,6 @@ describe('arg-parser', function () { 'when the first argument is an URI ending in .js but --file is used', function () { const argv = [ - ...baseArgv, '--file', 'mongodb://domain.foo.js', 'mongodb://domain.bar.js', @@ -1051,13 +1017,14 @@ describe('arg-parser', function () { it('returns the first filename as an URI', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed + .connectionSpecifier ).to.equal('mongodb://domain.bar.js'); }); it('uses the remainder as filenames', function () { expect( - parseMongoshCliOptionsArgs(argv).options.fileNames?.[0] + parseArgsWithCliOptions({ args: argv }).parsed.fileNames?.[0] ).to.equal('mongodb://domain.foo.js'); }); } @@ -1069,22 +1036,22 @@ describe('arg-parser', function () { context('when providing a DB address', function () { context('when only a db name is provided', function () { const db = 'foo'; - const argv = [...baseArgv, db]; + const argv = [db]; it('sets the db in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(db); }); }); context('when a db address is provided without a scheme', function () { const db = '192.168.0.5:9999/foo'; - const argv = [...baseArgv, db]; + const argv = [db]; it('sets the db in the object', function () { expect( - parseMongoshCliOptionsArgs(argv).options.connectionSpecifier + parseArgsWithCliOptions({ args: argv }).parsed.connectionSpecifier ).to.equal(db); }); }); @@ -1092,22 +1059,22 @@ describe('arg-parser', function () { context('when providing no DB address', function () { context('when providing a host', function () { - const argv = [...baseArgv, '--host', 'example.com']; + const argv = ['--host', 'example.com']; it('sets the host value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.host).to.equal( - 'example.com' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.host + ).to.equal('example.com'); }); }); context('when providing a port', function () { - const argv = [...baseArgv, '--port', '20000']; + const argv = ['--port', '20000']; it('sets the port value in the object', function () { - expect(parseMongoshCliOptionsArgs(argv).options.port).to.equal( - '20000' - ); + expect( + parseArgsWithCliOptions({ args: argv }).parsed.port + ).to.equal('20000'); }); }); }); @@ -1153,12 +1120,12 @@ describe('arg-parser', function () { }, ] as const) { it(`replaces --${deprecated} with --${replacement}`, function () { - const argv = [...baseArgv, `--${deprecated}`]; + const argv = [`--${deprecated}`]; if (value) { argv.push(value); } - const args = parseMongoshCliOptionsArgs(argv).options; + const args = parseArgsWithCliOptions({ args: argv }).parsed; expect(args).to.not.have.property(deprecated); expect(args[replacement]).to.equal(value ?? true); }); @@ -1167,32 +1134,44 @@ describe('arg-parser', function () { }); describe('union type fields', function () { - for (const { argument, values, strict } of [ + for (const { argument, values, onlyFalse, strict } of [ { argument: 'json', values: ['relaxed', 'canonical'] }, { argument: 'oidcDumpTokens', values: ['redacted', 'include-secrets'] }, - { argument: 'browser', values: ['test'], strict: false }, + { argument: 'browser', values: ['test'], onlyFalse: true, strict: false }, ] as const) { - const baseArgv = ['node', 'mongosh', 'mongodb://domain.com:20000']; describe(`with ${argument}`, function () { context('with boolean', function () { it(`get set to true with --${argument}`, function () { expect( - parseMongoshCliOptionsArgs([...baseArgv, `--${argument}`]) - .options[argument] + parseArgsWithCliOptions({ + args: [`--${argument}`], + }).parsed[argument] ).to.equal(true); }); - it(`sets to true with --${argument}=true`, function () { - expect( - parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=true`]) - .options[argument] - ).to.equal(true); - }); + if (!onlyFalse) { + it(`coerces to true with --${argument}=true`, function () { + expect( + parseArgsWithCliOptions({ + args: [`--${argument}=true`], + }).parsed[argument] + ).to.equal(true); + }); + } else { + it(`does not coerce with "--${argument} true"`, function () { + expect( + parseArgsWithCliOptions({ + args: [`--${argument}=true`], + }).parsed[argument] + ).to.be.equal('true'); + }); + } - it(`sets to false with --${argument}=false`, function () { + it(`coerces to false with --${argument}=false`, function () { expect( - parseMongoshCliOptionsArgs([...baseArgv, `--${argument}=false`]) - .options[argument] + parseArgsWithCliOptions({ + args: [`--${argument}=false`], + }).parsed[argument] ).to.equal(false); }); }); @@ -1202,19 +1181,17 @@ describe('arg-parser', function () { // This matches the legacy behavior pre-Zod schema migration. it(`does not work with "--${argument} ${value}"`, function () { expect( - parseMongoshCliOptionsArgs([ - ...baseArgv, - `--${argument} ${value}`, - ]).options[argument] + parseArgsWithCliOptions({ + args: [`--${argument} ${value}`], + }).parsed[argument] ).to.be.undefined; }); it(`works "--${argument}=${value}"`, function () { expect( - parseMongoshCliOptionsArgs([ - ...baseArgv, - `--${argument}=${value}`, - ]).options[argument] + parseArgsWithCliOptions({ + args: [`--${argument}=${value}`], + }).parsed[argument] ).to.equal(value); }); }); @@ -1223,11 +1200,9 @@ describe('arg-parser', function () { if (strict) { it('throws an error with invalid value', function () { try { - parseMongoshCliOptionsArgs([ - ...baseArgv, - `--${argument}`, - 'invalid', - ]); + parseArgsWithCliOptions({ + args: [`--${argument}`, 'invalid'], + }); } catch (e: any) { expect(e).to.be.instanceOf(MongoshUnimplementedError); expect(e.message).to.include( @@ -1292,8 +1267,8 @@ describe('arg-parser', function () { 'gssapiServiceName', 'sspiHostnameCanonicalization', 'sspiRealmOverride', - 'jsContext', 'host', + 'jsContext', 'keyVaultNamespace', 'kmsURL', 'locale', @@ -1349,7 +1324,7 @@ describe('arg-parser', function () { coerce: { json: coerceIfBoolean, oidcDumpTokens: coerceIfBoolean, - browser: coerceIfBoolean, + browser: coerceIfFalse, }, alias: { h: 'help', @@ -1378,43 +1353,123 @@ describe('arg-parser', function () { // Compare non-array properties normally expect(options.alias).to.deep.equal(expected.alias); expect(options.configuration).to.deep.equal(expected.configuration); + expect(options.coerce).to.deep.equal(expected.coerce); }); }); - describe('parseCliArgs', function () { - it('parses the expected options for Cli Options', function () { - const options = parseCliArgs({ - args: ['--port', '20000', '--ssl', '1', '--unknownField', '1'], - schema: CliOptionsSchema, + describe('parseArgs', function () { + it('passes any schema, independent of CliOptionsSchema', function () { + const options = parseArgs({ + args: [ + 'hello', + '--port', + '20000', + '--ssl', + '1', + '--unknownField', + '1', + '--deprecatedField', + '100', + ], + schema: z.object({ + port: z.number(), + ssl: z.boolean(), + unknownField: z.string(), + replacedField: z.number(), + deprecatedField: z.number().register(argMetadata, { + deprecationReplacement: 'replacedField', + }), + }), }); expect(options).to.deep.equal({ - _: ['1', '--unknownField', '1'], - port: '20000', - ssl: true, + positional: ['hello', '1'], + parsed: { + port: 20000, + replacedField: 100, + ssl: true, + unknownField: '1', + }, + deprecated: { + deprecatedField: 'replacedField', + }, }); }); }); - it('parses extended schema', function () { - const options = parseCliArgs({ - args: [ - '--port', - '20000', - '--extendedField', - '90', - '--unknownField', - '100', - ], - schema: CliOptionsSchema.extend({ - extendedField: z.number(), - }), + describe('parseArgsWithCliOptions', function () { + it('parses the expected options for Cli Options and replacements', function () { + const options = parseArgsWithCliOptions({ + args: ['--port', '20000', '--ssl', '1'], + }); + + expect(options).to.deep.equal({ + positional: [], + parsed: { + connectionSpecifier: '1', + fileNames: [], + port: '20000', + tls: true, + }, + deprecated: { + ssl: 'tls', + }, + }); + }); + + it('parses extended schema', function () { + const options = parseArgsWithCliOptions({ + args: [ + '--port', + '20000', + '--extendedField', + '90', + '--ssl', + 'true', + '--deprecatedField', + '100', + ], + schema: { + extendedField: z.number(), + replacedField: z.number(), + deprecatedField: z.number().register(argMetadata, { + deprecationReplacement: 'replacedField', + }), + }, + }); + + expect(options).to.deep.equal({ + positional: [], + parsed: { + port: '20000', + replacedField: 100, + extendedField: 90, + tls: true, + fileNames: [], + }, + deprecated: { + ssl: 'tls', + deprecatedField: 'replacedField', + }, + }); }); - expect(options).to.deep.equal({ - _: ['--unknownField', '100'], - port: '20000', - extendedField: 90, + it('throws an error for fields outside of the custom schema', function () { + expect(() => + parseArgsWithCliOptions({ + args: [ + '--port', + '20000', + '--extendedField', + '90', + '--unknownField', + '100', + ], + schema: { + extendedField: z.enum(['90', '100']), + }, + }) + ).to.throw(UnknownCliArgumentError, 'Unknown argument: --unknownField'); }); }); }); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index 5d1f237361..b455e3d1a4 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -1,199 +1,26 @@ -import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; -import i18n from '@mongosh/i18n'; import type { CliOptions } from '@mongosh/arg-parser'; import parser from 'yargs-parser'; import { z } from 'zod/v4'; import type { Options as YargsOptions } from 'yargs-parser'; - -/** - * Custom registry for CLI options metadata - */ -export const cliOptionsRegistry = z.registry(); - -/** - * CLI options schema with metadata attached via registry - */ -export const CliOptionsSchema = z - .object({ - // String options - apiVersion: z.string().optional(), - authenticationDatabase: z.string().optional(), - authenticationMechanism: z.string().optional(), - awsAccessKeyId: z.string().optional(), - awsIamSessionToken: z.string().optional(), - awsSecretAccessKey: z.string().optional(), - awsSessionToken: z.string().optional(), - csfleLibraryPath: z.string().optional(), - cryptSharedLibPath: z.string().optional(), - db: z.string().optional(), - gssapiHostName: z - .string() - .optional() - .register(cliOptionsRegistry, { unsupported: true }), - gssapiServiceName: z.string().optional(), - sspiHostnameCanonicalization: z.string().optional(), - sspiRealmOverride: z.string().optional(), - jsContext: z.string().optional(), - host: z.string().optional(), - keyVaultNamespace: z.string().optional(), - kmsURL: z.string().optional(), - locale: z.string().optional(), - oidcFlows: z.string().optional(), - oidcRedirectUri: z - .string() - .optional() - .register(cliOptionsRegistry, { - alias: ['oidcRedirectUrl'], - }), - password: z - .string() - .optional() - .register(cliOptionsRegistry, { alias: ['p'] }), - port: z.string().optional(), - username: z - .string() - .optional() - .register(cliOptionsRegistry, { alias: ['u'] }), - - // Deprecated SSL options (now TLS) - sslPEMKeyFile: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsCertificateKeyFile', - }), - sslPEMKeyPassword: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsCertificateKeyFilePassword', - }), - sslCAFile: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsCAFile', - }), - sslCertificateSelector: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsCertificateSelector', - }), - sslCRLFile: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsCRLFile', - }), - sslDisabledProtocols: z.string().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsDisabledProtocols', - }), - - // TLS options - tlsCAFile: z.string().optional(), - tlsCertificateKeyFile: z.string().optional(), - tlsCertificateKeyFilePassword: z.string().optional(), - tlsCertificateSelector: z.string().optional(), - tlsCRLFile: z.string().optional(), - tlsDisabledProtocols: z.string().optional(), - - // Boolean options - apiDeprecationErrors: z.boolean().optional(), - apiStrict: z.boolean().optional(), - buildInfo: z - .boolean() - .optional() - .register(cliOptionsRegistry, { alias: ['build-info'] }), - exposeAsyncRewriter: z.boolean().optional(), - help: z - .boolean() - .optional() - .register(cliOptionsRegistry, { alias: ['h'] }), - ipv6: z.boolean().optional(), - nodb: z.boolean().optional(), - norc: z.boolean().optional(), - oidcTrustedEndpoint: z.boolean().optional(), - oidcIdTokenAsAccessToken: z - .boolean() - .optional() - .register(cliOptionsRegistry, { - alias: ['oidcIDTokenAsAccessToken'], - }), - oidcNoNonce: z.boolean().optional(), - perfTests: z.boolean().optional(), - quiet: z.boolean().optional(), - retryWrites: z.boolean().optional(), - shell: z.boolean().optional(), - smokeTests: z.boolean().optional(), - skipStartupWarnings: z.boolean().optional(), - verbose: z.boolean().optional(), - version: z.boolean().optional(), - - // Deprecated SSL boolean options - ssl: z - .boolean() - .optional() - .register(cliOptionsRegistry, { deprecationReplacement: 'tls' }), - sslAllowInvalidCertificates: z - .boolean() - .optional() - .register(cliOptionsRegistry, { - deprecationReplacement: 'tlsAllowInvalidCertificates', - }), - sslAllowInvalidHostnames: z - .boolean() - .optional() - .register(cliOptionsRegistry, { - deprecationReplacement: 'tlsAllowInvalidHostnames', - }), - sslFIPSMode: z.boolean().optional().register(cliOptionsRegistry, { - deprecationReplacement: 'tlsFIPSMode', - unsupported: true, - }), - - // TLS boolean options - tls: z.boolean().optional(), - tlsAllowInvalidCertificates: z.boolean().optional(), - tlsAllowInvalidHostnames: z.boolean().optional(), - tlsFIPSMode: z.boolean().optional(), - tlsUseSystemCA: z.boolean().optional(), - - // Array options - eval: z.array(z.string()).optional(), - file: z - .array(z.string()) - .optional() - .register(cliOptionsRegistry, { alias: ['f'] }), - - // Options that can be boolean or string - json: z.union([z.boolean(), z.enum(['relaxed', 'canonical'])]).optional(), - oidcDumpTokens: z - .union([z.boolean(), z.enum(['redacted', 'include-secrets'])]) - .optional(), - browser: z.union([z.boolean(), z.string()]).optional(), - }) - .loose(); - -/** - * Metadata that can be used to define the yargs-parser configuration for a field. - */ -export type YargsOptionsMetadata = { - alias?: string[]; -}; - -/** - * Type for option metadata - */ -export type CliOptionsRegistryMetadata = YargsOptionsMetadata & { - deprecationReplacement?: keyof CliOptions; - unsupported?: boolean; -}; - -/** - * Extract metadata for a field using the custom registry - */ -const getCliOptionsMetadata = ( - fieldName: string -): CliOptionsRegistryMetadata | undefined => { - const fieldSchema = - CliOptionsSchema.shape[fieldName as keyof typeof CliOptionsSchema.shape]; - if (!fieldSchema) { - return undefined; - } - return cliOptionsRegistry.get(fieldSchema); -}; +import { + CliOptionsSchema, + processPositionalCliOptions, + validateCliOptions, +} from './cli-options'; +import { + argMetadata, + getArgumentMetadata, + getDeprecatedArgsWithReplacement, + getUnsupportedArgs, + UnknownCliArgumentError, + UnsupportedCliArgumentError, +} from './arg-metadata'; /** * Generate yargs-parser configuration from schema */ export function generateYargsOptionsFromSchema({ - schema = CliOptionsSchema, + schema, configuration = { 'camel-case-expansion': false, 'unknown-options-as-args': true, @@ -203,7 +30,7 @@ export function generateYargsOptionsFromSchema({ 'short-option-groups': false, }, }: { - schema?: z.ZodObject; + schema: z.ZodObject; configuration?: YargsOptions['configuration']; }): YargsOptions { const options = { @@ -221,7 +48,7 @@ export function generateYargsOptionsFromSchema({ >; for (const [fieldName, fieldSchema] of Object.entries(schema.shape)) { - const meta = getCliOptionsMetadata(fieldName); + const meta = getArgumentMetadata(schema, fieldName); // Unwrap optional type let unwrappedType = fieldSchema; @@ -240,25 +67,44 @@ export function generateYargsOptionsFromSchema({ options.number.push(fieldName); } else if (unwrappedType instanceof z.ZodUnion) { // Handle union types (like json, browser, oidcDumpTokens) - // Check if the union includes boolean const unionOptions = ( unwrappedType as z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]> ).options; - const hasBoolean = unionOptions.some( - (opt) => opt instanceof z.ZodBoolean - ); + const hasString = unionOptions.some( (opt) => opt instanceof z.ZodString || opt instanceof z.ZodEnum ); - if (hasString && !hasBoolean) { - options.string.push(fieldName); + if (hasString) { + const hasFalseLiteral = unionOptions.some( + (opt) => opt instanceof z.ZodLiteral && opt.value === false + ); + const hasBoolean = unionOptions.some( + (opt) => opt instanceof z.ZodBoolean + ); + if (hasFalseLiteral) { + // If set to 'false' coerce into false boolean; string in all other cases + options.coerce[fieldName] = coerceIfFalse; + } else if (hasBoolean) { + // If the field is 'true' or 'false', we coerce the value to a boolean. + options.coerce[fieldName] = coerceIfBoolean; + } else { + options.string.push(fieldName); + } } - - if (hasBoolean && hasString) { - // When a field has both boolean and string, we add a coerce function to the field. - // This allows to get a value in both -- and --= for boolean and string. - options.coerce[fieldName] = coerceIfBoolean; + } else if (unwrappedType instanceof z.ZodEnum) { + if ( + unwrappedType.options.every((opt: unknown) => typeof opt === 'string') + ) { + options.string.push(fieldName); + } else if ( + unwrappedType.options.every((opt: unknown) => typeof opt === 'number') + ) { + options.number.push(fieldName); + } else { + throw new Error( + `${fieldName} has unsupported enum options. Currently, only string and number enum options are supported.` + ); } } else { throw new Error(`Unknown field type: ${unwrappedType.constructor.name}`); @@ -278,37 +124,6 @@ export function generateYargsOptionsFromSchema({ }; } -/** - * Maps deprecated arguments to their new counterparts, derived from schema metadata. - */ -function getDeprecatedArgsWithReplacement(): Record< - keyof z.infer, - keyof CliOptions -> { - const deprecated: Record = {}; - for (const fieldName of Object.keys(CliOptionsSchema.shape)) { - const meta = getCliOptionsMetadata(fieldName); - if (meta?.deprecationReplacement) { - deprecated[fieldName] = meta.deprecationReplacement; - } - } - return deprecated; -} - -/** - * Get list of unsupported arguments, derived from schema metadata. - */ -function getUnsupportedArgs(schema: z.ZodObject): string[] { - const unsupported: string[] = []; - for (const fieldName of Object.keys(schema.shape)) { - const meta = getCliOptionsMetadata(fieldName); - if (meta?.unsupported) { - unsupported.push(fieldName); - } - } - return unsupported; -} - /** * Determine the locale of the shell. * @@ -325,130 +140,106 @@ export function getLocale(args: string[], env: any): string { return lang ? lang.split('.')[0] : lang; } -function isConnectionSpecifier(arg?: string): boolean { - return ( - typeof arg === 'string' && - (arg.startsWith('mongodb://') || - arg.startsWith('mongodb+srv://') || - !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) - ); -} - -export function parseCliArgs({ +export function parseArgs({ args, schema, parserConfiguration, }: { args: string[]; - schema?: z.ZodObject; + schema: z.ZodObject; parserConfiguration?: YargsOptions['configuration']; -}): T & parser.Arguments { +}): { + /** Parsed options from the schema, including replaced deprecated arguments. */ + parsed: T & Omit; + /** Record of used deprecated arguments which have been replaced. */ + deprecated: Record, T>; + /** Positional arguments which were not parsed as options. */ + positional: parser.Arguments['_']; +} { const options = generateYargsOptionsFromSchema({ schema, configuration: parserConfiguration, }); - return parser(args, options) as unknown as T & parser.Arguments; -} + const { _: positional, ...parsedArgs } = parser(args, options); -/** - * Parses mongosh-specific arguments into a JS object. - * - * @param args - The CLI arguments. - * - * @returns The arguments as cli options. - */ -export function parseMongoshCliOptionsArgs(args: string[]): { - options: CliOptions; - warnings: string[]; -} { - const programArgs = args.slice(2); - i18n.setLocale(getLocale(programArgs, process.env)); + const allDeprecatedArgs = getDeprecatedArgsWithReplacement(schema); + const usedDeprecatedArgs = {} as Record, T>; + + for (const deprecated of Object.keys(allDeprecatedArgs)) { + if (deprecated in parsedArgs) { + const replacement = allDeprecatedArgs[deprecated]; + + // This is a complicated type scenario. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (parsedArgs as any)[replacement] = + parsedArgs[deprecated as keyof typeof parsedArgs]; + usedDeprecatedArgs[deprecated] = replacement; - const parsed = parseCliArgs< - CliOptions & { - smokeTests: boolean; - perfTests: boolean; - buildInfo: boolean; - file?: string[]; + delete parsedArgs[deprecated as keyof typeof parsedArgs]; } - >({ args: programArgs, schema: CliOptionsSchema }); + } - const positionalArguments = parsed._ ?? []; - for (const arg of positionalArguments) { + for (const arg of positional) { if (typeof arg === 'string' && arg.startsWith('-')) { throw new UnknownCliArgumentError(arg); } } - if (typeof positionalArguments[0] === 'string') { - if (!parsed.nodb && isConnectionSpecifier(positionalArguments[0])) { - parsed.connectionSpecifier = positionalArguments.shift() as string; + const unsupportedArgs = getUnsupportedArgs(schema); + for (const unsupported of unsupportedArgs) { + if (unsupported in parsedArgs) { + throw new UnsupportedCliArgumentError(unsupported); } } - // Remove the _ property from the parsed object - const { _: _exclude, ...parsedCliOptions } = parsed; - return { - options: { - ...parsedCliOptions, - fileNames: [ - ...(parsedCliOptions.file ?? []), - ...(positionalArguments as string[]), - ], - }, - warnings: verifyCliArguments(parsed), + parsed: parsedArgs as T & Omit, + deprecated: usedDeprecatedArgs, + positional, }; } -function verifyCliArguments(args: CliOptions): string[] { - const unsupportedArgs = getUnsupportedArgs(CliOptionsSchema); - for (const unsupported of unsupportedArgs) { - if (unsupported in args) { - throw new MongoshUnimplementedError( - `Argument --${unsupported} is not supported in mongosh`, - CommonErrors.InvalidArgument - ); - } - } +type ParsedCliOptions = CliOptions & { + smokeTests: boolean; + perfTests: boolean; + buildInfo: boolean; + file?: string[]; +}; - const jsonValidation = CliOptionsSchema.shape.json.safeParse(args.json); - if (!jsonValidation.success) { - throw new MongoshUnimplementedError( - '--json can only have the values relaxed or canonical', - CommonErrors.InvalidArgument - ); - } +/** Parses the arguments with special handling of mongosh CLI options fields. */ +export function parseArgsWithCliOptions({ + args, + schema: schemaToExtend, +}: { + args: string[]; + /** Schema to extend the CLI options schema with. */ + schema?: Record; +}): ReturnType> { + const schema = + schemaToExtend !== undefined + ? CliOptionsSchema.extend(schemaToExtend) + : CliOptionsSchema; + const { parsed, positional, deprecated } = parseArgs({ + args, + schema, + }); - const oidcDumpTokensValidation = - CliOptionsSchema.shape.oidcDumpTokens.safeParse(args.oidcDumpTokens); - if (!oidcDumpTokensValidation.success) { - throw new MongoshUnimplementedError( - '--oidcDumpTokens can only have the values redacted or include-secrets', - CommonErrors.InvalidArgument - ); - } + const processed = processPositionalCliOptions({ + parsed, + positional, + }); - const messages = []; - const deprecatedArgs = getDeprecatedArgsWithReplacement(); - for (const deprecated of Object.keys(deprecatedArgs)) { - if (deprecated in args) { - const replacement = deprecatedArgs[deprecated]; - messages.push( - `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` - ); + validateCliOptions(processed); - // This is a complicated type scenario. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (args as any)[replacement] = args[deprecated as keyof CliOptions]; - delete args[deprecated as keyof CliOptions]; - } - } - return messages; + return { + parsed: processed, + positional, + deprecated, + }; } -export function coerceIfBoolean(value: unknown) { +export function coerceIfBoolean(value: unknown): unknown { if (typeof value === 'string') { if (value === 'true') { return true; @@ -461,12 +252,16 @@ export function coerceIfBoolean(value: unknown) { return value; } -export class UnknownCliArgumentError extends Error { - /** The argument that was not parsed. */ - readonly argument: string; - constructor(argument: string) { - super(`Unknown argument: ${argument}`); - this.name = 'UnknownCliArgumentError'; - this.argument = argument; +export function coerceIfFalse(value: unknown): unknown { + if (typeof value === 'string') { + if (value === 'false') { + return false; + } + return value; } + return value; } + +export { argMetadata, UnknownCliArgumentError, UnsupportedCliArgumentError }; +export { type ArgumentMetadata } from './arg-metadata'; +export { type CliOptions, CliOptionsSchema } from './cli-options'; diff --git a/packages/arg-parser/src/cli-options.ts b/packages/arg-parser/src/cli-options.ts index 8238cb504e..69812f337c 100644 --- a/packages/arg-parser/src/cli-options.ts +++ b/packages/arg-parser/src/cli-options.ts @@ -1,62 +1,230 @@ +import z from 'zod/v4'; +import { argMetadata } from './arg-metadata'; +import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; +import type parser from 'yargs-parser'; + +export const CurrentCliOptionsSchema = z.object({ + // String options + apiVersion: z.string().optional(), + authenticationDatabase: z.string().optional(), + authenticationMechanism: z.string().optional(), + awsAccessKeyId: z.string().optional(), + awsIamSessionToken: z.string().optional(), + awsSecretAccessKey: z.string().optional(), + awsSessionToken: z.string().optional(), + csfleLibraryPath: z.string().optional(), + cryptSharedLibPath: z.string().optional(), + db: z.string().optional(), + gssapiServiceName: z.string().optional(), + sspiHostnameCanonicalization: z.string().optional(), + sspiRealmOverride: z.string().optional(), + jsContext: z.enum(['repl', 'plain-vm', 'auto']).optional(), + host: z.string().optional(), + keyVaultNamespace: z.string().optional(), + kmsURL: z.string().optional(), + locale: z.string().optional(), + oidcFlows: z.string().optional(), + oidcRedirectUri: z + .string() + .optional() + .register(argMetadata, { + alias: ['oidcRedirectUrl'], + }), + password: z + .string() + .optional() + .register(argMetadata, { alias: ['p'] }), + port: z.string().optional(), + username: z + .string() + .optional() + .register(argMetadata, { alias: ['u'] }), + + // TLS options + tlsCAFile: z.string().optional(), + tlsCertificateKeyFile: z.string().optional(), + tlsCertificateKeyFilePassword: z.string().optional(), + tlsCertificateSelector: z.string().optional(), + tlsCRLFile: z.string().optional(), + tlsDisabledProtocols: z.string().optional(), + + // Boolean options + apiDeprecationErrors: z.boolean().optional(), + apiStrict: z.boolean().optional(), + buildInfo: z + .boolean() + .optional() + .register(argMetadata, { alias: ['build-info'] }), + exposeAsyncRewriter: z.boolean().optional(), + help: z + .boolean() + .optional() + .register(argMetadata, { alias: ['h'] }), + ipv6: z.boolean().optional(), + nodb: z.boolean().optional(), + norc: z.boolean().optional(), + oidcTrustedEndpoint: z.boolean().optional(), + oidcIdTokenAsAccessToken: z + .boolean() + .optional() + .register(argMetadata, { + alias: ['oidcIDTokenAsAccessToken'], + }), + oidcNoNonce: z.boolean().optional(), + perfTests: z.boolean().optional(), + quiet: z.boolean().optional(), + retryWrites: z.boolean().optional(), + shell: z.boolean().optional(), + smokeTests: z.boolean().optional(), + skipStartupWarnings: z.boolean().optional(), + verbose: z.boolean().optional(), + version: z.boolean().optional(), + + // TLS boolean options + tls: z.boolean().optional(), + tlsAllowInvalidCertificates: z.boolean().optional(), + tlsAllowInvalidHostnames: z.boolean().optional(), + tlsFIPSMode: z.boolean().optional(), + tlsUseSystemCA: z.boolean().optional(), + + // Array options + eval: z.array(z.string()).optional(), + file: z + .array(z.string()) + .optional() + .register(argMetadata, { alias: ['f'] }), + + // Options that can be boolean or string + json: z.union([z.boolean(), z.enum(['relaxed', 'canonical'])]).optional(), + oidcDumpTokens: z + .union([z.boolean(), z.enum(['redacted', 'include-secrets'])]) + .optional(), + browser: z.union([z.literal(false), z.string()]).optional(), +}); + +export const DeprecatedCliOptions = z.object({ + ssl: z + .boolean() + .optional() + .register(argMetadata, { deprecationReplacement: 'tls' }), + sslAllowInvalidCertificates: z.boolean().optional().register(argMetadata, { + deprecationReplacement: 'tlsAllowInvalidCertificates', + }), + sslAllowInvalidHostnames: z.boolean().optional().register(argMetadata, { + deprecationReplacement: 'tlsAllowInvalidHostnames', + }), + sslPEMKeyFile: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsCertificateKeyFile', + }), + sslPEMKeyPassword: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsCertificateKeyFilePassword', + }), + sslCAFile: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsCAFile', + }), + sslCertificateSelector: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsCertificateSelector', + }), + sslCRLFile: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsCRLFile', + }), + sslDisabledProtocols: z.string().optional().register(argMetadata, { + deprecationReplacement: 'tlsDisabledProtocols', + }), +}); + +export const UnsupportedCliOptions = z.object({ + gssapiHostName: z + .string() + .optional() + .register(argMetadata, { unsupported: true }), + sslFIPSMode: z.boolean().optional().register(argMetadata, { + unsupported: true, + }), +}); + +export const CliOptionsSchema = z.object({ + ...CurrentCliOptionsSchema.shape, + ...DeprecatedCliOptions.shape, + ...UnsupportedCliOptions.shape, +}); + +type ExcludedFields = + | keyof typeof DeprecatedCliOptions + | keyof typeof UnsupportedCliOptions; + /** * Valid options that can be parsed from the command line. */ -export interface CliOptions { - // Positional arguments: +export type CliOptions = Omit< + z.infer, + ExcludedFields +> & { + // Positional arguments connectionSpecifier?: string; fileNames?: string[]; +}; + +export function processPositionalCliOptions({ + parsed, + positional, +}: { + parsed: CliOptions; + positional: parser.Arguments['_']; +}): CliOptions { + const processed = { ...parsed }; + if (typeof positional[0] === 'string') { + if (!processed.nodb && isConnectionSpecifier(positional[0])) { + processed.connectionSpecifier = positional.shift() as string; + } + } + processed.fileNames = [ + ...(processed.file ?? []), + ...(positional as string[]), + ]; + + return processed; +} + +/** + * Validates the CLI options. + * TODO: Use proper schema validation for all fields. + * For now, to minimize impact of adopting Zod, this only validates the enum values. + */ +export function validateCliOptions(parsed: CliOptions): void { + const jsonValidation = CliOptionsSchema.shape.json.safeParse(parsed.json); + if (!jsonValidation.success) { + throw new MongoshUnimplementedError( + '--json can only have the values relaxed or canonical', + CommonErrors.InvalidArgument + ); + } + + const oidcDumpTokensValidation = + CliOptionsSchema.shape.oidcDumpTokens.safeParse(parsed.oidcDumpTokens); + if (!oidcDumpTokensValidation.success) { + throw new MongoshUnimplementedError( + '--oidcDumpTokens can only have the values redacted or include-secrets', + CommonErrors.InvalidArgument + ); + } + + const jsContextValidation = CliOptionsSchema.shape.jsContext.safeParse( + parsed.jsContext + ); + if (!jsContextValidation.success) { + throw new MongoshUnimplementedError( + '--jsContext can only have the values repl, plain-vm, or auto', + CommonErrors.InvalidArgument + ); + } +} - // Non-positional arguments: - apiDeprecationErrors?: boolean; - apiStrict?: boolean; - apiVersion?: string; - authenticationDatabase?: string; - authenticationMechanism?: string; - awsAccessKeyId?: string; - awsIamSessionToken?: string; - awsSecretAccessKey?: string; - awsSessionToken?: string; - csfleLibraryPath?: string; - cryptSharedLibPath?: string; - db?: string; - eval?: string[]; - exposeAsyncRewriter?: boolean; // internal testing only - gssapiServiceName?: string; - sspiHostnameCanonicalization?: string; - sspiRealmOverride?: string; - help?: boolean; - host?: string; - ipv6?: boolean; - jsContext?: 'repl' | 'plain-vm' | 'auto'; - json?: boolean | 'canonical' | 'relaxed'; - keyVaultNamespace?: string; - kmsURL?: string; - nodb?: boolean; - norc?: boolean; - password?: string; - port?: string; - quiet?: boolean; - retryWrites?: boolean; - shell?: boolean; - skipStartupWarnings?: boolean; - tls?: boolean; - tlsAllowInvalidCertificates?: boolean; - tlsAllowInvalidHostnames?: boolean; - tlsCAFile?: string; - tlsCertificateKeyFile?: string; - tlsCertificateKeyFilePassword?: string; - tlsCertificateSelector?: string; - tlsCRLFile?: string; - tlsDisabledProtocols?: boolean; - tlsFIPSMode?: boolean; - username?: string; - verbose?: boolean; // No-op since driver v5.0.0 (see also MONGOSH-970) - version?: boolean; - oidcFlows?: string; - oidcRedirectUri?: string; - oidcTrustedEndpoint?: boolean; - oidcIdTokenAsAccessToken?: boolean; - oidcDumpTokens?: boolean | 'redacted' | 'include-secrets'; - oidcNoNonce?: boolean; - browser?: string | false; +function isConnectionSpecifier(arg?: string): boolean { + return ( + typeof arg === 'string' && + (arg.startsWith('mongodb://') || + arg.startsWith('mongodb+srv://') || + !(arg.endsWith('.js') || arg.endsWith('.mongodb'))) + ); } diff --git a/packages/arg-parser/tsconfig.json b/packages/arg-parser/tsconfig.json index 51ef57e2cf..467155d4f5 100644 --- a/packages/arg-parser/tsconfig.json +++ b/packages/arg-parser/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@mongodb-js/tsconfig-mongosh/tsconfig.common.json", "compilerOptions": { "outDir": "./lib", + "rootDir": "./src", "allowJs": true }, "include": ["src/**/*"], diff --git a/packages/cli-repl/package.json b/packages/cli-repl/package.json index 55df5e39f3..020b9217bd 100644 --- a/packages/cli-repl/package.json +++ b/packages/cli-repl/package.json @@ -92,8 +92,7 @@ "pretty-repl": "^4.0.1", "semver": "^7.5.4", "strip-ansi": "^6.0.0", - "text-table": "^0.2.0", - "glibc-version": "^1.0.0" + "text-table": "^0.2.0" }, "devDependencies": { "@mongodb-js/eslint-config-mongosh": "^1.0.0", @@ -106,7 +105,6 @@ "@types/node": "^22.15.30", "@types/numeral": "^2.0.2", "@types/text-table": "^0.2.1", - "@types/yargs-parser": "^21.0.3", "chai-as-promised": "^8.0.2", "depcheck": "^1.4.7", "eslint": "^7.25.0", diff --git a/packages/cli-repl/src/arg-parser.spec.ts b/packages/cli-repl/src/arg-parser.spec.ts deleted file mode 100644 index 916dc64f16..0000000000 --- a/packages/cli-repl/src/arg-parser.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { expect } from 'chai'; -import { parseMongoshCliArgs } from './arg-parser'; -import stripAnsi from 'strip-ansi'; - -describe('parseMongoshCliArgs', function () { - const baseArgv = ['node', 'mongosh']; - const uri = 'mongodb://domain.com:2020'; - context('when providing an unknown parameter', function () { - const argv = [...baseArgv, uri, '--what']; - - it('raises an error', function () { - try { - parseMongoshCliArgs(argv); - } catch (err: any) { - return expect(stripAnsi(err.message)).to.contain( - 'Error parsing command line: unrecognized option: --what' - ); - } - expect.fail('parsing unknown parameter did not throw'); - }); - - context('parses standard arguments correctly', function () { - it('sets passed fields', function () { - const argv = [...baseArgv, uri, '--tls', '--port', '1234']; - - const args = parseMongoshCliArgs(argv); - expect(args['tls']).equals(true); - expect(args['port']).equals('1234'); - }); - - it(`replaces --sslPEMKeyFile with --tlsCertificateKeyFile`, function () { - const argv = [...baseArgv, `--sslPEMKeyFile`, `test`]; - - const args = parseMongoshCliArgs(argv); - expect(args).to.not.have.property('sslPEMKeyFile'); - expect(args['tlsCertificateKeyFile']).to.equal('test'); - }); - }); - }); -}); diff --git a/packages/cli-repl/src/arg-parser.ts b/packages/cli-repl/src/arg-parser.ts deleted file mode 100644 index 4ff9762a4a..0000000000 --- a/packages/cli-repl/src/arg-parser.ts +++ /dev/null @@ -1,31 +0,0 @@ -import i18n from '@mongosh/i18n'; -import { - parseCliArgs, - UnknownCliArgumentError, -} from '@mongosh/arg-parser/arg-parser'; -import { colorizeForStderr as clr } from './clr'; -import { USAGE } from './constants'; - -/** - * Unknown translation key. - */ -const UNKNOWN = 'cli-repl.arg-parser.unknown-option'; - -export function parseMongoshCliArgs( - args: string[] -): ReturnType { - try { - return parseCliArgs(args); - } catch (error) { - if (error instanceof UnknownCliArgumentError) { - throw new Error( - ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr( - String(error.argument), - 'bold' - )} - ${USAGE}` - ); - } - throw error; - } -} diff --git a/packages/cli-repl/src/parse-mongosh-args.spec.ts b/packages/cli-repl/src/parse-mongosh-args.spec.ts new file mode 100644 index 0000000000..8e61065062 --- /dev/null +++ b/packages/cli-repl/src/parse-mongosh-args.spec.ts @@ -0,0 +1,64 @@ +import { expect } from 'chai'; +import { parseMongoshArgs } from './parse-mongosh-args'; +import stripAnsi from 'strip-ansi'; +import { MongoshUnimplementedError } from '@mongosh/errors'; + +describe('parseMongoshArgs', function () { + const baseArgv = ['node', 'mongosh']; + const uri = 'mongodb://domain.com:2020'; + context('when providing an unknown parameter', function () { + const argv = [...baseArgv, uri, '--what']; + + it('raises an error', function () { + try { + parseMongoshArgs(argv); + } catch (err: any) { + return expect(stripAnsi(err.message)).to.contain( + 'Error parsing command line: unrecognized option: --what' + ); + } + expect.fail('parsing unknown parameter did not throw'); + }); + + context('parses standard arguments correctly', function () { + it('parses connectionSpecifier correctly', function () { + const argv = [...baseArgv, uri]; + const args = parseMongoshArgs(argv); + expect(args.parsed.connectionSpecifier).to.equal(uri); + }); + + it('parses fileNames correctly', function () { + const argv = [...baseArgv, uri, 'file1.js', 'file2.js']; + const args = parseMongoshArgs(argv); + expect(args.parsed.fileNames).to.deep.equal(['file1.js', 'file2.js']); + }); + + it('sets passed fields', function () { + const argv = [...baseArgv, uri, '--tls', '--port', '1234']; + + const args = parseMongoshArgs(argv); + expect(args.parsed['tls']).equals(true); + expect(args.parsed['port']).equals('1234'); + }); + + it('throws an error for unsupported arguments', function () { + const argv = [...baseArgv, '--gssapiHostName', 'example.com']; + expect(() => parseMongoshArgs(argv)).to.throw( + MongoshUnimplementedError, + 'Argument --gssapiHostName is not supported in mongosh' + ); + }); + + it(`replaces --sslPEMKeyFile with --tlsCertificateKeyFile`, function () { + const argv = [...baseArgv, `--sslPEMKeyFile`, `test`]; + + const args = parseMongoshArgs(argv); + expect(args).to.not.have.property('sslPEMKeyFile'); + expect(args.parsed['tlsCertificateKeyFile']).to.equal('test'); + expect(args.warnings).to.deep.equal([ + 'WARNING: argument --sslPEMKeyFile is deprecated and will be removed. Use --tlsCertificateKeyFile instead.', + ]); + }); + }); + }); +}); diff --git a/packages/cli-repl/src/parse-mongosh-args.ts b/packages/cli-repl/src/parse-mongosh-args.ts new file mode 100644 index 0000000000..82ac89e85f --- /dev/null +++ b/packages/cli-repl/src/parse-mongosh-args.ts @@ -0,0 +1,54 @@ +import i18n from '@mongosh/i18n'; +import { + getLocale, + parseArgsWithCliOptions, + UnknownCliArgumentError, + UnsupportedCliArgumentError, +} from '@mongosh/arg-parser/arg-parser'; +import { colorizeForStderr as clr } from './clr'; +import { USAGE } from './constants'; +import type { CliOptions } from '@mongosh/arg-parser'; +import { CommonErrors, MongoshUnimplementedError } from '@mongosh/errors'; + +/** + * Unknown translation key. + */ +const UNKNOWN = 'cli-repl.arg-parser.unknown-option'; + +export function parseMongoshArgs(argsWithProgram: string[]): { + parsed: CliOptions; + warnings: string[]; +} { + try { + const args = argsWithProgram.slice(2); + i18n.setLocale(getLocale(argsWithProgram, process.env)); + + const { parsed, deprecated } = parseArgsWithCliOptions({ args }); + const warnings = Object.entries(deprecated).map( + ([deprecated, replacement]) => + `WARNING: argument --${deprecated} is deprecated and will be removed. Use --${replacement} instead.` + ); + return { + parsed, + warnings, + }; + } catch (error) { + if (error instanceof UnsupportedCliArgumentError) { + throw new MongoshUnimplementedError( + `Argument --${error.argument} is not supported in mongosh`, + CommonErrors.InvalidArgument + ); + } + if (error instanceof UnknownCliArgumentError) { + throw new MongoshUnimplementedError( + ` ${clr(i18n.__(UNKNOWN), 'mongosh:error')} ${clr( + String(error.argument), + 'bold' + )} + ${USAGE}`, + CommonErrors.InvalidArgument + ); + } + throw error; + } +} diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 612bf42ee1..89e73ba22d 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -15,7 +15,7 @@ enableFipsIfRequested(); import { markTime } from './startup-timing'; import { CliRepl } from './cli-repl'; -import { parseMongoshCliArgs } from './arg-parser'; +import { parseMongoshArgs } from './parse-mongosh-args'; import { runSmokeTests } from './smoke-tests'; import { USAGE } from './constants'; import { baseBuildInfo, buildInfo } from './build-info'; @@ -85,8 +85,8 @@ async function main() { try { (net as any)?.setDefaultAutoSelectFamily?.(true); - const options = parseMongoshCliArgs(process.argv); - for (const warning of options._argParseWarnings) { + const { parsed: options, warnings } = parseMongoshArgs(process.argv); + for (const warning of warnings) { console.warn(warning); } From 708932c820be6c6eed0b2054b103ba1e1d8f2918 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 1 Dec 2025 16:44:38 +0100 Subject: [PATCH 08/12] chore: fixups --- packages/arg-parser/src/arg-metadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/arg-parser/src/arg-metadata.ts b/packages/arg-parser/src/arg-metadata.ts index a0caa9f3ad..05e9784e7f 100644 --- a/packages/arg-parser/src/arg-metadata.ts +++ b/packages/arg-parser/src/arg-metadata.ts @@ -11,7 +11,7 @@ export const argMetadata = z.registry(); export type ArgumentMetadata = { /** If set, sets this field as deprecated and replaces this field with the set field. */ deprecationReplacement?: string; - /** If set, gets replaced with a differet field name (without deprecation) */ + /** If set, gets replaced with a different field name (without deprecation) */ replacement?: string; /** Whether this argument is unsupported. Always throws an error if set to true. */ unsupported?: boolean; From 42d6f1a04340b68fef37abf00e118e50d329ddf2 Mon Sep 17 00:00:00 2001 From: gagik Date: Mon, 1 Dec 2025 16:45:55 +0100 Subject: [PATCH 09/12] chore: make diffs better by keeping the rename --- .../src/{parse-mongosh-args.spec.ts => arg-parser.spec.ts} | 2 +- packages/cli-repl/src/{parse-mongosh-args.ts => arg-parser.ts} | 0 packages/cli-repl/src/run.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/cli-repl/src/{parse-mongosh-args.spec.ts => arg-parser.spec.ts} (97%) rename packages/cli-repl/src/{parse-mongosh-args.ts => arg-parser.ts} (100%) diff --git a/packages/cli-repl/src/parse-mongosh-args.spec.ts b/packages/cli-repl/src/arg-parser.spec.ts similarity index 97% rename from packages/cli-repl/src/parse-mongosh-args.spec.ts rename to packages/cli-repl/src/arg-parser.spec.ts index 8e61065062..15293b546a 100644 --- a/packages/cli-repl/src/parse-mongosh-args.spec.ts +++ b/packages/cli-repl/src/arg-parser.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { parseMongoshArgs } from './parse-mongosh-args'; +import { parseMongoshArgs } from './arg-parser'; import stripAnsi from 'strip-ansi'; import { MongoshUnimplementedError } from '@mongosh/errors'; diff --git a/packages/cli-repl/src/parse-mongosh-args.ts b/packages/cli-repl/src/arg-parser.ts similarity index 100% rename from packages/cli-repl/src/parse-mongosh-args.ts rename to packages/cli-repl/src/arg-parser.ts diff --git a/packages/cli-repl/src/run.ts b/packages/cli-repl/src/run.ts index 89e73ba22d..62cfb43664 100644 --- a/packages/cli-repl/src/run.ts +++ b/packages/cli-repl/src/run.ts @@ -15,7 +15,7 @@ enableFipsIfRequested(); import { markTime } from './startup-timing'; import { CliRepl } from './cli-repl'; -import { parseMongoshArgs } from './parse-mongosh-args'; +import { parseMongoshArgs } from './arg-parser'; import { runSmokeTests } from './smoke-tests'; import { USAGE } from './constants'; import { baseBuildInfo, buildInfo } from './build-info'; From 387925af5277aec0eb7466d875e846cb5f9d2457 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 2 Dec 2025 09:42:33 +0100 Subject: [PATCH 10/12] chore: cleanup browser behavior --- packages/arg-parser/src/arg-parser.spec.ts | 93 +++++++++++++--------- packages/arg-parser/src/arg-parser.ts | 30 +++++-- packages/arg-parser/src/cli-options.ts | 16 +++- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts index 25762d4d89..b4745b7ecd 100644 --- a/packages/arg-parser/src/arg-parser.spec.ts +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -1134,10 +1134,45 @@ describe('arg-parser', function () { }); describe('union type fields', function () { - for (const { argument, values, onlyFalse, strict } of [ + describe('--browser', function () { + it('does not coerce to boolean with --browser=true', function () { + expect( + parseArgsWithCliOptions({ args: ['--browser=true'] }).parsed.browser + ).to.equal('true'); + }); + + it('coerces to boolean with --browser=false', function () { + expect( + parseArgsWithCliOptions({ args: ['--browser=false'] }).parsed.browser + ).to.equal(false); + }); + + it('coerces to false with --no-browser', function () { + expect( + parseArgsWithCliOptions({ args: ['--no-browser'] }).parsed.browser + ).to.equal(false); + }); + + it('uses string if browser=something', function () { + expect( + parseArgsWithCliOptions({ args: ['--browser=something'] }).parsed + .browser + ).to.equal('something'); + }); + + it('throws if just --browser is provided', function () { + expect( + () => parseArgsWithCliOptions({ args: ['--browser'] }).parsed.browser + ).to.throw( + MongoshUnimplementedError, + '--browser can only be true or a string' + ); + }); + }); + + for (const { argument, values } of [ { argument: 'json', values: ['relaxed', 'canonical'] }, { argument: 'oidcDumpTokens', values: ['redacted', 'include-secrets'] }, - { argument: 'browser', values: ['test'], onlyFalse: true, strict: false }, ] as const) { describe(`with ${argument}`, function () { context('with boolean', function () { @@ -1149,23 +1184,13 @@ describe('arg-parser', function () { ).to.equal(true); }); - if (!onlyFalse) { - it(`coerces to true with --${argument}=true`, function () { - expect( - parseArgsWithCliOptions({ - args: [`--${argument}=true`], - }).parsed[argument] - ).to.equal(true); - }); - } else { - it(`does not coerce with "--${argument} true"`, function () { - expect( - parseArgsWithCliOptions({ - args: [`--${argument}=true`], - }).parsed[argument] - ).to.be.equal('true'); - }); - } + it(`coerces to true with --${argument}=true`, function () { + expect( + parseArgsWithCliOptions({ + args: [`--${argument}=true`], + }).parsed[argument] + ).to.equal(true); + }); it(`coerces to false with --${argument}=false`, function () { expect( @@ -1197,22 +1222,16 @@ describe('arg-parser', function () { }); } - if (strict) { - it('throws an error with invalid value', function () { - try { - parseArgsWithCliOptions({ - args: [`--${argument}`, 'invalid'], - }); - } catch (e: any) { - expect(e).to.be.instanceOf(MongoshUnimplementedError); - expect(e.message).to.include( - `--${argument} can only have the values ${values.join(', ')}` - ); - return; - } - expect.fail('Expected error'); - }); - } + it('throws an error with invalid value', function () { + expect(() => + parseArgsWithCliOptions({ + args: [`--${argument}`, 'invalid'], + }) + ).to.throw( + MongoshUnimplementedError, + `--${argument} can only have the values ${values.join(', ')}` + ); + }); }); } }); @@ -1246,7 +1265,7 @@ describe('arg-parser', function () { }); }); - it('generates the expected options for Cli Options', function () { + it('generates the expected options for CliOptions', function () { const options = generateYargsOptionsFromSchema({ schema: CliOptionsSchema, }); @@ -1260,6 +1279,7 @@ describe('arg-parser', function () { 'awsIamSessionToken', 'awsSecretAccessKey', 'awsSessionToken', + 'browser', 'csfleLibraryPath', 'cryptSharedLibPath', 'db', @@ -1294,6 +1314,7 @@ describe('arg-parser', function () { 'apiDeprecationErrors', 'apiStrict', 'buildInfo', + 'deepInspect', 'exposeAsyncRewriter', 'help', 'ipv6', diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index d798b8dd37..6a0f7957c5 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -16,6 +16,16 @@ import { UnsupportedCliArgumentError, } from './arg-metadata'; +function unwrapType(type: unknown): unknown { + if (type instanceof z.ZodOptional) { + return unwrapType(type.unwrap()); + } + if (type instanceof z.ZodDefault) { + return unwrapType(type.unwrap()); + } + return type; +} + /** * Generate yargs-parser configuration from schema */ @@ -50,11 +60,7 @@ export function generateYargsOptionsFromSchema({ for (const [fieldName, fieldSchema] of Object.entries(schema.shape)) { const meta = getArgumentMetadata(schema, fieldName); - // Unwrap optional type - let unwrappedType = fieldSchema; - if (fieldSchema instanceof z.ZodOptional) { - unwrappedType = fieldSchema.unwrap(); - } + const unwrappedType = unwrapType(fieldSchema); // Determine type if (unwrappedType instanceof z.ZodArray) { @@ -85,6 +91,8 @@ export function generateYargsOptionsFromSchema({ if (hasFalseLiteral) { // If set to 'false' coerce into false boolean; string in all other cases options.coerce[fieldName] = coerceIfFalse; + // Setting as string prevents --{field} from being valid. + options.string.push(fieldName); } else if (hasBoolean) { // If the field is 'true' or 'false', we coerce the value to a boolean. options.coerce[fieldName] = coerceIfBoolean; @@ -107,7 +115,13 @@ export function generateYargsOptionsFromSchema({ ); } } else { - throw new Error(`Unknown field type: ${unwrappedType.constructor.name}`); + throw new Error( + `Unknown field type: ${ + unwrappedType instanceof Object + ? unwrappedType.constructor.name + : typeof unwrappedType + }` + ); } // Add aliases @@ -253,6 +267,10 @@ export function coerceIfBoolean(value: unknown): unknown { } export function coerceIfFalse(value: unknown): unknown { + if (value === undefined || value === '') { + return null; + } + if (typeof value === 'string') { if (value === 'false') { return false; diff --git a/packages/arg-parser/src/cli-options.ts b/packages/arg-parser/src/cli-options.ts index 92c656e8a5..173828d5f1 100644 --- a/packages/arg-parser/src/cli-options.ts +++ b/packages/arg-parser/src/cli-options.ts @@ -197,7 +197,7 @@ export function validateCliOptions(parsed: CliOptions): void { const jsonValidation = CliOptionsSchema.shape.json.safeParse(parsed.json); if (!jsonValidation.success) { throw new MongoshUnimplementedError( - '--json can only have the values relaxed or canonical', + '--json can only have the values relaxed, canonical', CommonErrors.InvalidArgument ); } @@ -206,7 +206,7 @@ export function validateCliOptions(parsed: CliOptions): void { CliOptionsSchema.shape.oidcDumpTokens.safeParse(parsed.oidcDumpTokens); if (!oidcDumpTokensValidation.success) { throw new MongoshUnimplementedError( - '--oidcDumpTokens can only have the values redacted or include-secrets', + '--oidcDumpTokens can only have the values redacted, include-secrets', CommonErrors.InvalidArgument ); } @@ -216,7 +216,17 @@ export function validateCliOptions(parsed: CliOptions): void { ); if (!jsContextValidation.success) { throw new MongoshUnimplementedError( - '--jsContext can only have the values repl, plain-vm, or auto', + '--jsContext can only have the values repl, plain-vm, auto', + CommonErrors.InvalidArgument + ); + } + + const browserValidation = CliOptionsSchema.shape.browser.safeParse( + parsed.browser + ); + if (!browserValidation.success) { + throw new MongoshUnimplementedError( + '--browser can only be true or a string', CommonErrors.InvalidArgument ); } From f7ff517af83f760b57337aa860937a6ccc9d353d Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 2 Dec 2025 09:59:49 +0100 Subject: [PATCH 11/12] chore: let one pass a z.object --- packages/arg-parser/src/arg-parser.spec.ts | 8 ++++---- packages/arg-parser/src/arg-parser.ts | 7 +++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts index b4745b7ecd..0dd6775452 100644 --- a/packages/arg-parser/src/arg-parser.spec.ts +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -1450,13 +1450,13 @@ describe('arg-parser', function () { '--deprecatedField', '100', ], - schema: { + schema: z.object({ extendedField: z.number(), replacedField: z.number(), deprecatedField: z.number().register(argMetadata, { deprecationReplacement: 'replacedField', }), - }, + }), }); expect(options).to.deep.equal({ @@ -1486,9 +1486,9 @@ describe('arg-parser', function () { '--unknownField', '100', ], - schema: { + schema: z.object({ extendedField: z.enum(['90', '100']), - }, + }), }) ).to.throw(UnknownCliArgumentError, 'Unknown argument: --unknownField'); }); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index 6a0f7957c5..21849f7004 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -228,11 +228,14 @@ export function parseArgsWithCliOptions({ }: { args: string[]; /** Schema to extend the CLI options schema with. */ - schema?: Record; + schema?: z.ZodObject; }): ReturnType> { const schema = schemaToExtend !== undefined - ? CliOptionsSchema.extend(schemaToExtend) + ? z.object({ + ...CliOptionsSchema.shape, + ...schemaToExtend.shape, + }) : CliOptionsSchema; const { parsed, positional, deprecated } = parseArgs({ args, From 7455b83b67b654bb230edfee5e1c7a5af2624266 Mon Sep 17 00:00:00 2001 From: gagik Date: Tue, 2 Dec 2025 12:09:52 +0100 Subject: [PATCH 12/12] chore: make types cleaner and more flexible --- packages/arg-parser/src/arg-parser.spec.ts | 6 ++- packages/arg-parser/src/arg-parser.ts | 43 +++++++++++++--------- packages/arg-parser/src/cli-options.ts | 6 +-- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/packages/arg-parser/src/arg-parser.spec.ts b/packages/arg-parser/src/arg-parser.spec.ts index 0dd6775452..3934b09889 100644 --- a/packages/arg-parser/src/arg-parser.spec.ts +++ b/packages/arg-parser/src/arg-parser.spec.ts @@ -1247,8 +1247,10 @@ describe('arg-parser', function () { it('generates from arbitrary schema', function () { const options = generateYargsOptionsFromSchema({ schema: testSchema, - configuration: { - 'combine-arrays': true, + parserOptions: { + configuration: { + 'combine-arrays': true, + }, }, }); diff --git a/packages/arg-parser/src/arg-parser.ts b/packages/arg-parser/src/arg-parser.ts index 21849f7004..c84f0e020e 100644 --- a/packages/arg-parser/src/arg-parser.ts +++ b/packages/arg-parser/src/arg-parser.ts @@ -26,12 +26,8 @@ function unwrapType(type: unknown): unknown { return type; } -/** - * Generate yargs-parser configuration from schema - */ -export function generateYargsOptionsFromSchema({ - schema, - configuration = { +export const defaultParserOptions: Partial = { + configuration: { 'camel-case-expansion': false, 'unknown-options-as-args': true, 'parse-positional-numbers': false, @@ -39,11 +35,22 @@ export function generateYargsOptionsFromSchema({ 'greedy-arrays': false, 'short-option-groups': false, }, +}; + +export type ParserOptions = Partial; + +/** + * Generate yargs-parser configuration from schema + */ +export function generateYargsOptionsFromSchema({ + schema, + parserOptions = defaultParserOptions, }: { schema: z.ZodObject; - configuration?: YargsOptions['configuration']; + parserOptions?: Partial; }): YargsOptions { const options = { + ...parserOptions, string: [], boolean: [], array: [], @@ -132,10 +139,7 @@ export function generateYargsOptionsFromSchema({ } } - return { - ...options, - configuration, - }; + return options; } /** @@ -157,11 +161,11 @@ export function getLocale(args: string[], env: any): string { export function parseArgs({ args, schema, - parserConfiguration, + parserOptions, }: { args: string[]; schema: z.ZodObject; - parserConfiguration?: YargsOptions['configuration']; + parserOptions?: YargsOptions; }): { /** Parsed options from the schema, including replaced deprecated arguments. */ parsed: T & Omit; @@ -172,7 +176,7 @@ export function parseArgs({ } { const options = generateYargsOptionsFromSchema({ schema, - configuration: parserConfiguration, + parserOptions, }); const { _: positional, ...parsedArgs } = parser(args, options); @@ -222,14 +226,18 @@ type ParsedCliOptions = CliOptions & { }; /** Parses the arguments with special handling of mongosh CLI options fields. */ -export function parseArgsWithCliOptions({ +export function parseArgsWithCliOptions< + T extends CliOptions = ParsedCliOptions +>({ args, schema: schemaToExtend, + parserOptions, }: { args: string[]; /** Schema to extend the CLI options schema with. */ schema?: z.ZodObject; -}): ReturnType> { + parserOptions?: Partial; +}): ReturnType> { const schema = schemaToExtend !== undefined ? z.object({ @@ -237,9 +245,10 @@ export function parseArgsWithCliOptions({ ...schemaToExtend.shape, }) : CliOptionsSchema; - const { parsed, positional, deprecated } = parseArgs({ + const { parsed, positional, deprecated } = parseArgs({ args, schema, + parserOptions, }); const processed = processPositionalCliOptions({ diff --git a/packages/arg-parser/src/cli-options.ts b/packages/arg-parser/src/cli-options.ts index 173828d5f1..fbe1c1a333 100644 --- a/packages/arg-parser/src/cli-options.ts +++ b/packages/arg-parser/src/cli-options.ts @@ -167,13 +167,13 @@ export type CliOptions = Omit< fileNames?: string[]; }; -export function processPositionalCliOptions({ +export function processPositionalCliOptions({ parsed, positional, }: { - parsed: CliOptions; + parsed: T; positional: parser.Arguments['_']; -}): CliOptions { +}): T { const processed = { ...parsed }; if (typeof positional[0] === 'string') { if (!processed.nodb && isConnectionSpecifier(positional[0])) {