bgexec — Execute pipelines with full I/O control

NAME

bgexec — Run one or more processes in the foreground or background, collecting stdout and stderr into Tcl variables and/or callbacks.


SYNOPSIS

bgexec statVar ?options? program ?arg ...? ?| program ?arg ...?? ... ?&?

DESCRIPTION

bgexec runs one or more child processes (a pipeline) and collects their output. It is closely related to the built-in exec command but offers several important differences:

Foreground execution

Without & or -detach yes, bgexec blocks until all processes in the pipeline exit, running the Tcl event loop internally so that timers, file events, and -onoutput/-onerror callbacks still fire. The return value is the empty string on success. If any process exits with a non-zero code (and -ignoreexitcode is not set), a Tcl error is raised whose message is the content of stderr.

Background execution

With & or -detach yes, bgexec returns immediately with a Tcl list of the process IDs in the pipeline. statVar is set when all processes finish.


ARGUMENTS

statVar

The name of a global Tcl variable. When all processes in the pipeline have exited, statVar is set to a five-element list:

{status pid code msg description}
Field Meaning
status EXITED, KILLED, STOPPED, or UNKNOWN
pid PID of the last process in the pipeline
code Exit code (0 on clean exit, −1 on signal)
msg Short status message
description Human-readable description

A Tcl variable trace on statVar is the idiomatic way to react to pipeline completion in background mode.

Important: setting statVar while a bgexec is running sends the kill signal to the child pipeline. This means you must use a distinct statVar for each concurrent bgexec — if two bgexec instances share the same variable and you set it to kill one, the other will be killed too. This applies equally to vwait: vwait statVar returns as soon as any bgexec watching that variable finishes, which may not be the one you intended.

program ?arg ...?

The program to execute and its arguments. Multiple programs separated by | form a pipeline; stdout of each program is connected to stdin of the next.

&

An optional trailing & (as a separate argument) runs the pipeline detached, equivalent to -detach yes.


I/O REDIRECTIONS

Shell-style redirections may appear anywhere among the program arguments. They apply to the child processes, not to the Tcl interpreter.

Syntax Effect
< filename Connect child stdin to filename
<@ channel Connect child stdin to an open Tcl channel
<< string Write string to a temp file and connect it to child stdin
> filename Redirect child stdout to filename (truncate)
>> filename Redirect child stdout to filename (append)
2> filename Redirect child stderr to filename (truncate)
2>> filename Redirect child stderr to filename (append)
>& filename Redirect both stdout and stderr to filename (truncate)
>>& filename Redirect both stdout and stderr to filename (append)
2>@1 Merge child stderr into child stdout (must be last argument)
>@ channel Redirect child stdout to an open Tcl channel
2>@ channel Redirect child stderr to an open Tcl channel

When stdout or stderr is redirected to a file, that stream is not collected into the corresponding Tcl variable. When both streams are redirected to the same filename with append mode (>> and 2>>), bgexec shares a single file descriptor between them so that writes are serialised and no data is lost.


OPTIONS

All options must appear before the program name.


-decodeerror encodingName

Encoding used to translate raw bytes from child stderr into a Tcl string. Accepts any encoding name recognised by encoding names, plus the special values:

Value Meaning
"" (empty) Use the system default encoding
binary No translation; treat bytes as-is

Default: system encoding.

Example — read UTF-8 stderr from a Python script:

bgexec status \
    -decodeerror utf-8 \
    -errorvariable errText \
    python3 my_script.py

puts $errText

-decodeoutput encodingName

Same as -decodeerror but applies to child stdout.

Example — collect Latin-1 output:

bgexec status \
    -decodeoutput iso8859-1 \
    -outputvariable result \
    legacy_program

puts $result

-detach bool

If yes, start the pipeline and return immediately without waiting for it to finish. The return value is the list of PIDs. statVar is set when all processes exit.

Equivalent to appending & as the last argument.

Default: no

Note: in detached mode bgexec returns immediately but the child processes continue running in the background. If your script exits before they finish, the children become orphans. Use vwait statVar or a variable trace to ensure you wait for completion when that matters.

Example — fire and forget, react via variable trace:

proc pipelineDone {name1 name2 op} {
    global pipeStatus
    puts "Pipeline finished: $pipeStatus"
}

trace add variable pipeStatus write pipelineDone

set pids [bgexec pipeStatus -detach yes \
              long_running_job --arg value]

puts "Started PIDs: $pids"
vwait pipeStatus     ;# wait in event loop if needed

-echo echoValue

Controls which streams are echoed (written) to the terminal in real time as data arrives, in addition to being collected.

Value Effect
none No echoing (default)
output Echo child stdout to Tcl stdout
error Echo child stderr to Tcl stderr
both Echo both streams

Default: none

Example — show progress while also capturing output:

bgexec status \
    -echo both \
    -outputvariable result \
    make -j4

puts "\n--- build output captured ---"
puts $result

Tcler's Wiki standalone compatibility note:

The Wiki standalone bgexec accepted -echo as a boolean (true/false or 1/0), where true meant echo stdout only. bgexec 3.5 does not accept a boolean for -echo — passing true or 1 will raise an error.

If you have scripts using -echo true, replace as follows:

Wiki standalone bgexec 3.5 equivalent
-echo true -echo output
-echo false -echo none (or omit entirely)
-echo 1 -echo output
-echo 0 -echo none (or omit entirely)

If you need to echo both stdout and stderr (which the Wiki version could not do), use -echo both.

If you need a drop-in wrapper that accepts a boolean -echo and translates it transparently, you can define one in pure Tcl:

proc bgexec_compat {args} {
    set newargs {}
    set i 0
    while {$i < [llength $args]} {
        set opt [lindex $args $i]
        if {$opt eq "-echo"} {
            incr i
            set val [lindex $args $i]
            # Translate boolean to bgexec 3.5 enum
            if {$val in {true 1 yes}} {
                lappend newargs -echo output
            } elseif {$val in {false 0 no}} {
                lappend newargs -echo none
            } else {
                # Already a valid 3.1 value (none/output/error/both)
                lappend newargs -echo $val
            }
        } else {
            lappend newargs $opt
        }
        incr i
    }
    tailcall bgexec {*}$newargs
}

-environ list

Specifies the environment for child processes as a flat key–value list:

{KEY1 value1 KEY2 value2 ...}

Default: inherit parent environment.

Example — run a command with a clean, minimal environment:

bgexec status \
    -environ {PATH /usr/bin HOME /tmp LANG C} \
    -outputvariable out \
    env

puts $out   ;# only PATH, HOME, LANG will appear

-errorvariable varName

Alias: -error (Tcler's Wiki standalone bgexec compatibility)

Name of a global variable to set to the complete stderr output of the pipeline when all processes finish. Trailing newlines are stripped unless -keepnewline yes is set.

Default: not set (stderr is discarded unless also redirected).

Example — separate stdout and stderr:

bgexec status \
    -outputvariable out \
    -errorvariable  err \
    sh -c {echo hello; echo oops >&2}

puts "stdout: $out"
puts "stderr: $err"

-ignoreexitcode bool

If yes, a non-zero exit status from any process does not cause bgexec to raise a Tcl error. The exit code is still recorded in statVar.

Default: no

Example — run a command that intentionally exits non-zero:

bgexec status \
    -ignoreexitcode yes \
    -outputvariable out \
    grep "pattern" file.txt   ;# grep exits 1 when no match found

puts "grep output: '$out'"
puts "status: $status"

-input varName

Creates a pipe connected to child stdin and stores the writable Tcl channel name in varName. The caller can then write to that channel and close it to signal EOF to the child.

This option and shell < / <@ / << redirections are mutually exclusive.

Default: not set (child stdin is /dev/null or equivalent).

Important usage notes:

tcl bgexec status -detach yes -input ::ch $tclsh files/cat.tcl puts $::ch "hello" ;# $::ch is "file4" — use it directly close $::ch

Example — feed data to a child process:

set ::writeChan {}
bgexec status \
    -detach yes \
    -input ::writeChan \
    -outputvariable result \
    $tclsh files/cat.tcl

puts $::writeChan "line one"
puts $::writeChan "line two"
close $::writeChan   ;# signals EOF so the child can exit

vwait status
puts $result         ;# "line one\nline two"

Example — concurrent bgexec instances must use distinct status variables:

# WRONG — both share ::myVar; killing job1 also kills job2
set ::ch1 {} ; set ::ch2 {}
bgexec ::myVar -detach yes -input ::ch1 -outputvariable out1 cat &
bgexec ::myVar -detach yes -input ::ch2 -outputvariable out2 cat &
set ::myVar die   ;# kills BOTH children

# CORRECT — separate status variables
set ::ch1 {} ; set ::ch2 {}
bgexec ::status1 -detach yes -input ::ch1 -outputvariable out1 cat
bgexec ::status2 -detach yes -input ::ch2 -outputvariable out2 cat
close $::ch1 ; vwait ::status1
close $::ch2 ; vwait ::status2

-keepnewline bool

If yes, the trailing newline is preserved in the collected output stored in -outputvariable, -errorvariable, -lastoutputvariable, and -lasterrorvariable. Normally the final newline is stripped (matching the behaviour of exec).

Default: no

Example:

bgexec status \
    -keepnewline yes \
    -outputvariable out \
    echo hello

# $out is "hello\n" not "hello"
puts -nonewline $out
puts "(end)"

-killsignal signal

The signal sent to all processes in the pipeline if statVar is unset (by a trace unset) while processes are still running. Accepts either a signal number or a name (with or without the SIG prefix, e.g. TERM, SIGTERM, 15).

Default: SIGKILL (signal 9)

Note: SIGKILL cannot be caught or ignored by the child — the process is terminated immediately with no chance to clean up. If your child process writes temporary files, holds locks, or maintains a network connection, use SIGTERM instead and give the child time to shut down gracefully before resorting to SIGKILL.

Example — use SIGTERM for graceful shutdown:

trace add variable pipeStatus write pipelineDone

set pids [bgexec pipeStatus \
    -detach yes \
    -killsignal SIGTERM \
    long_running_server --port 8080]

# Later, to kill gracefully:
unset pipeStatus

-lasterrorvariable varName

Alias: -lasterror (Tcler's Wiki standalone bgexec compatibility)

Name of a global variable updated with only the most recent chunk of stderr data as it arrives from the child. Unlike -errorvariable (which accumulates all output), this variable is overwritten with each new chunk.

Useful for displaying a live "last stderr line" in a status bar.

Default: not set.

Example — show the most recent log line:

trace add variable lastErr write {
    apply {{n1 n2 op} {
        global lastErr
        .statusbar configure -text $lastErr
    }}
}

bgexec status \
    -detach yes \
    -lasterrorvariable lastErr \
    long_job 2>&1

-lastoutputvariable varName

Alias: -lastoutput (Tcler's Wiki standalone bgexec compatibility)

Same as -lasterrorvariable but for stdout chunks.

Example — progress monitor:

trace add variable lastOut write {
    apply {{n1 n2 op} {
        global lastOut
        puts -nonewline "\r$lastOut"
        flush stdout
    }}
}

bgexec status \
    -detach yes \
    -lastoutputvariable lastOut \
    wget -q --show-progress http://example.com/file.iso

-linebuffered bool

If yes, the -onoutput / -onerror callbacks and the -lastoutputvariable / -lasterrorvariable updates are triggered after each complete line of output rather than after each read chunk.

Default: no

Example — process output line by line:

proc handleLine {data} {
    puts "got line: $data"
}

bgexec status \
    -linebuffered yes \
    -onoutput handleLine \
    tail -f /var/log/syslog

Note on pipe buffering: When a child process writes to a pipe (rather than a terminal), the C library switches to fully unbuffered mode, so each fprintf() call arrives as a separate read event — individual words or tokens rather than complete lines. bgexec 3.5 correctly accumulates these fragments in its internal buffer and only fires the callback once a complete \n-terminated line has been received, regardless of how the child has split its writes. Earlier versions (3.2–3.4) had a bug where the buffer was discarded between reads, causing callbacks to fire on every fragment regardless of this setting.


-onerror cmdPrefix

A Tcl command prefix invoked each time new data arrives from child stderr. The collected data since the last call is appended as a single argument. If -linebuffered yes is set, the callback fires once per complete line.

Note: callbacks only fire while the Tcl event loop is running. In foreground mode bgexec runs the event loop internally, so callbacks work automatically. In detached mode (-detach yes) you must keep the event loop running yourself — typically via vwait, tkwait, or update — otherwise no callbacks will fire until control returns to the event loop.

Default: not set.

Example — log stderr to a widget:

proc appendErr {data} {
    .logwidget insert end $data
    .logwidget see end
}

bgexec status \
    -detach yes \
    -onerror appendErr \
    make 2>&1 1>/dev/null

-onoutput cmdPrefix

Same as -onerror but invoked for child stdout data.

Example — parse streaming JSON lines:

proc handleJson {chunk} {
    foreach line [split $chunk \n] {
        if {$line eq ""} continue
        set obj [json::decode $line]
        processObject $obj
    }
}

bgexec status \
    -detach yes \
    -onoutput handleJson \
    streaming_server --format jsonlines

-outputvariable varName

Alias: -output (Tcler's Wiki standalone bgexec compatibility)

Name of a global variable set to the complete stdout of the pipeline when all processes finish. Trailing newlines are stripped unless -keepnewline yes.

Note: the variable is only written once, when the pipeline finishes. It is not updated incrementally as data arrives. If you need to react to output as it comes in, use -onoutput or -lastoutputvariable instead.

Default: not set (stdout is still captured internally to detect errors, but the data is discarded unless another stdout sink is active).

Example:

bgexec status \
    -outputvariable result \
    date

puts "The date is: $result"

-poll milliseconds

Interval in milliseconds at which bgexec polls for child process exit when no pipe file-event handler is active (e.g. both stdout and stderr are redirected to files). A lower value gives faster response at the cost of more CPU.

Default: 1000 (foreground), 100 (detached).

Example — detect exit quickly when output is redirected:

bgexec status \
    -poll 50 \
    -outputvariable dummy \
    $tclsh script.tcl > output.txt 2> errors.txt

-session bool

(Unix only — PTY not yet implemented in this build; accepted and treated as -detach yes.)

If yes, the pipeline is started as a new session leader (setsid), with a pseudo-terminal (PTY) as its controlling terminal. Intended for programs that require a TTY (e.g. interactive programs, sudo, ssh).

Default: no


-tty bool

(Unix only — PTY not yet implemented in this build; accepted and treated as -detach yes.)

If yes, allocates a controlling TTY for the child process. Implies -detach yes.

Default: no


-updatevariable varName

Alias: -update (Tcler's Wiki standalone bgexec compatibility)

Alias for -lastoutputvariable. Provided for compatibility.


RETURN VALUE


THE statVar LIST

When all processes have exited, statVar is set to a five-element list. Example values:

# Clean exit, PID 12345, exit code 0
EXITED 12345 0 {child completed normally} {}

# Killed by SIGTERM (signal 15)
KILLED 12345 -1 {SIGTERM} {terminated}

# Stopped by SIGTSTP
STOPPED 12345 -1 {SIGTSTP} {suspended}

COMPLETE EXAMPLES

1. Simple synchronous capture

bgexec status \
    -outputvariable out \
    -errorvariable  err \
    ls -la /tmp

if {[lindex $status 2] != 0} {
    puts "Error: $err"
} else {
    puts $out
}

2. Background pipeline with completion callback

proc done {n1 n2 op} {
    global compressStatus
    set code [lindex $compressStatus 2]
    if {$code == 0} {
        puts "Compression complete."
    } else {
        puts "Compression failed (code $code)."
    }
}

trace add variable compressStatus write done

set pids [bgexec compressStatus \
    -detach yes \
    -outputvariable compressLog \
    gzip --best largefile.tar &]

puts "Compressing in background, PIDs: $pids"
vwait compressStatus
# In a Tk application, the event loop is already running (via [tk mainloop])
# so you can omit vwait and simply let the trace callback fire on its own.

3. Live output in a Tk text widget

proc appendOutput {data} {
    .output insert end $data
    .output see end
}

proc appendError {data} {
    .output insert end $data err
    .output see end
}

bgexec buildStatus \
    -detach yes \
    -linebuffered yes \
    -onoutput appendOutput \
    -onerror  appendError \
    make -C /path/to/project all
# Note: -linebuffered controls how bgexec delivers data to callbacks, but
# the child process must also be writing line-by-line for this to work as
# expected.  If the child fully-buffers its stdout (common when stdout is
# not a TTY), output may arrive in large chunks regardless.  For such
# programs, running them via "unbuffer" (expect package) or setting
# PYTHONUNBUFFERED=1 / stdbuf -oL can help.

4. Multi-process pipeline

# Sort the 10 most common words in a file (Unix)
bgexec status \
    -outputvariable result \
    tr -cs {A-Za-z} {\n} < /usr/share/dict/words \
    | sort \
    | uniq -c \
    | sort -rn \
    | head -10

puts $result
# Windows equivalent — use real executables, not shell built-ins.
# sort.exe ships with Windows at C:/Windows/System32/sort.exe.
# For uniq/head, use Tcl itself as a filter stage.
bgexec status \
    -outputvariable result \
    cmd.exe /c type C:\\Windows\\System32\\drivers\\etc\\hosts \
    | sort.exe

puts $result

5. Input pipe — feeding a child process interactively

# Start a child that reads from stdin; we write to it asynchronously.
# -detach yes is REQUIRED — without it bgexec blocks before returning
# the channel name and the script deadlocks.
bgexec status \
    -detach yes \
    -input  stdinChan \
    -outputvariable result \
    cat

# stdinChan contains the channel name as a string — retrieve it
set ch $stdinChan
fconfigure $ch -buffering line

puts $ch "hello from Tcl"
puts $ch "second line"

# Close BEFORE vwait — many programs (cat, sort, wc ...) won't produce
# output or exit until they see EOF on stdin.  Forgetting this close
# causes vwait to block forever.
close $ch

vwait status
puts "Child output: $result"

Feeding a child process from a variable (one shot):

set inputData "apple\nbanana\ncherry\n"

bgexec status \
    -detach yes \
    -input  stdinChan \
    -outputvariable result \
    sort

set ch $stdinChan
puts -nonewline $ch $inputData
close $ch       ;# EOF — sort can now produce output

vwait status
puts $result    ;# apple\nbanana\ncherry (sorted)

Multiple concurrent bgexec instances — use distinct status variables:

# Each bgexec must have its own status variable.  If two bgexec instances
# share the same status variable, writing to it (e.g. to kill one) sends
# SIGTERM to ALL children watching that variable simultaneously.

bgexec ::s1 -detach yes -input ch1 -outputvariable out1 cat
bgexec ::s2 -detach yes -input ch2 -outputvariable out2 cat

puts $ch1 "data for job 1" ; close $ch1
puts $ch2 "data for job 2" ; close $ch2

vwait ::s1
vwait ::s2
puts "Job 1: $out1"
puts "Job 2: $out2"

6. Custom environment

# Run a script with only specific environment variables
bgexec status \
    -environ {
        PATH    /usr/local/bin:/usr/bin:/bin
        HOME    /tmp
        TMPDIR  /tmp
        LANG    en_US.UTF-8
    } \
    -outputvariable out \
    my_script.sh

puts $out

7. File redirections

# Redirect stdin from a file, stderr to a log, stdout captured in variable
bgexec status \
    -outputvariable result \
    my_program < input.dat 2> errors.log

puts "Result: $result"
set f [open errors.log]
puts "Errors: [read $f]"
close $f

8. Merge stderr into stdout (2>@1)

# Collect interleaved stdout+stderr in one variable
bgexec status \
    -outputvariable combined \
    sh -c {echo out; echo err >&2; echo out2} 2>@1

puts $combined
# Output: out\nerr\nout2  (order reflects actual write order)

9. Append both streams to one log file

# Both stderr and stdout appended to the same file, serialised.
# bgexec shares a single file descriptor when both streams are appended
# to the same filename, so writes from stdout and stderr are interleaved
# correctly without data loss.  This only applies within one bgexec call;
# if multiple bgexec instances append to the same file concurrently, their
# writes may interleave unpredictably.
bgexec status \
    my_program 2>> run.log >> run.log

set f [open run.log]
puts [read $f]
close $f

10. Kill on variable unset — graceful shutdown

proc onExit {n1 n2 op} {
    puts "Server stopped."
}

trace add variable serverStatus write onExit

set pids [bgexec serverStatus \
    -detach     yes \
    -killsignal SIGTERM \
    ./my_server --port 9090]

after 5000 {unset serverStatus}   ;# shut down after 5 seconds
vwait serverStatus

VARIABLE SCOPE

All variable name arguments accepted by bgexec — statVar, -outputvariable, -errorvariable, -updatevariable, -lastoutputvariable, -lasterrorvariable, and -input — must refer to global variables.

This is a fundamental consequence of how bgexec works: it registers file-event handlers and timer callbacks with the Tcl event loop, and those callbacks fire from the event loop's top-level context, which has no access to any procedure's local variable frame. The C code therefore always uses TCL_GLOBAL_ONLY when reading or writing these variables.

Correct usage from inside a proc:

proc runPipeline {} {
    global pipeStatus myOutput
    set pipeStatus {}
    set myOutput {}
    bgexec pipeStatus -outputvariable myOutput $tclsh myscript.tcl
}

Correct usage with explicit :: namespace prefix:

proc runPipeline {} {
    set ::pipeStatus {}
    set ::myOutput {}
    bgexec ::pipeStatus -outputvariable ::myOutput $tclsh myscript.tcl
}

Passing a bare variable name that is local to a proc or catch body will not work — bgexec will update the global ::varName but the local slot in the proc frame remains unchanged, and vwait varName inside the proc will wait forever because the local variable is never modified.

The same rule applies to -input: the variable name passed receives the writable channel handle via TCL_GLOBAL_ONLY, so it must be a global or use the :: prefix. See the -input option description and examples for the correct pattern.


TCLER'S WIKI STANDALONE COMPATIBILITY

bgexec 3.5 is a drop-in replacement for the Tcler's Wiki standalone bgexec with the following notes:

Option aliases

The following short option names from the Wiki standalone are accepted as direct aliases — no script changes required:

Wiki standalone bgexec 3.5 canonical
-output -outputvariable
-error -errorvariable
-lastoutput -lastoutputvariable
-lasterror -lasterrorvariable
-update -updatevariable

Both forms are accepted interchangeably. If both forms are passed in the same call, the last one wins (standard Tcl option behaviour).

statVar format difference

The Wiki standalone typically set statVar to a plain exit code integer (e.g. 0). bgexec 3.5 sets it to a five-element BLT-format list:

EXITED 12345 0 {child completed normally} {}

If your script checks if {$statVar == 0}, change it to:

if {[lindex $statVar 2] == 0} { ... }

Or use the exit condition word:

if {[lindex $statVar 0] eq "EXITED"} { ... }

-echo compatibility

See the -echo option description above for details and a pure-Tcl wrapper that translates boolean -echo true/false to bgexec 3.5's enum values.


DIFFERENCES FROM exec

Feature exec bgexec
Event loop active during run No Yes
Separate stderr variable No -errorvariable
Streaming callbacks No -onoutput, -onerror
Background (detached) mode No -detach yes or &
Completion variable No statVar
Custom environment No -environ
Input pipe creation No -input
Encoding control No -decodeoutput, -decodeerror
Shell redirections in args Yes Yes

PLATFORM NOTES

Unix

Full pipeline support: multi-process pipelines (|), all I/O redirections, 2>@1, signal delivery, and PTY options (PTY pending in this build).

Windows

Full pipeline support is implemented, including multi-process pipelines with |, all I/O redirections, << (here-string via temp file), <@/>@/2>@ channel redirections, and 2>@1 stderr merge.

Process creation: Children are spawned with CreateProcess and CREATE_NO_WINDOW — no console window appears. Programs that require a console (e.g. cmd.exe /c somecommand) work normally; they just don't pop up a window.

Line endings: Windows programs typically write \r\n. bgexec does not automatically strip carriage returns. If you compare or split collected output and see stray \r characters, apply string map {\r {}} $result or configure the channel with fconfigure $ch -translation crlf before reading.

bgexec status -outputvariable raw ipconfig
set clean [string map {
 {}} $raw]
puts $clean

Command interpreter: Unlike exec, bgexec does not implicitly invoke cmd.exe. Shell built-ins (dir, echo, set, if, for, ...) and constructs like 2>&1 in the argument list are not understood — they are features of cmd.exe, not of bgexec's redirection parser. To use shell built-ins, invoke cmd.exe /c explicitly:

# WRONG on Windows — "dir" is a cmd.exe built-in, not an executable
bgexec status -outputvariable out dir C:\Temp

# CORRECT — invoke cmd.exe explicitly
bgexec status -outputvariable out cmd.exe /c dir C:\Temp

Executable search: bgexec searches %PATH% for the program name. Extensions (.exe, .cmd, .bat) must be supplied explicitly when the program name has no extension — there is no PATHEXT auto-extension:

# Explicit extension required
bgexec status -outputvariable out python.exe script.py
bgexec status -outputvariable out cmd.exe /c myscript.bat

Pipelines: Multi-process pipelines work on Windows, but each segment must be a real executable (not a shell built-in). Because Windows inherits all open inheritable handles into every child, only the handles a child actually needs are made inheritable at spawn time; the rest are hidden. This is handled internally and requires no special care from the caller.

# This works on Windows — all three are real executables
bgexec status     -outputvariable result     tclsh.exe produce.tcl | tclsh.exe transform.tcl | tclsh.exe consume.tcl

# Pipe through sort.exe (Windows built-in at C:/Windows/System32/sort.exe)
bgexec status     -outputvariable sorted     cmd.exe /c type data.txt | sort.exe

Signals and -killsignal: Windows does not have Unix signals. When bgexec kills a child (via unset statVar or -killsignal), it calls TerminateProcess regardless of the signal name specified. The -killsignal option is accepted for cross-platform script compatibility but has no effect on which mechanism is used — TerminateProcess always delivers an immediate, unclean termination. If your child needs to perform cleanup on exit, use an application-level shutdown mechanism (a named pipe message, a shared event, a temp file sentinel) rather than relying on signal handlers.

-session and -tty: These options are Unix-only (PTY-based) and have no effect on Windows. They are accepted without error for cross-platform compatibility.

SIGTERM-style graceful shutdown on Windows: Because TerminateProcess is always used, the clean shutdown pattern requires an out-of-band mechanism:

# Create a sentinel file that the child checks periodically,
# then wait for it to exit cleanly before bgexec fires the callback.
proc requestShutdown {} {
    close [open shutdown.sentinel w]
}

proc onDone {n1 n2 op} {
    global serverStatus
    file delete -force shutdown.sentinel
    puts "Server exited: $serverStatus"
}

trace add variable serverStatus write onDone

bgexec serverStatus -detach yes tclsh.exe my_server.tcl
after 5000 requestShutdown
vwait serverStatus

Encoding: The default encoding follows Tcl's system encoding, which on Windows is typically cp1252 (Western) or another code-page encoding. If your child writes UTF-8 (Python 3, modern PowerShell, etc.) use -decodeoutput utf-8:

bgexec status     -decodeoutput utf-8     -decodeerror  utf-8     -outputvariable out     python.exe -c {print("héllo wörld")}

puts $out   ;# héllo wörld — not garbled

NUL device: The Windows equivalent of /dev/null is NUL (case insensitive). bgexec maps stdin to NUL internally when no input redirect is given, so you rarely need this — but if a child writes diagnostic output you want to discard:

bgexec status -outputvariable out my_verbose_program.exe 2> NUL

Tcl version compatibility

Compatible with Tcl 8.6 and Tcl 9.0 on both Unix and Windows.



CHANGELOG

3.5 (2026-04)

Critical bug fix: -linebuffered had no effect — callbacks fired on every fragment instead of on complete lines. CollectData() called ResetSink() unconditionally when running detached, discarding partially accumulated data between reads so NextLine() never found a newline. Fix: skip ResetSink() when SINK_BUFFERED is set. Applies to -onerror, -onoutput, -lasterrorvariable, -lastoutputvariable.

3.4 (2026-04)

Critical bug fix: READ_AGAIN (-3) fell through to TCL_RETURN in CollectData() because the test status >= 0 did not cover the negative sentinel. This caused premature sink close and timer restart on every EAGAIN read. Fix: explicit READ_AGAIN check added.

3.3 (2026-04)

Critical bug fix: 100% CPU in detached mode. The -poll switch was missing BLT_SWITCH_DONT_SET_DEFAULT, causing bgPtr->interval to be reset to 0 on every call, creating a zero-millisecond polling timer loop.

3.2 (2025-12)

Initial public release. Cross-platform (POSIX + Win32), Tcl 8.6 + 9.0, ARM64, glibc 2.31+ compatibility.

SEE ALSO

exec(n), open(n), Tcl_CreatePipeline(3), after(n), vwait(n)


CREDITS

Based on bltBgexec.c from the BLT toolkit by George A. Howlett. Critcl packaging and -input option by Daniel A. Steffen. Windows support, Tcl 8.6/9.0 compatibility, BLT 3.x switch names, and bug fixes (3.3, 3.4, 3.5) by contributors to this wrapper.