Skip to content

Debugger fails when file path contains spaces (all platforms) #3984

@ntijoh-daniel-berg

Description

@ntijoh-daniel-berg

Description

Ruby LSP Information

VS Code Version

1.109.5

Ruby LSP Extension Version

0.10.0

Ruby LSP Server Version

0.26.7

Ruby LSP Add-ons

Ruby Version

4.0.1

Ruby Version Manager

chruby

Installed Extensions

Click to expand
  • EditorConfig (0.18.1)
  • LiveServer (5.7.10)
  • asciidoctor-vscode (3.4.5)
  • csdevkit (2.10.3)
  • csharp (2.120.3)
  • elixir-ls (0.30.0)
  • git-graph (1.30.0)
  • html-preview-vscode (0.2.5)
  • remote-explorer (0.5.0)
  • remote-ssh (0.122.0)
  • remote-ssh-edit (0.87.0)
  • ruby-extensions-pack (0.1.13)
  • ruby-lsp (0.10.0)
  • vscode-dotnet-runtime (3.0.0)
  • vscode-elixir (1.1.0)
  • vscode-postgres (1.4.3)

Ruby LSP Settings

Click to expand
Workspace
{}
User
{
  "enabledFeatures": {
    "codeActions": true,
    "diagnostics": true,
    "documentHighlights": true,
    "documentLink": true,
    "documentSymbols": true,
    "foldingRanges": true,
    "formatting": true,
    "hover": true,
    "inlayHint": true,
    "onTypeFormatting": true,
    "selectionRanges": true,
    "semanticHighlighting": true,
    "completion": true,
    "codeLens": true,
    "definition": true,
    "workspaceSymbol": true,
    "signatureHelp": true,
    "typeHierarchy": true
  },
  "featuresConfiguration": {},
  "addonSettings": {},
  "rubyVersionManager": {
    "identifier": "auto"
  },
  "customRubyCommand": "",
  "formatter": "auto",
  "linters": null,
  "bundleGemfile": "",
  "testTimeout": 30,
  "branch": "",
  "pullDiagnosticsOn": "both",
  "useBundlerCompose": false,
  "bypassTypechecker": false,
  "rubyExecutablePath": "",
  "indexing": {},
  "erbSupport": true,
  "featureFlags": {},
  "sigOpacityLevel": "1"
}

Also reproduced on Windows 11 with Ruby 3.4.8 via RubyInstaller2 (x64-mingw-ucrt).

Reproduction steps

  1. Create or open a project whose absolute path contains a space (e.g. /Users/John Doe/Projects/hello or C:\Users\John Doe\Documents\hello)
  2. Use the default generated launch.json (via provideDebugConfigurations)
  3. Launch the debugger
  4. Debugger exits immediately with status 1

Code snippet or error message

2026-02-28 14:28:53.852 [info] [debugger]: Command bundle exec rdbg --open --command -- ruby /Users/john.doe/Desktop/Ruby Test/hello.rb
2026-02-28 14:28:54.093 [info] [debugger]: ruby: No such file or directory -- /Users/john.doe/Desktop/Ruby (LoadError)
2026-02-28 14:28:54.094 [info] [debugger]: Debugger exited with status 1.

The extension builds the rdbg command without quoting the file path. Since the path is unquoted, ruby only sees /Users/john.doe/Desktop/Ruby as the filename and stops at the first space.

Related issue

This is the same underlying bug reported in #1709 (on Linux, closed after a user-side workaround was identified). The maintainer asked which workaround resolved it and noted: "We can do something from the extension side to improve this." That improvement was not followed up on.

Why the existing workaround is insufficient

Issue #1709 was closed after wrapping ${file} in single quotes in launch.json:

"program": "ruby '${file}'"

This has two problems:

  1. It doesn't work on Windowscmd.exe treats single quotes as literal characters.
  2. It requires manual intervention — users who use the auto-generated template (via provideDebugConfigurations) always get the broken version. Every user with spaces in their path has to independently discover and apply this workaround.

Root cause

The program field conflates the Ruby command and the file path into a single shell command string. By the time spawnDebuggeeForLaunch sees i.program, VS Code has already substituted ${file} with the literal path — the extension can no longer distinguish where the command ends and where the path begins. The extension then passes this string to the shell unquoted.

VS Code does not add quotes during variable substitution; quoting is the responsibility of whoever constructs the shell command — in this case, the extension.

Suggested fix

Option A — Fix the template (partial fix)

Update provideDebugConfigurations to emit double-quoted paths:

{ type: "ruby_lsp", name: "Debug script", request: "launch", program: 'ruby "${file}"' },
{ type: "ruby_lsp", name: "Debug test",   request: "launch", program: 'ruby -Itest "${relativeFile}"' },

Double quotes work on Unix shells, cmd.exe, and PowerShell (with the caveat that PowerShell interpolates $ in double quotes, though this is unlikely to affect file paths in practice).

This only helps users who generate a fresh launch.json; existing configurations remain broken.

Option B — Separate command and file fields with program fallback

The cleanest way to eliminate the parsing ambiguity is to introduce separate command and file fields, while continuing to honour the existing program field for backwards compatibility:

const program = i.program
  ? i.program
  : `${i.command} "${i.file}"`;

The new launch.json template would emit:

{
  "type": "ruby_lsp",
  "name": "Debug script",
  "request": "launch",
  "command": "ruby",
  "file": "${file}"
}

With i.file as a dedicated path field, the extension can quote it correctly without any heuristic parsing. Users with existing program configurations are unaffected — their launch.json keeps working as before.

Option C — Parse the path out of program and quote it (fragile)

Another approach would be to heuristically extract the file path from the existing program string and quote it. Since program always starts with ruby optionally followed by flags, the extension could split on that known prefix and treat the remainder as the path:

const match = program.match(/^(ruby\s+(?:-\S+\s+)*)(.+)$/);
if (match) {
  const cmd = match[1];
  const filePath = match[2];
  if (!filePath.startsWith('"') && !filePath.startsWith("'")) {
    program = `${cmd}"${filePath}"`;
  }
}

While this would fix the common case, it is inherently fragile because the extension is guessing where the command ends and the path begins. It breaks on relative paths (ruby test/foo.rb), commands without a file path (ruby -e 'code'), or any future change to the allowed shape of program. It also adds complexity that would need to be maintained and tested across platforms. For these reasons I'd suggest Option B over this approach — separating the fields eliminates the ambiguity rather than trying to work around it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions