A Go library and CLI for executing Windows binaries from WSL with pure Go path translation, codepage sanitization, interactive PTY support, environment bridging, shim generation, and bounded concurrency.
Running Windows binaries from WSL via os/exec works, but lacks:
- Path translation — Linux paths like
./file.txtdon't resolve on the Windows side - Encoding handling — Windows tools output CP1252/UTF-16LE, Go assumes UTF-8
- Interactive support — REPLs like
python.exeand TUI apps break with buffered I/O - Environment bridging — Windows processes don't inherit Linux env vars without
WSLENV - Signal propagation — Ctrl+C in your terminal won't kill the Windows child process
- Concurrency control — Spawning dozens of Windows processes simultaneously is expensive
GoWinBridge solves all of these behind a clean Go API and a ready-to-use CLI.
cmd/winrun/ CLI entry point (flags, signal handling, shim subcommand)
│
├── pkg/workerpool/ Bounded worker pool (configurable concurrency)
│ │
│ └── pkg/bridge/ Core executor
│ ├── exec.go CommandContext, .exe resolution, buffered + interactive modes
│ ├── encoding.go CP1252/UTF-16LE/BE → UTF-8 decoder middleware
│ ├── env.go WSLENV formatting with value-based heuristics
│ └── config.go CommandConfig / Output types
│
└── internal/wsl/ WSL detection & path translation
├── detect.go WSL1 vs WSL2 detection (cached singleton)
└── path.go Pure Go path resolver (/proc/mounts + string manipulation)
curl -sL https://raw.githubusercontent.com/sibikrish3000/gowinbridge/main/install.sh | sh# Clone
git clone https://github.com/sibikrish3000/gowinbridge.git
cd gowinbridge
# Build the CLI
go build -o winrun ./cmd/winrun
# Or install via Go
go install github.com/sibikrish3000/gowinbridge/cmd/winrun@latest# Basic execution
winrun -- cmd.exe /c echo hello
# Auto-convert Linux paths to Windows paths
winrun --convert-paths -- cmd.exe /c type ./go.mod
# Handle Windows codepage output (legacy tools outputting CP1252)
winrun --encoding cp1252 -- cmd.exe /c chcp
# Interactive mode for REPLs (auto-detected for python, node, mysql, etc.)
winrun --interactive -- python.exe
# Environment variable tunneling (flags auto-detected from values)
winrun --env MY_VAR=hello --env MY_PATH=/home/user --tunnel-env -- cmd.exe /c echo %MY_VAR%
# Concurrent execution with timeout
winrun --concurrency 4 --timeout 30s -- powershell.exe -Command Get-Process
# Version info
winrun --version| Flag | Default | Description |
|---|---|---|
--concurrency N |
NumCPU |
Max concurrent Windows process executions |
--convert-paths |
false |
Auto-detect and convert file path arguments to Windows format |
--encoding ENC |
"" (UTF-8) |
Output encoding: utf8, cp1252, utf16le, utf16be, auto |
--interactive |
false |
Run in interactive/PTY mode (bypasses output capture) |
--env KEY=VAL |
— | Set environment variable (repeatable) |
--tunnel-env |
false |
Enable WSLENV tunneling for --env vars |
--timeout DURATION |
0 (none) |
Max execution time (e.g., 30s, 5m) |
--version |
— | Print version information and exit |
Create transparent wrapper scripts so Windows tools behave like native Linux binaries:
# Install a shim — creates ~/.local/bin/docker that proxies to docker.exe
winrun shim install docker.exe --as docker
# Install with custom bin directory
winrun shim install code.exe --as code --bin-dir /usr/local/bin
# Auto-derive name from binary (docker.exe → docker)
winrun shim install docker.exe
# List installed shims
winrun shim list
# Remove a shim
winrun shim remove dockerEach shim is a shell script:
#!/bin/sh
# Generated by winrun shim install
# Binary: docker.exe
exec winrun --convert-paths -- docker.exe "$@"package main
import (
"context"
"fmt"
"log"
"github.com/sibikrish3000/gowinbridge/pkg/bridge"
)
func main() {
output, err := bridge.Execute(context.Background(), bridge.CommandConfig{
Command: "cmd.exe",
Args: []string{"/c", "echo", "hello from Windows"},
ConvertPaths: false,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(output.Stdout)
// Output: hello from Windows
}output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "cmd.exe",
Args: []string{"/c", "type", "./myfile.txt"},
ConvertPaths: true, // ./myfile.txt → C:\Users\...\myfile.txt
Env: map[string]string{
"MY_VAR": "value", // auto-detected as /u (plain string)
"MY_PATH": "/home/user", // auto-detected as /p (path)
},
EnvTunneling: true, // Generates WSLENV=MY_PATH/p:MY_VAR/u
Timeout: 30 * time.Second,
})output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "legacy_tool.exe",
Args: []string{"/report"},
Encoding: "cp1252", // Decode CP1252 output to UTF-8
})
// output.Stdout is now valid UTF-8, even if the tool outputs "café"output, err := bridge.Execute(ctx, bridge.CommandConfig{
Command: "python.exe",
Interactive: true, // Direct stdin/stdout/stderr copy
Stdin: os.Stdin, // Pass through terminal input
})import "github.com/sibikrish3000/gowinbridge/pkg/workerpool"
pool := workerpool.NewPool(4, bridge.Execute)
pool.Submit(bridge.CommandConfig{Command: "build1.exe"})
pool.Submit(bridge.CommandConfig{Command: "build2.exe"})
pool.Submit(bridge.CommandConfig{Command: "build3.exe"})
go pool.Shutdown()
for result := range pool.Results() {
if result.Err != nil {
log.Printf("Error: %v", result.Err)
continue
}
fmt.Printf("[%s] %s\n", result.Config.Command, result.Output.Stdout)
}import "github.com/sibikrish3000/gowinbridge/internal/wsl"
if !wsl.IsWSL() {
log.Fatal("This tool requires WSL")
}
fmt.Printf("Running on WSL%d\n", wsl.DetectWSLVersion())import "github.com/sibikrish3000/gowinbridge/internal/wsl"
// Linux → Windows (no subprocess, <1µs)
winPath, _ := wsl.ToWindowsPath("/mnt/c/Users/test")
// winPath = "C:\Users\test"
// Non-mount paths get UNC notation
uncPath, _ := wsl.ToWindowsPath("/home/user/project")
// uncPath = "\\wsl.localhost\Ubuntu\home\user\project"
// Windows → Linux
linPath, _ := wsl.ToLinuxPath(`C:\Users\test`)
// linPath = "/mnt/c/Users/test"- Go 1.21+
- WSL (for integration tests; unit tests run anywhere)
.
├── cmd/winrun/ CLI tool
│ ├── main.go Entry point, flag parsing, shim dispatch
│ ├── shim.go Shim install/list/remove subcommands
│ └── shim_test.go
├── internal/wsl/ WSL detection & path translation (private)
│ ├── detect.go
│ ├── detect_test.go
│ ├── path.go Pure Go resolver (/proc/mounts parsing)
│ └── path_test.go
├── pkg/bridge/ Core executor (public API)
│ ├── config.go CommandConfig / Output types
│ ├── encoding.go CP1252/UTF-16LE/BE decoder middleware
│ ├── encoding_test.go
│ ├── env.go WSLENV formatting with value-based heuristics
│ ├── env_test.go
│ ├── exec.go Buffered + interactive execution modes
│ └── exec_test.go
├── pkg/workerpool/ Bounded concurrency pool (public API)
│ ├── pool.go
│ └── pool_test.go
├── go.mod
├── go.sum
└── README.md
# All unit tests (works on any OS — WSL integration tests auto-skip)
go test ./... -v
# With race detector
go test ./... -race
# Short / unit tests only
go test ./... -short| Decision | Rationale |
|---|---|
| Pure Go path resolver | Parses /proc/mounts once; eliminates wslpath subprocess overhead (~20ms → <1µs) |
| Value-based WSLENV heuristics | inferWSLEnvFlag inspects values (not just key names) to auto-select /p, /l, /u |
| Dual execution modes | Buffered (Scanner) for output capture; interactive (io.Copy) for REPLs and TUI apps |
| Encoding middleware | transform.Reader wraps stdio pipes to decode CP1252/UTF-16 transparently before Scanner |
sync.Once for WSL detection |
Avoids repeated /proc/version reads; cached after first call |
sync.Map for path cache |
Memoizes resolved paths; concurrent-safe without locks |
exec.CommandContext |
Ensures context cancellation (timeout / SIGINT) kills the Windows process |
.exe auto-resolution |
Appends .exe and checks PATH if the user passes cmd instead of cmd.exe |
| Worker pool with injectable executor | Testable concurrency engine; mock executor eliminates WSL dependency in tests |
| Shim scripts with marker comments | Safe identification and removal; prevents accidental deletion of non-shim files |
- Binary names: Always use
.exesuffix (e.g.,cmd.exe, notcmd). The library attempts auto-resolution but explicit is better. - Path separators: Windows uses
\. The library handles this via the pure Go resolver, but be careful with manual string building. - Zombie processes: The CLI registers
SIGINT/SIGTERMhandlers to cancel all in-flight Windows processes on exit. - WSLENV: Only works for environment variables you explicitly pass — it does not auto-export your entire shell environment.
- Encoding: If unsure about the encoding, use
--encoding autofor BOM-based detection, or--encoding cp1252for legacy Western European tools. - Interactive mode: Auto-detected for
python,node,mysql,psql,irb,bash. Use--interactiveexplicitly for other REPLs. - Shim PATH: Ensure
~/.local/binis in your$PATH(addexport PATH="$HOME/.local/bin:$PATH"to your shell profile).
GNU GENERAL PUBLIC LICENSE