Repository: stateful/vscode-runme
Pull request #953 (2023-11-07 17:07:09)

PHP shebang fix
Fixes https://github.com/stateful/runme/issues/418 by skipping shell/bash normalization for non-shell.

Completed an over-due refactor of the utility functions, which will avoid circular dependencies in dev.

Cc @edeediong. Once, https://github.com/stateful/runme/pull/419, ships we can remove `interpreter=php` from the shebang example.


Patch:
diff --git a/examples/shebang.md b/examples/shebang.md
index e3de46a0..daa59a50 100644
--- a/examples/shebang.md
+++ b/examples/shebang.md
@@ -35,3 +35,19 @@ then run:
 const myVar: string = 'I am a typed string!'
 console.log(myVar)
 ```
+
+## and, of course, PHP
+
+Be sure to have the `php` interpreter in your `$PATH`:
+
+```php { interpreter=php }
+<?php
+$greeting = "Hello, World!";
+$currentDateTime = date('Y-m-d H:i:s');
+
+// Concatenate the greeting with the current date and time
+$fullGreeting = $greeting . " It's now " . $currentDateTime . "\n";
+
+echo $fullGreeting;
+?>
+```
diff --git a/src/extension/executors/runner.ts b/src/extension/executors/runner.ts
index 19cd74c5..b99e16c6 100644
--- a/src/extension/executors/runner.ts
+++ b/src/extension/executors/runner.ts
@@ -19,14 +19,7 @@ import { ClientMessages } from '../../constants'
 import { ClientMessage } from '../../types'
 import { PLATFORM_OS } from '../constants'
 import { IRunner, IRunnerEnvironment, RunProgramExecution } from '../runner'
-import {
-  getAnnotations,
-  getCellRunmeId,
-  getCmdShellSeq,
-  getTerminalByCell,
-  getWorkspaceEnvs,
-  prepareCmdSeq,
-} from '../utils'
+import { getAnnotations, getCellRunmeId, getTerminalByCell, getWorkspaceEnvs } from '../utils'
 import { postClientMessage } from '../../utils/messaging'
 import { isNotebookTerminalEnabledForCell } from '../../utils/configuration'
 import { Kernel } from '../kernel'
@@ -40,6 +33,7 @@ import {
   getCellCwd,
   getCellProgram,
   getNotebookSkipPromptEnvSetting,
+  getCmdShellSeq,
 } from './utils'
 import { handleVercelDeployOutput, isVercelDeployScript } from './vercel'
 
@@ -95,9 +89,9 @@ export async function executeRunner(
 
   const commands = await parseCommandSeq(
     cellText,
+    execKey, // same as languageId
     skipPromptEnvDocumentLevel === false ? promptEnv : false,
     new Set([...(environment?.initialEnvs() ?? []), ...Object.keys(envs)]),
-    prepareCmdSeq,
   )
   if (!commands) {
     return false
diff --git a/src/extension/executors/task.ts b/src/extension/executors/task.ts
index d7c8bbc7..c14aab6d 100644
--- a/src/extension/executors/task.ts
+++ b/src/extension/executors/task.ts
@@ -14,12 +14,12 @@ import {
 
 import getLogger from '../logger'
 // import { ExperimentalTerminal } from "../terminal"
-import { getCmdShellSeq, getAnnotations, getTerminalRunmeId } from '../utils'
+import { getAnnotations, getTerminalRunmeId } from '../utils'
 import { PLATFORM_OS, ENV_STORE } from '../constants'
 import type { Kernel } from '../kernel'
 import { NotebookCellOutputManager } from '../cell'
 
-import { retrieveShellCommand } from './utils'
+import { getCmdShellSeq, retrieveShellCommand } from './utils'
 import { sh as inlineSh } from './shell'
 
 const BACKGROUND_TASK_HIDE_TIMEOUT = 2000
diff --git a/src/extension/executors/utils.ts b/src/extension/executors/utils.ts
index b923ba31..6c78558a 100644
--- a/src/extension/executors/utils.ts
+++ b/src/extension/executors/utils.ts
@@ -21,6 +21,7 @@ import { NotebookCellOutputManager } from '../cell'
 import { getAnnotations, getWorkspaceFolder } from '../utils'
 import { CommandMode } from '../grpc/runnerTypes'
 
+const HASH_PREFIX_REGEXP = /^\s*\#\s*/g
 const ENV_VAR_REGEXP = /(\$\w+)/g
 /**
  * for understanding post into https://jex.im/regulex/
@@ -357,6 +358,72 @@ function resolveOrAbsolute(parent?: string, child?: string): string | undefined
   return child
 }
 
+/**
+ * treat cells like a series of individual commands
+ * which need to be executed in sequence
+ */
+export function getCmdSeq(cellText: string): string[] {
+  return cellText
+    .trimStart()
+    .split('\\\n')
+    .map((l) => l.trim())
+    .join(' ')
+    .split('\n')
+    .map((l) => {
+      const hashPos = l.indexOf('#')
+      if (hashPos > -1) {
+        return l.substring(0, hashPos).trim()
+      }
+      const stripped = l.trim()
+
+      if (stripped.startsWith('$')) {
+        return stripped.slice(1).trim()
+      } else {
+        return stripped
+      }
+    })
+    .filter((l) => {
+      const hasPrefix = (l.match(HASH_PREFIX_REGEXP) || []).length > 0
+      return l !== '' && !hasPrefix
+    })
+}
+
+/**
+ * treat cells like like a series of individual commands
+ * which need to be executed in sequence
+ *
+ * packages command sequence into single callable script
+ */
+export function getCmdShellSeq(cellText: string, os: string): string {
+  const trimmed = getCmdSeq(cellText).join('; ')
+
+  if (['darwin'].find((entry) => entry === os)) {
+    return `set -e -o pipefail; ${trimmed}`
+  } else if (os.toLocaleLowerCase().startsWith('win')) {
+    return trimmed
+  }
+
+  return `set -e; ${trimmed}`
+}
+
+/**
+ * Does the following to a command list:
+ *
+ * - Splits by new lines
+ * - Removes trailing `$` characters
+ */
+export function prepareCmdSeq(cellText: string): string[] {
+  return cellText.split('\n').map((l) => {
+    const stripped = l.trimStart()
+
+    if (stripped.startsWith('$')) {
+      return stripped.slice(1).trimStart()
+    }
+
+    return l
+  })
+}
+
 /**
  * Parse set of commands, requiring user input for prompted environment
  * variables, and supporting multiline strings
@@ -365,11 +432,13 @@ function resolveOrAbsolute(parent?: string, child?: string): string | undefined
  */
 export async function parseCommandSeq(
   cellText: string,
+  languageId: string,
   promptForEnv = DEFAULT_PROMPT_ENV,
   skipEnvs?: Set<string>,
-  parseBlock?: (block: string) => string[],
 ): Promise<string[] | undefined> {
-  parseBlock ??= (s) => (s ? s.split('\n') : [])
+  const parseBlock = isShellLanguage(languageId)
+    ? prepareCmdSeq
+    : (s: string) => (s ? s.split('\n') : [])
 
   const exportMatches = getCommandExportExtractMatches(cellText, false, promptForEnv)
 
diff --git a/src/extension/provider/runmeTask.ts b/src/extension/provider/runmeTask.ts
index 55de63c4..07b249ad 100644
--- a/src/extension/provider/runmeTask.ts
+++ b/src/extension/provider/runmeTask.ts
@@ -20,7 +20,7 @@ import {
 } from 'vscode'
 
 import getLogger from '../logger'
-import { getAnnotations, getWorkspaceEnvs, prepareCmdSeq } from '../utils'
+import { getAnnotations, getWorkspaceEnvs } from '../utils'
 import { Serializer, RunmeTaskDefinition } from '../../types'
 import { SerializerBase } from '../serializer'
 import type { IRunner, IRunnerEnvironment, RunProgramOptions } from '../runner'
@@ -138,9 +138,9 @@ export class RunmeTaskProvider implements TaskProvider {
         const cellContent = 'value' in cell ? cell.value : cell.document.getText()
         const commands = await parseCommandSeq(
           cellContent,
+          languageId,
           promptEnv,
           new Set([...(environment?.initialEnvs() ?? []), ...Object.keys(envs)]),
-          prepareCmdSeq,
         )
 
         if (!environment) {
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index ca71b088..2fad63e7 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -49,7 +49,6 @@ import { setCurrentCellExecutionDemo } from './handler/utils'
 
 declare var globalThis: any
 
-const HASH_PREFIX_REGEXP = /^\s*\#\s*/g
 const log = getLogger()
 
 /**
@@ -170,72 +169,6 @@ export function getKey(runningCell: vscode.TextDocument): string {
   return languageId
 }
 
-/**
- * treat cells like a series of individual commands
- * which need to be executed in sequence
- */
-export function getCmdSeq(cellText: string): string[] {
-  return cellText
-    .trimStart()
-    .split('\\\n')
-    .map((l) => l.trim())
-    .join(' ')
-    .split('\n')
-    .map((l) => {
-      const hashPos = l.indexOf('#')
-      if (hashPos > -1) {
-        return l.substring(0, hashPos).trim()
-      }
-      const stripped = l.trim()
-
-      if (stripped.startsWith('$')) {
-        return stripped.slice(1).trim()
-      } else {
-        return stripped
-      }
-    })
-    .filter((l) => {
-      const hasPrefix = (l.match(HASH_PREFIX_REGEXP) || []).length > 0
-      return l !== '' && !hasPrefix
-    })
-}
-
-/**
- * Does the following to a command list:
- *
- * - Splits by new lines
- * - Removes trailing `$` characters
- */
-export function prepareCmdSeq(cellText: string): string[] {
-  return cellText.split('\n').map((l) => {
-    const stripped = l.trimStart()
-
-    if (stripped.startsWith('$')) {
-      return stripped.slice(1).trimStart()
-    }
-
-    return l
-  })
-}
-
-/**
- * treat cells like like a series of individual commands
- * which need to be executed in sequence
- *
- * packages command sequence into single callable script
- */
-export function getCmdShellSeq(cellText: string, os: string): string {
-  const trimmed = getCmdSeq(cellText).join('; ')
-
-  if (['darwin'].find((entry) => entry === os)) {
-    return `set -e -o pipefail; ${trimmed}`
-  } else if (os.toLocaleLowerCase().startsWith('win')) {
-    return trimmed
-  }
-
-  return `set -e; ${trimmed}`
-}
-
 export function normalizeLanguage(l?: string) {
   switch (l) {
     case 'zsh':


Test patch:
diff --git a/tests/extension/__snapshots__/utils.test.ts.snap b/tests/extension/__snapshots__/utils.test.ts.snap
index 1fbfd81b..9d3f1849 100644
--- a/tests/extension/__snapshots__/utils.test.ts.snap
+++ b/tests/extension/__snapshots__/utils.test.ts.snap
@@ -1,23 +1,5 @@
 // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
 
-exports[`getCmdShellSeq > complex wrapped 1`] = `"set -e -o pipefail; curl \\"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\\" --compressed 2>/dev/null | jq -r '.[].posts[] | \\"(.title) - by (.authors[0].name), id: (.id)\\"'"`;
-
-exports[`getCmdShellSeq > env only 1`] = `"set -e -o pipefail; export DENO_INSTALL=\\"$HOME/.deno\\"; export PATH=\\"$DENO_INSTALL/bin:$PATH\\""`;
-
-exports[`getCmdShellSeq > leading prompts 1`] = `"set -e -o pipefail; docker build -t runme/demo .; docker ps -qa"`;
-
-exports[`getCmdShellSeq > linux without pipefail 1`] = `"set -e; ls ~/"`;
-
-exports[`getCmdShellSeq > one command 1`] = `"set -e -o pipefail; deno task start"`;
-
-exports[`getCmdShellSeq > trailing comment 1`] = `"set -e -o pipefail; cd ..; ls /; cd ..; ls /"`;
-
-exports[`getCmdShellSeq > windows without shell flags 1`] = `"ls ~/"`;
-
-exports[`getCmdShellSeq > with comments 1`] = `"set -e -o pipefail; echo \\"Install deno via installer script\\"; curl -fsSL https://deno.land/x/install/install.sh | sh"`;
-
-exports[`getCmdShellSeq > wrapped command 1`] = `"set -e -o pipefail; deno install --allow-read --allow-write --allow-env --allow-net --allow-run --no-check -r -f https://deno.land/x/deploy/deployctl.ts"`;
-
 exports[`resetEnv 1`] = `
 Map {
   "RUNME_TASK" => "true",
diff --git a/tests/extension/executors/__snapshots__/utils.test.ts.snap b/tests/extension/executors/__snapshots__/utils.test.ts.snap
new file mode 100644
index 00000000..ab4266fd
--- /dev/null
+++ b/tests/extension/executors/__snapshots__/utils.test.ts.snap
@@ -0,0 +1,19 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`getCmdShellSeq > complex wrapped 1`] = `"set -e -o pipefail; curl \\"https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)\\" --compressed 2>/dev/null | jq -r '.[].posts[] | \\"(.title) - by (.authors[0].name), id: (.id)\\"'"`;
+
+exports[`getCmdShellSeq > env only 1`] = `"set -e -o pipefail; export DENO_INSTALL=\\"$HOME/.deno\\"; export PATH=\\"$DENO_INSTALL/bin:$PATH\\""`;
+
+exports[`getCmdShellSeq > leading prompts 1`] = `"set -e -o pipefail; docker build -t runme/demo .; docker ps -qa"`;
+
+exports[`getCmdShellSeq > linux without pipefail 1`] = `"set -e; ls ~/"`;
+
+exports[`getCmdShellSeq > one command 1`] = `"set -e -o pipefail; deno task start"`;
+
+exports[`getCmdShellSeq > trailing comment 1`] = `"set -e -o pipefail; cd ..; ls /; cd ..; ls /"`;
+
+exports[`getCmdShellSeq > windows without shell flags 1`] = `"ls ~/"`;
+
+exports[`getCmdShellSeq > with comments 1`] = `"set -e -o pipefail; echo \\"Install deno via installer script\\"; curl -fsSL https://deno.land/x/install/install.sh | sh"`;
+
+exports[`getCmdShellSeq > wrapped command 1`] = `"set -e -o pipefail; deno install --allow-read --allow-write --allow-env --allow-net --allow-run --no-check -r -f https://deno.land/x/deploy/deployctl.ts"`;
diff --git a/tests/extension/executors/utils.test.ts b/tests/extension/executors/utils.test.ts
index cd8b4f8d..29c71dc0 100644
--- a/tests/extension/executors/utils.test.ts
+++ b/tests/extension/executors/utils.test.ts
@@ -13,8 +13,11 @@ import {
   getSystemShellPath,
   getCellCwd,
   isShellLanguage,
+  getCmdShellSeq,
+  prepareCmdSeq,
+  getCmdSeq,
 } from '../../../src/extension/executors/utils'
-import { getCmdSeq, getWorkspaceFolder, getAnnotations } from '../../../src/extension/utils'
+import { getWorkspaceFolder, getAnnotations } from '../../../src/extension/utils'
 import { getCellProgram } from '../../../src/extension/executors/utils'
 
 const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
@@ -25,13 +28,6 @@ vi.mock('vscode')
 
 vi.mock('../../../src/extension/utils', () => ({
   replaceOutput: vi.fn(),
-  // TODO: this should use importActual
-  getCmdSeq: vi.fn((cellText: string) =>
-    cellText
-      .trim()
-      .split('\n')
-      .filter((x) => x),
-  ),
   getWorkspaceFolder: vi.fn(),
   getAnnotations: vi.fn(),
 }))
@@ -233,20 +229,20 @@ suite('parseCommandSeq', () => {
     vi.mocked(window.showInputBox).mockReset()
   })
 
-  test('single-line export', async () => {
+  test('single-line export with prompt', async () => {
     vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
 
-    const res = await parseCommandSeq(['export TEST="<placeholder>"'].join('\n'))
+    const res = await parseCommandSeq(['$ export TEST="<placeholder>"'].join('\n'), 'sh')
 
     expect(res).toBeTruthy()
-    expect(res).toHaveLength(1)
-    expect(res?.[0]).toBe('export TEST="test value"')
+    expect(res).toHaveLength(3)
+    expect(res?.[1]).toBe('export TEST="test value"')
   })
 
   test('single-line export with prompt disabled', async () => {
     vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
 
-    const res = await parseCommandSeq(['export TEST="placeholder"'].join('\n'), false)
+    const res = await parseCommandSeq(['export TEST="placeholder"'].join('\n'), 'sh', false)
 
     expect(window.showInputBox).toBeCalledTimes(0)
 
@@ -258,15 +254,35 @@ suite('parseCommandSeq', () => {
   test('single line export with cancelled prompt', async () => {
     vi.mocked(window.showInputBox).mockImplementationOnce(async () => undefined)
 
-    const res = await parseCommandSeq(['export TEST="<placeholder>"'].join('\n'))
+    const res = await parseCommandSeq(['export TEST="<placeholder>"'].join('\n'), 'sh')
 
     expect(res).toBe(undefined)
   })
 
+  test('non shell code with exports retains leading dollar signs', async () => {
+    vi.mocked(window.showInputBox).mockImplementationOnce(async () => undefined)
+
+    const res = await parseCommandSeq(
+      [
+        "$currentDateTime = date('Y-m-d H:i:s');",
+        '$fullGreeting = $greeting . " It\'s now " . $currentDateTime;',
+        'echo $fullGreeting;',
+      ].join('\n'),
+      'php',
+    )
+
+    expect(res).toBeTruthy()
+    expect(res).toStrictEqual([
+      "$currentDateTime = date('Y-m-d H:i:s');",
+      '$fullGreeting = $greeting . " It\'s now " . $currentDateTime;',
+      'echo $fullGreeting;',
+    ])
+  })
+
   test('multiline export', async () => {
     const exportLines = ['export TEST="I', 'am', 'doing', 'well!"']
 
-    const res = await parseCommandSeq(exportLines.join('\n'))
+    const res = await parseCommandSeq(exportLines.join('\n'), 'sh')
 
     expect(res).toBeTruthy
     expect(res).toHaveLength(4)
@@ -289,7 +305,7 @@ suite('parseCommandSeq', () => {
       'echo $TEST_MULTILINE',
     ]
 
-    const res = await parseCommandSeq(cmdLines.join('\n'))
+    const res = await parseCommandSeq(cmdLines.join('\n'), 'sh')
 
     expect(res).toBeTruthy()
     expect(res).toStrictEqual([
@@ -303,34 +319,35 @@ suite('parseCommandSeq', () => {
     ])
   })
 
-  test('exports between normal command sequences with getCmdSeq', async () => {
-    vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
-
-    const cmdLines = [
-      'echo "Hello!"',
-      'echo "Hi!"',
-      'export TEST="<placeholder>"',
-      'echo $TEST',
-      'export TEST_MULTILINE="This',
-      'is',
-      'a',
-      'multiline',
-      'env!"',
-      'echo $TEST_MULTILINE',
-    ]
-
-    const res = await parseCommandSeq(cmdLines.join('\n'), true, undefined, getCmdSeq)
-
-    expect(res).toBeTruthy()
-    expect(res).toStrictEqual([
-      'echo "Hello!"',
-      'echo "Hi!"',
-      'export TEST="test value"',
-      'echo $TEST',
-      ...['export TEST_MULTILINE="This', 'is', 'a', 'multiline', 'env!"'],
-      'echo $TEST_MULTILINE',
-    ])
-  })
+  // todo(seb): Not being used directly anywhere - deactivate for now and remove in future
+  // test('exports between normal command sequences with getCmdSeq', async () => {
+  //   vi.mocked(window.showInputBox).mockImplementationOnce(async () => 'test value')
+
+  //   const cmdLines = [
+  //     'echo "Hello!"',
+  //     'echo "Hi!"',
+  //     'export TEST="<placeholder>"',
+  //     'echo $TEST',
+  //     'export TEST_MULTILINE="This',
+  //     'is',
+  //     'a',
+  //     'multiline',
+  //     'env!"',
+  //     'echo $TEST_MULTILINE',
+  //   ]
+
+  //   const res = await parseCommandSeq(cmdLines.join('\n'), true, undefined, getCmdSeq) // signature obsolete
+
+  //   expect(res).toBeTruthy()
+  //   expect(res).toStrictEqual([
+  //     'echo "Hello!"',
+  //     'echo "Hi!"',
+  //     'export TEST="test value"',
+  //     'echo $TEST',
+  //     ...['export TEST_MULTILINE="This', 'is', 'a', 'multiline', 'env!"'],
+  //     'echo $TEST_MULTILINE',
+  //   ])
+  // })
 })
 
 suite('getCellShellPath', () => {
@@ -527,3 +544,84 @@ suite('getCellCwd', () => {
     expect(await testGetCellCwd(frntmtr, '/opt', files)).toStrictEqual('/opt')
   })
 })
+
+suite('getCmdShellSeq', () => {
+  test('one command', () => {
+    const cellText = 'deno task start'
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('wrapped command', () => {
+    // eslint-disable-next-line max-len
+    const cellText = Buffer.from(
+      // eslint-disable-next-line max-len
+      'ZGVubyBpbnN0YWxsIFwKICAgICAgLS1hbGxvdy1yZWFkIC0tYWxsb3ctd3JpdGUgXAogICAgICAtLWFsbG93LWVudiAtLWFsbG93LW5ldCAtLWFsbG93LXJ1biBcCiAgICAgIC0tbm8tY2hlY2sgXAogICAgICAtciAtZiBodHRwczovL2Rlbm8ubGFuZC94L2RlcGxveS9kZXBsb3ljdGwudHMK',
+      'base64',
+    ).toString('utf-8')
+
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('env only', () => {
+    const cellText = `export DENO_INSTALL="$HOME/.deno"
+      export PATH="$DENO_INSTALL/bin:$PATH"
+    `
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('complex wrapped', () => {
+    // eslint-disable-next-line max-len
+    const cellText =
+      // eslint-disable-next-line max-len
+      'curl "https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)" --compressed 2>/dev/null \\\n| jq -r \'.[].posts[] | "(.title) - by (.authors[0].name), id: (.id)"\''
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('linux without pipefail', () => {
+    const cellText = 'ls ~/'
+    expect(getCmdShellSeq(cellText, 'linux')).toMatchSnapshot()
+  })
+
+  test('windows without shell flags', () => {
+    const cellText = 'ls ~/'
+    expect(getCmdShellSeq(cellText, 'win32')).toMatchSnapshot()
+  })
+
+  test('with comments', () => {
+    const cellText =
+      // eslint-disable-next-line max-len
+      'echo "Install deno via installer script"\n# macOS or Linux\ncurl -fsSL https://deno.land/x/install/install.sh | sh'
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('trailing comment', () => {
+    const cellText = 'cd ..\nls / # list dir contents\ncd ..\nls /'
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+
+  test('leading prompts', () => {
+    const cellText = '$ docker build -t runme/demo .\n$ docker ps -qa'
+    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
+  })
+})
+
+suite('prepareCmdSeq', () => {
+  test('should eliminate leading dollar signs', () => {
+    expect(prepareCmdSeq('$ echo hi')).toStrictEqual(['echo hi'])
+    expect(prepareCmdSeq('  $  echo hi')).toStrictEqual(['echo hi'])
+    expect(prepareCmdSeq('echo 1\necho 2\n $ echo 4')).toStrictEqual(['echo 1', 'echo 2', 'echo 4'])
+  })
+})
+
+suite('getCmdSeq', () => {
+  test('Rejects invalid deno command', () => {
+    const cellText = `export DENO_INSTALL="$HOME/.deno"
+    export PATH="$DENO_INSTALL/bin:$PATH"
+  `
+    const result = getCmdSeq(cellText)
+    expect(result).toStrictEqual([
+      'export DENO_INSTALL="$HOME/.deno"',
+      'export PATH="$DENO_INSTALL/bin:$PATH"',
+    ])
+  })
+})
diff --git a/tests/extension/utils.test.ts b/tests/extension/utils.test.ts
index c35059c2..fd97ad06 100644
--- a/tests/extension/utils.test.ts
+++ b/tests/extension/utils.test.ts
@@ -6,19 +6,16 @@ import {
   getTerminalByCell,
   resetEnv,
   getKey,
-  getCmdShellSeq,
   normalizeLanguage,
   getAnnotations,
   mapGitIgnoreToGlobFolders,
   hashDocumentUri,
   getGrpcHost,
-  prepareCmdSeq,
   openFileAsRunmeNotebook,
   getWorkspaceFolder,
   getWorkspaceEnvs,
   isGitHubLink,
   isDenoScript,
-  getCmdSeq,
   validateAnnotations,
   setNotebookCategories,
   getNotebookCategories,
@@ -149,66 +146,6 @@ test('getKey', () => {
   ).toBe('sh')
 })
 
-suite('getCmdShellSeq', () => {
-  test('one command', () => {
-    const cellText = 'deno task start'
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('wrapped command', () => {
-    // eslint-disable-next-line max-len
-    const cellText = Buffer.from(
-      // eslint-disable-next-line max-len
-      'ZGVubyBpbnN0YWxsIFwKICAgICAgLS1hbGxvdy1yZWFkIC0tYWxsb3ctd3JpdGUgXAogICAgICAtLWFsbG93LWVudiAtLWFsbG93LW5ldCAtLWFsbG93LXJ1biBcCiAgICAgIC0tbm8tY2hlY2sgXAogICAgICAtciAtZiBodHRwczovL2Rlbm8ubGFuZC94L2RlcGxveS9kZXBsb3ljdGwudHMK',
-      'base64',
-    ).toString('utf-8')
-
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('env only', () => {
-    const cellText = `export DENO_INSTALL="$HOME/.deno"
-      export PATH="$DENO_INSTALL/bin:$PATH"
-    `
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('complex wrapped', () => {
-    // eslint-disable-next-line max-len
-    const cellText =
-      // eslint-disable-next-line max-len
-      'curl "https://api-us-west-2.graphcms.com/v2/cksds5im94b3w01xq4hfka1r4/master?query=$(deno run -A query.ts)" --compressed 2>/dev/null \\\n| jq -r \'.[].posts[] | "(.title) - by (.authors[0].name), id: (.id)"\''
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('linux without pipefail', () => {
-    const cellText = 'ls ~/'
-    expect(getCmdShellSeq(cellText, 'linux')).toMatchSnapshot()
-  })
-
-  test('windows without shell flags', () => {
-    const cellText = 'ls ~/'
-    expect(getCmdShellSeq(cellText, 'win32')).toMatchSnapshot()
-  })
-
-  test('with comments', () => {
-    const cellText =
-      // eslint-disable-next-line max-len
-      'echo "Install deno via installer script"\n# macOS or Linux\ncurl -fsSL https://deno.land/x/install/install.sh | sh'
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('trailing comment', () => {
-    const cellText = 'cd ..\nls / # list dir contents\ncd ..\nls /'
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-
-  test('leading prompts', () => {
-    const cellText = '$ docker build -t runme/demo .\n$ docker ps -qa'
-    expect(getCmdShellSeq(cellText, 'darwin')).toMatchSnapshot()
-  })
-})
-
 suite('normalizeLanguage', () => {
   test('with zsh', () => {
     const lang = normalizeLanguage('zsh')
@@ -339,14 +276,6 @@ suite('#getGrpcHost', () => {
   })
 })
 
-suite('prepareCmdSeq', () => {
-  test('should eliminate trailing dollar signs', () => {
-    expect(prepareCmdSeq('$ echo hi')).toStrictEqual(['echo hi'])
-    expect(prepareCmdSeq('  $  echo hi')).toStrictEqual(['echo hi'])
-    expect(prepareCmdSeq('echo 1\necho 2\n $ echo 4')).toStrictEqual(['echo 1', 'echo 2', 'echo 4'])
-  })
-})
-
 suite('openFileAsRunmeNotebook', () => {
   beforeEach(() => {
     vi.mocked(commands.executeCommand).mockClear()
@@ -488,19 +417,6 @@ suite('isDenoScript', () => {
   })
 })
 
-suite('getCmdSeq', () => {
-  test('Rejects invalid deno command', () => {
-    const cellText = `export DENO_INSTALL="$HOME/.deno"
-    export PATH="$DENO_INSTALL/bin:$PATH"
-  `
-    const result = getCmdSeq(cellText)
-    expect(result).toStrictEqual([
-      'export DENO_INSTALL="$HOME/.deno"',
-      'export PATH="$DENO_INSTALL/bin:$PATH"',
-    ])
-  })
-})
-
 suite('validateAnnotations', () => {
   test('it should fail for invalid annotations values', () => {
     const cell: any = {