bgexec — Run one or more processes in the foreground or background, collecting stdout and stderr into Tcl variables and/or callbacks.
bgexec statVar ?options? program ?arg ...? ?| program ?arg ...?? ... ?&?
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:
statVar) is set to the pipeline's exit status when every
process has finished; a trace on this variable can be used to detect
completion asynchronously.& or
using -detach yes.<, <@, <<, >, >>, 2>, 2>>,
>&, >>&, 2>@1) are supported directly in the argument list.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.
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.
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.
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.
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.
All options must appear before the program name.
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
Same as -decodeerror but applies to child stdout.
Example — collect Latin-1 output:
bgexec status \
-decodeoutput iso8859-1 \
-outputvariable result \
legacy_program
puts $result
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
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
}
Specifies the environment for child processes as a flat key–value list:
{KEY1 value1 KEY2 value2 ...}
""), the child is started with an empty
environment.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
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"
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"
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:
-input requires -detach yes. Without it, bgexec blocks waiting for the
child to finish before returning — which means you can never write to the
channel. Always pair -input with -detach yes.
After bgexec returns, varName contains a Tcl channel handle such as
file4. In Tcl, channel handles are ordinary strings — there is no separate
"channel object" type. You use $varName directly with puts, close, and
fconfigure, exactly as you would with any channel returned by open:
tcl
bgexec status -detach yes -input ::ch $tclsh files/cat.tcl
puts $::ch "hello" ;# $::ch is "file4" — use it directly
close $::ch
The child will not see EOF until you explicitly close the channel. Many
programs (including cat, sort, wc) read until EOF before producing
output. If you vwait on the status variable without first closing the
channel, the script will deadlock.
Close the channel before calling vwait. The correct sequence is always:
write data → close $::ch → vwait status.
If the channel is left open when your script exits or the bgexec is destroyed, bgexec will not forcibly close it. Always close it explicitly to avoid leaving the child process blocked indefinitely.
On Tcl 9 the event loop dispatches timers more aggressively than Tcl 8.6.
If multiple bgexec instances share the same status variable, a set on that
variable (e.g. to kill one instance) will trigger the VariableProc kill
callback of every bgexec currently watching it. Use distinct status
variables for each concurrent bgexec invocation.
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
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)"
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
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
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
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.
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
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
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"
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
(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
(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
Alias: -update (Tcler's Wiki standalone bgexec compatibility)
Alias for -lastoutputvariable. Provided for compatibility.
&, no -detach yes): returns the empty string on
success; raises a Tcl error on pipeline failure (unless -ignoreexitcode yes
is set).& or -detach yes): returns a Tcl list of PIDs of all
processes in the pipeline.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}
bgexec status \
-outputvariable out \
-errorvariable err \
ls -la /tmp
if {[lindex $status 2] != 0} {
puts "Error: $err"
} else {
puts $out
}
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.
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.
# 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
# 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"
# 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
# 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
# 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)
# 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
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
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.
bgexec 3.5 is a drop-in replacement for the Tcler's Wiki standalone bgexec with the following notes:
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).
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"} { ... }
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.
| 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 |
Full pipeline support: multi-process pipelines (|), all I/O redirections,
2>@1, signal delivery, and PTY options (PTY pending in this build).
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
Compatible with Tcl 8.6 and Tcl 9.0 on both Unix and Windows.
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.
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.
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.
Initial public release. Cross-platform (POSIX + Win32), Tcl 8.6 + 9.0, ARM64, glibc 2.31+ compatibility.
exec(n), open(n), Tcl_CreatePipeline(3), after(n), vwait(n)
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.