From c17ca80d87e65d1c82b480b38f95224f2e003d92 Mon Sep 17 00:00:00 2001 From: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:55:52 +0530 Subject: [PATCH] fix(js-sdk): parse Dockerfile ENV/ARG values with spaces handleEnvInstruction parsed each dockerfile-ast argument token in isolation, which splits on whitespace (including inside quotes). ENV NAME="John Doe" and ENV KEY=hello world fell into the "key value" branch and produced a malformed key like NAME="John. Rejoin the tokens and parse the raw value the same way as the Python SDK: a key=value value runs across whitespace until the next key= token, and surrounding quotes are stripped. Multiple pairs on one line stay separated. --- .../fix-dockerfile-env-quoted-values.md | 5 ++ .../js-sdk/src/template/dockerfileParser.ts | 85 ++++++------------- .../template/methods/fromDockerfile.test.ts | 21 +++++ 3 files changed, 53 insertions(+), 58 deletions(-) create mode 100644 .changeset/fix-dockerfile-env-quoted-values.md diff --git a/.changeset/fix-dockerfile-env-quoted-values.md b/.changeset/fix-dockerfile-env-quoted-values.md new file mode 100644 index 0000000000..f8d6ad8bee --- /dev/null +++ b/.changeset/fix-dockerfile-env-quoted-values.md @@ -0,0 +1,5 @@ +--- +'e2b': patch +--- + +Fix `Template.fromDockerfile` parsing of `ENV`/`ARG` values that contain whitespace. A quoted value like `ENV NAME="John Doe"` (and an unquoted `ENV KEY=hello world`) was split into a malformed key; it is now parsed as a single value with surrounding quotes stripped, matching the Python SDK. Multiple `key=value` pairs on one line stay separated. diff --git a/packages/js-sdk/src/template/dockerfileParser.ts b/packages/js-sdk/src/template/dockerfileParser.ts index 6ed62b57af..f5ca974c5b 100644 --- a/packages/js-sdk/src/template/dockerfileParser.ts +++ b/packages/js-sdk/src/template/dockerfileParser.ts @@ -231,67 +231,36 @@ function handleEnvInstruction( const argumentsData = instruction.getArguments() const keyword = instruction.getKeyword() - if (argumentsData && argumentsData.length >= 1) { - const envVars: Record = {} - - if (argumentsData.length === 2) { - // ENV key value format OR multiple key=value pairs (from line continuation) - const firstArg = argumentsData[0].getValue() - const secondArg = argumentsData[1].getValue() - - // Check if both arguments contain '=' (multiple key=value pairs) - if (firstArg.includes('=') && secondArg.includes('=')) { - // Both are key=value pairs (line continuation) - for (const arg of argumentsData) { - const envString = arg.getValue() - const equalIndex = envString.indexOf('=') - if (equalIndex > 0) { - const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) - envVars[key] = value - } - } - } else { - // Traditional ENV key value format - envVars[firstArg] = secondArg - } - } else if (argumentsData.length === 1) { - // ENV/ARG key=value format (single argument) or ARG key (without default) - const envString = argumentsData[0].getValue() - - // Check if it's a simple key=value or just a key (for ARG without default) - const equalIndex = envString.indexOf('=') - if (equalIndex > 0) { - const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) - envVars[key] = value - } else if (keyword === 'ARG' && envString.trim()) { - // ARG without default value - set as empty ENV - const key = envString.trim() - envVars[key] = '' - } - } else { - // Multiple arguments (from line continuation with backslashes) - for (const arg of argumentsData) { - const envString = arg.getValue() - const equalIndex = envString.indexOf('=') - if (equalIndex > 0) { - const key = envString.substring(0, equalIndex) - const value = envString.substring(equalIndex + 1) - envVars[key] = value - } else if (keyword === 'ARG') { - // ARG without default value - const key = envString - envVars[key] = '' - } - } - } + if (!argumentsData || argumentsData.length === 0) { + return + } - // Call setEnvs once with all environment variables from this instruction - if (Object.keys(envVars).length > 0) { - templateBuilder.setEnvs(envVars) + // dockerfile-ast splits arguments on whitespace, including inside quotes, so + // rejoin them to recover the raw value and parse it like the Python SDK: in + // the `key=value` form a value runs across whitespace until the next `key=` + // token, and surrounding quotes are stripped. Parsing each token in isolation + // mangled `ENV NAME="John Doe"` and `ENV KEY=a b` into a broken key. + const value = argumentsData.map((arg) => arg.getValue()).join(' ') + const envVars: Record = {} + + if (value.includes('=')) { + const pairRegex = /(\w+)=([^\s]*(?:\s+(?!\w+=)[^\s]*)*)/g + let match: RegExpExecArray | null + while ((match = pairRegex.exec(value)) !== null) { + envVars[match[1]] = match[2].replace(/^["']+|["']+$/g, '') + } + } else { + const spaceForm = value.match(/^(\S+)\s+([\s\S]+)$/) + if (spaceForm) { + envVars[spaceForm[1]] = spaceForm[2].replace(/^["']+|["']+$/g, '') + } else if (keyword === 'ARG' && value.trim()) { + envVars[value.trim()] = '' } } + + if (Object.keys(envVars).length > 0) { + templateBuilder.setEnvs(envVars) + } } function handleCmdEntrypointInstruction( diff --git a/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts b/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts index 3b2f7256db..4e1efcbc75 100644 --- a/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts +++ b/packages/js-sdk/tests/template/methods/fromDockerfile.test.ts @@ -196,3 +196,24 @@ COPY --chown=anotheruser config.json /config/` assert.equal(copyInstruction2.args[1], '/config/') assert.equal(copyInstruction2.args[2], 'anotheruser') // user from --chown (without group) }) + +buildTemplateTest('fromDockerfile parses quoted and spaced ENV values', async () => { + const dockerfile = `FROM node:24 +ENV NAME="John Doe" +ENV GREETING=hello world +ENV A=1 B=2` + + const template = Template().fromDockerfile(dockerfile) + + const envInstructions = ( + // @ts-expect-error - instructions is not a property of TemplateBuilder + template.instructions as { type: InstructionType; args: string[] }[] + ).filter((i) => i.type === InstructionType.ENV) + + // A quoted value with a space stays a single value (was split into a broken key). + assert.deepEqual(envInstructions[0].args, ['NAME', 'John Doe']) + // An unquoted value runs to the next `key=` token. + assert.deepEqual(envInstructions[1].args, ['GREETING', 'hello world']) + // Multiple key=value pairs on one line stay separated. + assert.deepEqual(envInstructions[2].args, ['A', '1', 'B', '2']) +})