Skip to main content
Skip to main content

Debugger

argsh includes a Debug Adapter Protocol (DAP) server that provides step-through debugging for argsh and bash scripts directly in VSCode. Set breakpoints, step through code, inspect variables, and navigate the call stack — no set -x or echo debugging needed.

How it works

The debugger uses bash's built-in DEBUG trap — no external debugger (bashdb) is required. When you start a debug session:

  1. argsh-dap injects a lightweight debug prelude into your script
  2. The prelude sets up a trap DEBUG handler that checks for breakpoints and step commands
  3. Communication between the debugger and your script happens via named pipes (FIFOs)
  4. VSCode's debug UI (breakpoints, call stack, variables) is powered by the standard DAP protocol

Quick start

  1. Open any .sh or argsh script in VSCode
  2. Set a breakpoint by clicking in the gutter (left of line numbers)
  3. Press F5 (or Run → Start Debugging)
  4. Select argsh: Debug Script if prompted
  5. The script runs and stops at your breakpoints

Launch configuration

Create a .vscode/launch.json in your project:

{
"version": "0.2.0",
"configurations": [
{
"type": "argsh",
"request": "launch",
"name": "Debug script",
"program": "${file}",
"args": ["deploy", "--env", "staging"],
"cwd": "${workspaceFolder}",
"stopOnEntry": true
}
]
}

Configuration options

OptionTypeDefaultDescription
programstringrequiredPath to the script to debug
argsstring[][]Arguments to pass to the script
cwdstring${workspaceFolder}Working directory
envobject{}Environment variables
stopOnEntrybooleantrueStop on the first executable line

Features

Breakpoints

Click in the gutter to set breakpoints. Breakpoints work in the main script and in any file loaded via source or import.

Smart breakpoints (by subcommand name)

Set a breakpoint on a :usage subcommand by name instead of a specific line number. The debugger resolves the name through the namespace chain (main::deploy, deploy::run, argsh::deploy, deploy) to find the target function.

Use Run → New Breakpoint → Function Breakpoint and enter the subcommand name (e.g., deploy). The debugger resolves it to the actual function's file and line, following the same resolution rules as :usage.

This also works across files — if the target function is in an imported module, the resolver follows the import chain to find it.

Stepping

ActionShortcutDescription
ContinueF5Run until the next breakpoint
Step OverF10Execute the current line, step over function calls
Step IntoF11Step into function calls
Step OutShift+F11Run until the current function returns

Call stack

The call stack panel shows the current function call chain with file names and line numbers. argsh's namespace resolution (main::deploy, deploy::run) is preserved in the stack display.

Variable inspection

The Variables panel shows two scopes when stopped at a breakpoint:

  • Locals — runtime variable values from the bash process (planned — not yet implemented in the MVP; will use declare -p via FIFO round-trip)
  • argsh Args — static view of the :args field definitions with types (available now)

argsh Args inspector

When stopped inside a function that uses :args, the Variables panel shows an argsh Args scope with a structured view of the args array definition:

  • Each entry shows the field spec, description, and type
  • Flags are annotated with their modifiers (:+ boolean, :~int typed, :! required)
  • Positional arguments are distinguished from flags

This gives you immediate visibility into what arguments the current function expects without reading the source.

Variable type tooltips

Hover over a variable name while debugging to see its argsh type annotation. For example, hovering over port in a function with 'port|p:~int' shows:

argsh: port|p:~int

This works for all variables that appear in :args definitions — flags, positionals, and typed fields.

Note

Runtime values in hover tooltips are planned for a future release. Currently only the argsh field definition is shown.

Auto-generated launch configurations

The debugger can generate launch configurations for every subcommand path in your script's :usage tree. Each subcommand becomes a separate debug target with the correct args pre-filled.

For a script with subcommands deploy, deploy staging, and status, the debugger generates:

[
{ "name": "Debug: deploy", "args": ["deploy"] },
{ "name": "Debug: deploy staging", "args": ["deploy", "staging"] },
{ "name": "Debug: status", "args": ["status"] }
]

To generate these, use the Debug Console and evaluate argsh.generateLaunchConfigs.

Requirements

  • Bash 4.0+ — for the DEBUG trap and FUNCNAME/BASH_LINENO/BASH_SOURCE arrays
  • VSCode with the argsh extension

No additional dependencies (bashdb, etc.) are needed.

Conditional breakpoints

Right-click a breakpoint in the gutter and select Edit Breakpoint to add a condition. The breakpoint only fires when the condition evaluates to true in bash:

# Stop only when $env equals "production"
[[ "$env" == "production" ]]

# Stop when count exceeds 10
(( count > 10 ))

# Stop when a file exists
[[ -f "/tmp/trigger" ]]

The condition is evaluated via bash eval at each hit — use any valid bash expression.

Watch expressions

Add watch expressions via the Watch panel in the debug sidebar. Expressions are evaluated at each breakpoint stop and their values are displayed alongside the stop event.

Modify variables at runtime

In the Variables panel, double-click a variable value to modify it. The new value is applied immediately in the running bash process via printf -v (safe assignment, no eval). This is useful for testing different code paths without restarting the script.

Subshell debugging

Commands inside $(), pipes (|), ( ... ), and { ...; } & run in subshells. The debugger fully supports debugging inside subshells:

  • Breakpoints work: breakpoints fire inside subshells just like in the main shell
  • Stepping works: step in/over/out work inside subshell code
  • Separate thread: each subshell appears as a separate thread in VSCode's call stack panel
  • No deadlocks: subshells use per-PID control FIFOs so they don't conflict with the parent process. FIFO writes are serialized via flock to prevent interleaving.
  • Automatic cleanup: subshell FIFOs are cleaned up via trap EXIT when the subshell exits
Note

Short-lived subshells like $(echo foo) may complete before the debugger can stop them. Breakpoints work best in longer-running subshells like background jobs ({ ...; } &) or complex pipe segments.

Limitations

  • set -e: The debug trap always returns 0 to avoid triggering set -e exits.
  • Performance: The trap fires on every command. When not at a breakpoint and not stepping, the overhead is minimal (in-memory check only).

Extension settings

SettingTypeDefaultDescription
argsh.dap.pathstring""Path to argsh-dap binary (auto-detected if empty)

Troubleshooting

"Cannot find a script to debug"

Make sure you have a .sh file open in the editor, or specify program explicitly in your launch.json.

Script exits immediately

If your script uses set -e and the debug trap interferes, try adding set +e at the top of your script temporarily, or set breakpoints after the set -e line.

Breakpoints not hit

  • Verify the breakpoint is on an executable line (not a comment, blank line, or then/do/fi keyword)
  • Check that the file path in the breakpoint matches BASH_SOURCE — symlinks may cause mismatches
Was this section helpful?