Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli-repl/src/format-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { HelpProperties, CollectionNamesWithTypes } from '@mongosh/shell-api';

type EvaluationResult = {
value: any;
type?: string;
type?: string | null;
};

type FormatOptions = {
Expand Down
30 changes: 30 additions & 0 deletions packages/cli-repl/src/mongosh-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,36 @@ describe('MongoshNodeRepl', () => {
}
expect(output).not.to.include('!this should not run!');
});

it('_ returns the last result', async() => {
input.write('42\n');
await waitEval(bus);
output = '';

input.write('_\n');
await waitEval(bus);
expect(output).to.include('42');
});

it('_ can be used like the last result in expressions', async() => {
input.write('({ foo: "bar", baz: "quux" });\n');
await waitEval(bus);
output = '';

input.write('JSON.stringify(_)\n');
await waitEval(bus);
expect(output).to.include('{"foo":"bar","baz":"quux"}');
});

it('_error yields the last exception', async() => {
input.write('throw new Error("blah")\n');
await waitEval(bus);
output = '';

input.write('_error.message\n');
await waitEval(bus);
expect(output).to.include('blah');
});
});

context('with terminal: true', () => {
Expand Down
32 changes: 19 additions & 13 deletions packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import completer from '@mongosh/autocomplete';
import { MongoshCommandFailed, MongoshInternalError, MongoshWarning } from '@mongosh/errors';
import { changeHistory } from '@mongosh/history';
import type { AutoEncryptionOptions, ServiceProvider } from '@mongosh/service-provider-core';
import { EvaluationListener, OnLoadResult, ShellCliOptions, ShellInternalState } from '@mongosh/shell-api';
import { EvaluationListener, OnLoadResult, ShellCliOptions, ShellInternalState, getShellApiType, toShellResult } from '@mongosh/shell-api';
import { ShellEvaluator, ShellResult } from '@mongosh/shell-evaluator';
import { CliUserConfig, ConfigProvider, CliUserConfigValidator, MongoshBus } from '@mongosh/types';
import askcharacter from 'askcharacter';
Expand Down Expand Up @@ -46,7 +46,7 @@ export type MongoshNodeReplOptions = {
export type InitializationToken = { __initialized: 'yes' };

type MongoshRuntimeState = {
shellEvaluator: ShellEvaluator;
shellEvaluator: ShellEvaluator<any>;
internalState: ShellInternalState;
repl: REPLServer;
console: Console;
Expand Down Expand Up @@ -111,6 +111,7 @@ class MongoshNodeRepl implements EvaluationListener {
showStackTraces = false;
loadNestingLevel = 0;
redactHistory: 'keep' | 'remove' | 'remove-redact' = 'remove';
rawValueToShellResult: WeakMap<any, ShellResult> = new WeakMap();

constructor(options: MongoshNodeReplOptions) {
this.input = options.input;
Expand All @@ -130,7 +131,7 @@ class MongoshNodeRepl implements EvaluationListener {

async initialize(serviceProvider: ServiceProvider): Promise<InitializationToken> {
const internalState = new ShellInternalState(serviceProvider, this.bus, this.shellCliOptions);
const shellEvaluator = new ShellEvaluator(internalState);
const shellEvaluator = new ShellEvaluator(internalState, (value: any) => value);
internalState.setEvaluationListener(this);
await internalState.fetchConnectionInfo();

Expand Down Expand Up @@ -398,6 +399,7 @@ class MongoshNodeRepl implements EvaluationListener {
this.output.write(text);
}

// eslint-disable-next-line complexity
async eval(originalEval: asyncRepl.OriginalEvalFunction, input: string, context: any, filename: string): Promise<any> {
if (!this.insideAutoCompleteOrGetPrompt) {
this.lineByLineInput.enableBlockOnNewLine();
Expand All @@ -407,21 +409,21 @@ class MongoshNodeRepl implements EvaluationListener {
let interrupted = false;

try {
const shellResult = await shellEvaluator.customEval(originalEval, input, context, filename);
if (!this.insideAutoCompleteOrGetPrompt) {
return shellResult;
const rawValue = await shellEvaluator.customEval(originalEval, input, context, filename);
if ((typeof rawValue === 'object' && rawValue !== null) || typeof rawValue === 'function') {
this.rawValueToShellResult.set(rawValue, await toShellResult(rawValue));
}
// The Node.js auto completion needs to access the raw values in order
// to be able to autocomplete their properties properly. One catch is
// that we peform some filtering of mongosh methods depending on
// topology, server version, etc., so for those, we only autocomplete
// own, enumerable, non-underscore-prefixed properties and instead leave
// the rest to the @mongosh/autocomplete package.
if (shellResult.type === null) {
return shellResult.rawValue;
if (!this.insideAutoCompleteOrGetPrompt || getShellApiType(rawValue) === null) {
return rawValue;
}
return Object.fromEntries(
Object.entries(shellResult.rawValue)
Object.entries(rawValue)
.filter(([key]) => !key.startsWith('_')));
} catch (err) {
if (this.runtimeState().internalState.interrupted.isSet()) {
Expand Down Expand Up @@ -525,7 +527,7 @@ class MongoshNodeRepl implements EvaluationListener {
/**
* Format the result to a string so it can be written to the output stream.
*/
writer(result: any /* Error | ShellResult */): string {
writer(result: any): string {
// This checks for error instances.
// The writer gets called immediately by the internal `repl.eval`
// in case of errors.
Expand All @@ -540,11 +542,15 @@ class MongoshNodeRepl implements EvaluationListener {
return this.formatError(output);
}

return this.formatShellResult(this.rawValueToShellResult.get(result) ?? { type: null, printable: result });
}

formatShellResult(result: { type: null | string, printable: any }): string {
return this.formatOutput({ type: result.type, value: result.printable });
}

onPrint(values: ShellResult[]): void {
const joined = values.map((value) => this.writer(value)).join(' ');
const joined = values.map((value) => this.formatShellResult(value)).join(' ');
this.output.write(joined + '\n');
}

Expand Down Expand Up @@ -579,7 +585,7 @@ class MongoshNodeRepl implements EvaluationListener {
throw new Error(`Unrecognized prompt type ${type}`);
}

formatOutput(value: { value: any, type?: string }): string {
formatOutput(value: { value: any, type?: string | null }): string {
return formatOutput(value, this.getFormatOptions());
}

Expand Down Expand Up @@ -699,7 +705,7 @@ class MongoshNodeRepl implements EvaluationListener {

function isErrorLike(value: any): boolean {
try {
return value && (
return value && getShellApiType(value) === null && (
(value.message !== undefined && typeof value.stack === 'string') ||
(value.code !== undefined && value.errmsg !== undefined)
);
Expand Down