Repository navigation
feat: process trace — generate markdown execution log (--argsh-trace / ARGSH_TRACE) #99
Copy link
Copy link
Open
Description
Summary
Add a process trace feature that generates a structured, readable markdown file documenting a script's execution flow — every function call, :args/:usage dispatch, variable state, and timing.
Two activation methods:
- Env var (zero deps):
ARGSH_TRACE=/path/out.md myscript deploy --env staging - Binary wrapper (rich, argsh-aware):
argsh-dap --trace out.md -- myscript deploy --env staging
If the binary isn't available locally, the env var path forwards to the Docker container (same pattern as argsh lint, argsh test, etc.).
Use cases
- CI debugging: attach trace to failing pipeline runs
- Documentation: auto-generate "how this script works" docs from a real execution
- Code review: show reviewers the actual execution path of a change
- Onboarding: new team members can see what a script does without reading all the code
- Bug reports: attach a trace showing exactly what happened
Activation
Method 1: Environment variable (pure bash)
ARGSH_TRACE=./trace.md myscript deploy --env staging- Intercepted early in
:args/:usage(before user code runs) - Sets up a
trap DEBUGhandler that writes markdown - Pure bash — no binary dependency
- Output: functional but basic (no argsh type analysis)
Method 2: Binary wrapper (full analysis)
argsh-dap --trace ./trace.md -- myscript deploy --env staging- Uses the existing DAP debug prelude + FIFO infrastructure
- Runs headless (no VSCode, no DAP protocol — just collects data and writes markdown)
- Has full
argsh_syntaxanalysis: field types,:usagetree, import resolution - Output: enriched with argsh-specific annotations
If argsh-dap is not installed, falls back to Docker:
docker run --rm -v "$(pwd):/workspace" -w /workspace \
-e ARGSH_TRACE=/workspace/trace.md \
ghcr.io/arg-sh/argsh:latest myscript deploy --env stagingMarkdown output structure
# Process Trace: myscript deploy --env staging
> Generated: 2026-04-18 12:00:00
> Script: `/path/to/myscript`
> Exit code: 0 | Duration: 1.2s | Steps: 47
---
## Command Tree
Shows the `:usage` dispatch path taken during this execution:
myscript
└── deploy
├── :args "Deploy to environment"
│ ├── env|e:! = "staging" (required flag, --env staging)
│ ├── verbose|v:+ = 0 (boolean flag, default)
│ └── target = "all" (positional, default)
└── :usage "Deploy commands"
└── run → deploy::run
---
## Execution
### → `main` (myscript:3)
| # | Line | Command | Duration |
|---|------|---------|----------|
| 1 | 5 | `:args "Deploy" "${@}"` | <1ms |
| 2 | 7 | `deploy::validate` | 2ms |
| 3 | 12 | `echo "Deploying to staging"` | <1ms |
<details>
<summary>Variables after :args (line 5)</summary>
| Variable | Value | Type | Source |
|----------|-------|------|--------|
| `env` | `"staging"` | string :! | `--env staging` |
| `verbose` | `0` | boolean :+ | default |
| `target` | `"all"` | string | positional default |
</details>
### → `deploy::validate` (deploy.sh:20)
| # | Line | Command | Duration |
|---|------|---------|----------|
| 4 | 22 | `[[ -f "${config}" ]]` | <1ms |
| 5 | 23 | `source "${config}"` | 1ms |
### ← `deploy::validate` returned (exit: 0)
### → `deploy::run` (deploy.sh:35)
| # | Line | Command | Duration |
|---|------|---------|----------|
| 6 | 37 | `:args "Run deployment" "${@}"` | <1ms |
| 7 | 40 | `kubectl apply -f "${manifest}"` | 850ms |
<details>
<summary>Variables after :args (line 37)</summary>
| Variable | Value | Type | Source |
|----------|-------|------|--------|
| `manifest` | `"k8s/deploy.yaml"` | file :~file | positional |
</details>
### ← `deploy::run` returned (exit: 0)
### ← `main` returned (exit: 0)
---
## Import Tree
Files loaded via `import` or `source` during execution:
| Module | Path | Loaded at |
|--------|------|-----------|
| `deploy` | `./lib/deploy.sh` | main:4 |
| `utils` | `./lib/utils.sh` | deploy.sh:2 |
---
## Summary
| Metric | Value |
|--------|-------|
| Total steps | 47 |
| Functions called | 5 |
| `:args` parsed | 2 |
| `:usage` dispatched | 1 |
| Imports loaded | 2 |
| Exit code | 0 |
| Wall time | 1.2s |
## Final Variable State
<details>
<summary>All variables at exit</summary>
```bash
declare -- env="staging"
declare -- verbose="0"
declare -- target="all"
declare -- manifest="k8s/deploy.yaml"
Implementation plan
Phase 1: Env var (pure bash)
- Intercept
ARGSH_TRACEin:argsand:usage— set up DEBUG trap - DEBUG trap writes markdown to the trace file incrementally
- Track: function entry/exit,
:args/:usagecalls, variable state after:args - EXIT trap writes summary section
- Keep the trace handler lightweight — only write on function boundaries and
:args/:usage, not every command
Phase 2: Binary wrapper (argsh-dap --trace)
- Add
--trace <path> -- <script> [args...]mode toargsh-dap - Reuse the DEBUG trap prelude (already exists for DAP debugging)
- Run headless: no stdin/stdout DAP protocol, just collect events from FIFO
- After script exits, use
argsh_syntaxanalysis to enrich the trace:- Add field type annotations to variable tables
- Build the command tree section from
:usageanalysis - Add import tree from resolver
- Write the final markdown file
Phase 3: Docker fallback
- If
argsh-dapis not on PATH, check if Docker is available - Forward the trace invocation to the argsh Docker container
- Mount the output path so the trace file lands on the host
Files to change
| File | Change |
|---|---|
libraries/args.sh |
Detect ARGSH_TRACE env var, set up DEBUG trap |
builtin/src/args.rs |
Same detection in the Rust builtin |
crates/argsh-lsp/src/bin/argsh-dap.rs |
Add --trace headless mode |
docs/development/tools/debugger.mdx |
Document the trace feature |
libraries/main.bats |
Tests for env var trace |
crates/argsh-lsp/tests/dap_integration.rs |
Tests for --trace mode |
Not in scope
- Reverse debugging (see todos/dap-subshell-and-reverse.md)
- Interactive trace (that's the DAP debugger)
- Trace filtering (only trace specific functions) — future enhancement
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
feat: process trace via ARGSH_TRACE env var (Phase 1) (#168)