Flash Scripting
- Snapshot
- v0.2.0
- Source path
components/flash/docs/scripting.md- Tree
c41b976e0362
Use this guide when writing and running .fsh files. It covers script arguments, non-executing checks, formatting, external processes, pipelines, redirections, command statuses, errors, and background jobs. Syntax, values, bindings, expressions, modules, functions, and structured-data operations are in the Language Guide.
Project status: FlashOS as a complete operating system remains pre-alpha software. Flash 1.0.0 is released as the scripting and execution contract in the current source. Availability in a particular FlashOS image or on another target is qualified separately; execution on a Linux or macOS host is not proof of FlashOS target support.
On this page
- Running scripts
- Script arguments
- Script and interactive execution
- Checking and formatting without execution
- Session state
- Invoking commands
- Command substitution
- Pipelines
- Redirections
- Statuses and failures
- Background execution
- Job commands
- Interactive job control
- Portability and operational boundaries
Running scripts
The Flash executable is named fsh. Run a UTF-8 source file by passing its path:
fsh program.fsh
A relative script path is resolved by the process that starts fsh. Commands inside the script begin with the caller's current working directory and inherited environment.
Use -- when a script path begins with a hyphen:
fsh -- --maintenance.fsh
The standard informational modes are:
fsh --help
fsh --version
fsh --help describes the launcher and script invocation. Inside Flash, the
separate language command help [NAME] inspects standard built-ins and visible
named functions. It is not a top-level fsh help mode.
Flash reports command-line invocation errors before opening a script. Before
execution, it canonicalizes the root and every static import '<path>', reads
and parses each canonical source once, and rejects load, UTF-8, syntax, cycle,
module-name, or lexical-reference failures. Lexical resolution covers every
loaded source without executing it, so an invalid dormant load-only dependency
also stops program construction. Diagnostics use the applicable source file and
group excerpts when a failure spans multiple files. Dependencies reached
through named imports initialize once in deterministic dependency-first order
before the root. Their explicit exports become immutable snapshot bindings in
the importer. A valid source reached only through a load-only import does not
run initialization or bind names.
Named initializers otherwise run as ordinary Flash code. They share the
program's logical cwd, child environment, current status, output routing, and
background-job coordinator in dependency-first order. Successful cd,
export, and unset effects are visible to later initializers and the root;
normal completion and initializer exit commit the final child environment to
the caller. Runtime or required-output failure does not commit that environment.
Output, filesystem writes, and process activity are immediate external effects,
not a whole-program transaction. A later failure cannot retract bytes, restore
a truncated file, or unspawn work. The runtime stops later initializers and the
root, joins every program-owned background job, and retains ordered background
failure evidence. exit inside an initializer is whole-program control: it
skips that module's export materialization and all later execution, joins jobs,
and follows normal completion rules, including background-failure precedence.
See Initializer effects for the
complete per-class contract.
Source files
Flash scripts conventionally use the .fsh extension. Source files:
- must contain UTF-8 text;
- may use LF or CRLF line endings;
- use the same grammar and evaluator as interactive submissions;
- are parsed as Flash rather than POSIX shell source.
A .fsh file is not a Bash or sh program. Do not use POSIX-specific syntax unless an external POSIX shell is invoked explicitly.
Script arguments
Invoke a script with zero or more arguments after its path:
fsh program.fsh first "" --mode
The first non-option operand is the script path. Every following operand belongs
to the script, including -- and option-like values such as --mode. A leading
-- ends fsh option parsing and makes the next operand the script path:
fsh -- --maintenance.fsh first
The root module receives the arguments as an immutable args: List[String]
binding:
let first = $args[0]
let second = $args[1]
The script path and process argument zero are not included. Order and
cardinality are exact: the empty operand above remains one empty String.
Arguments must be valid UTF-8; an invalid operand is rejected before source
loading without lossy replacement.
args is available only to the root module through an implicit parent scope.
Imported and load-only modules do not receive it. A root declaration or named
import may shadow args through ordinary source-order scope rules.
Script arguments are data. They are not reparsed as Flash source, do not undergo implicit whitespace splitting, and do not trigger implicit wildcard expansion. A script must request any later parsing, conversion, or explicit collection expansion itself.
Script and interactive execution
Interactive input and scripts share the Flash parser, value model, command planner, and evaluator. The surrounding session behavior is intentionally different.
| Behavior | Interactive session | Script execution |
|---|---|---|
| Startup command | fsh |
fsh program.fsh |
| Language parser and evaluator | Shared | Shared |
| Line editor and prompts | Enabled | Not used |
| Persistent interactive history | Optional | Not used |
| Interactive startup configuration | Considered once | Never loaded |
| Recover after a source/runtime diagnostic | Return to the prompt | End the script |
| Background-job completion | Prompt-safe notices | Joined before script exit |
Interactive configuration cannot silently change the meaning of an automation script. A script starts from deterministic session defaults, its inherited environment, and its initial working directory.
Running:
fsh --no-config
starts an interactive session without loading its startup configuration. The --no-history option similarly disables interactive history for that session. These policies do not turn script execution into an interactive session.
Interactive startup settings
The interactive config is ordinary Flash source. During its isolated startup transaction, six mutable settings are available:
$pipefail = true
$capture_limit = 1048576
$completion = true
$history = false
$prompt = 'flash> '
$continuation_prompt = 'more> '
This is the complete v1 settings surface. Flash deliberately keeps terminal themes and colors, editor key bindings, history location and capacity, completion ranking, and editor-backend tuning out of config. Ordinary Flash bindings, function definitions, and staged environment exports can initialize the interactive namespace and child environment, but they do not create additional settings.
$pipefail, $completion, and $history require Bool values.
$capture_limit requires a nonnegative Int that fits the host byte-count
range; zero is valid. $prompt and $continuation_prompt require String
values without terminal control characters. These six bindings exist only
while config is evaluated and are not visible to scripts or at the interactive
prompt. A successful config commits the resulting session options, prompts,
and editor settings together with its ordinary bindings and staged environment.
A parse, evaluation, setting, or startup-policy failure discards all of them
and enters visible safe mode with the fixed [SAFE] >> primary prompt and
clean defaults.
Flash reads the v1 config from flash/config.fsh below an absolute,
nonempty XDG_CONFIG_HOME. Without that variable, FlashOS and Linux use
$HOME/.config/flash/config.fsh, while macOS uses
$HOME/Library/Application Support/flash/config.fsh. Persistent history uses
flash/history below an absolute, nonempty XDG_STATE_HOME; its fallbacks are
$HOME/.local/state/flash/history on FlashOS and Linux and
$HOME/Library/Application Support/flash/history on macOS. History is enabled
by default and is created only for interactive sessions unless disabled by
configuration or --no-history.
If config discovery, loading, or evaluation enters safe mode, the startup
diagnostic names the failure and, when discovery selected one, the file.
Continue with fsh --no-config to isolate the problem, then repair the file
using only the six settings above plus ordinary startup-safe Flash
declarations, functions, and exports. A history initialization failure is
fatal rather than a safe-mode fallback; use --no-history to confirm that
boundary before repairing ownership, modes, or the selected state path.
--no-config wins before config discovery, and --no-history wins over a
config request to enable history. No interactive config or setting is loaded by
script, command, checker, formatter, help, or version modes.
Completion candidates refresh before every prompt from the committed command
registry, visible lexical scope, logical working directory, child PATH, and
the executable and recursively discovered path entries visible in bounded host
snapshots. This makes config functions, later definitions, cwd changes,
exports, and PATH changes visible without performing environment or
filesystem I/O during a Tab keypress. Path completion uses Flash token spans
for bare, single-quoted, double-quoted, interpolated, argument, redirection,
executable, and glob(...) positions. It inserts reversible escapes, preserves
wildcard pattern spelling, and never expands or executes the submitted source.
Native names that cannot be represented exactly in the UTF-8 source buffer are
omitted rather than converted lossily.
Checking and formatting without execution
Flash v1 provides inspection modes for validating and normalizing source without running the program.
The currently available runtime inspection surface is:
help
help NAME
It accepts zero or one static name, produces UTF-8 bytes, and uses ordinary
capture, pipeline, and redirection behavior. Lookup is resolved from immutable
metadata during planning; it never calls the documented function or queries an
external executable. Dynamic query forms such as help $name, substitutions,
spreads, and closures are rejected rather than evaluated as discovery logic.
Static checking
Check one root program with:
fsh check program.fsh
fsh check -- --maintenance.fsh
fsh check --help
The checker accepts exactly one native source path and no script arguments.
Exact first-operand check selects this launcher mode; fsh ./check and
fsh -- check still treat check as a script path. Stdin, multiple explicit
roots, globs, directory walks, implicit extensions, configuration, and ambient
module search are not supported.
The root and every recursively reachable static import are canonicalized, loaded, and parsed. Canonical aliases share one source identity, so a symlink to a regular source is accepted and read once. Roots and imports must resolve to finite regular files containing UTF-8 source; directories and special files are rejected.
Checking reports syntax and module-graph failures, name and export/import failures, assignment mutability and known-type failures, named-function annotation and known-call signature failures, shared built-in argument-schema failures, and statically knowable pipeline-carrier failures. Diagnostics use retained source spans and deterministic phase order: discovery and graph issues, then name issues, then signature issues, then command and carrier issues. Within a phase, sources follow canonical first-visit depth-first order and constructs follow source order. A broken discovery graph suppresses name and signature analysis; name failures suppress signature analysis. Command and pipeline analysis still visit every successfully parsed source because they do not depend on those phases.
Bare command names absent from the built-in registry and explicitly forced
external commands are classified as byte-stream stages without checking
PATH. An exact built-in uses its shared runtime carrier and argument contract,
including positional arity and kinds, option arity and conflicts, -- policy,
and dynamic-tail policy. Help, completion, hover, signature help, planning, and
runtime validation consume the same registry metadata. A dynamic command head
remains unknown, so the checker suppresses only answers that depend on guessing
its runtime command or an interpolation-dependent argument. Diagnostics
therefore describe language-level contract incompatibility, not executable
availability.
A successful check is silent and exits with status 0. Any analysis or source
error is written only to stderr and exits with status 1. Invocation misuse
writes one fsh: message to stderr and exits with status 2; checker help writes
to stdout and exits with status 0.
fsh check does not load startup configuration or history, initialize any
module, evaluate declarations or substitutions, expand words, probe an
executable, apply redirections, change the working directory, mutate the
environment, access a terminal, or start a process. It does not format source,
predict runtime output or status, validate external-command availability,
infer types that depend on runtime values, discover a multi-root project, or
start a language server. Success means only that the supported non-executing
analyses completed without error diagnostics; target capabilities and
runtime-only data remain separate concerns.
Execution-plan inspection
Inspect one exact command pipeline with:
fsh plan command.fsh
fsh plan -- --maintenance.fsh
fsh plan --help
The named regular UTF-8 source must contain exactly one top-level foreground
job with one pipeline. Declarations, assignments, imports, environment
statements, control flow, callable definitions, background jobs, and
status-dependent && or || chains are rejected because their exact plan can
depend on execution or prior state. Stdin, multiple roots, script arguments,
configuration, and history are not accepted.
Inspection parses and statically analyzes the source with the standard command
registry, then expands the pipeline against an empty lexical scope, the
inherited process environment, the launcher's current working directory, and
default session options. Bare and forced external commands use read-only
executable metadata checks, so the plan shows the executable selected from the
inherited PATH. Command substitution is rejected rather than evaluated.
The deterministic plan includes its source spans, native cwd and child environment, session options, process-group policy, source-ordered stage resolution, exact escaped native arguments, typed internal arguments, carrier contracts, redirections, help snapshots, and pipeline edges. Native units use escaped bytes so distinct non-UTF-8 values do not collapse through replacement characters. The output is human-facing inspection text, not executable input or a serialization format.
The plan contains the complete inherited child environment and may therefore include credentials or other secrets. Treat captured or shared plan output as sensitive data.
Successful inspection writes the newline-terminated plan to stdout and exits 0. Source, analysis, shape, expansion, resolution, or preflight failure writes a source-backed diagnostic to stderr and exits 1. Invocation misuse exits 2. Inspection never initializes a session, mutates lexical/environment/cwd state, opens a redirection, creates a pipe, spawns or waits for a process, starts background work, or accesses a terminal.
Canonical formatting
Use the launcher formatter modes on one or more explicit files:
fsh format --check source.fsh library.fsh
fsh format --write source.fsh library.fsh
fsh format --help
--check visits every operand in command-line order without writing. Canonical
files are silent; each complete source that differs receives an anchored
FMT001 diagnostic. --write parses and formats the complete batch before its
first mutation. A read, UTF-8, incomplete, or invalid-source failure therefore
leaves every operand untouched.
Operands must name existing regular files. Use the formatter -- delimiter
before a dash-leading path. The formatter does not read stdin, expand globs,
walk directories, follow a final symlink, or discover imported sources. Each
named file is independent, and two operands that resolve to the same canonical
target are rejected.
After a successful write preflight, each changed file is replaced in operand order through a synchronized sibling temporary file and same-directory atomic rename. Unchanged files are not opened for write. Changed files preserve their permission bits, but replacement creates a new file identity and does not promise hard-link identity, ownership, group, timestamps, ACLs, or extended attributes. A source or permission change detected after preflight is refused. An individual replacement is atomic, but a multi-file write is not a transaction: the first replacement failure stops the batch after any earlier successful replacements.
Successful check and write operations emit nothing and exit with status 0.
Noncanonical check results and source or filesystem failures use stderr and
status 1. Invocation misuse uses one fsh: message on stderr and status 2.
Formatter output never uses stdout except for format --help.
Formatting is idempotent: formatting already formatted source does not produce
another change. The canonical result preserves parsed program structure,
significant token spelling, comments, and documentation-comment attachment.
Incomplete input retains SYN002, while invalid input retains the parser's
structured diagnostics.
fsh format is a non-executing launcher frontend. It does not load startup
configuration or history, traverse imports, initialize a runtime session,
resolve commands, probe executables, or execute source.
Session state
A script owns session-local state that persists between its statements:
- lexical bindings and functions;
- a logical working directory;
- the environment inherited by child processes;
- session options;
- the most recent normally completed command status;
- background jobs started by the script.
Working directory
Use cd to change the logical working directory:
cd workspace
Later external commands and relative filesystem operations use the updated directory.
With no argument, cd uses the inherited HOME environment entry:
cd
Use pwd to produce the current directory as a structured Path value:
pwd
A successful directory change updates the child environment entries PWD and OLDPWD.
Child environment
Lexical bindings and child-process environment entries are separate.
let mode = "release"
export BUILD_MODE = $mode
The lexical binding $mode is available to Flash evaluation. The exported BUILD_MODE entry is included in the environment of external processes started later.
Remove an environment entry with unset:
unset BUILD_MODE
Use env("BUILD_MODE") to read the current child-environment entry. The result
is a String, or null when the name is absent. Flash does not treat $NAME as
an environment lookup, and a present native value that is not valid UTF-8 fails
explicitly rather than being decoded lossily. Reads observe successful
export and unset changes in source order.
Environment changes belong to the running Flash session. They do not modify the environment of the process that started fsh.
Invoking commands
A bare command name is resolved in this order:
- a registered Flash internal command;
- an external executable.
pwd
ls
In this example, pwd and ls select the Flash internal commands when those commands are registered.
Built-in namespace compatibility
The standard command namespace distinguishes canonical core commands, invocable migration aliases, and reserved names that cannot fall through to an external executable. The Flash v1 standard manifest contains no aliases or reserved names.
The Flash v1 core command inventory is:
bg cd check collect command decode each encode exit fg first from get help jobs
kill last length lines ls open pwd save select sort to update wait where which
export and unset are statement keywords, not commands in this inventory.
The inventory is a source-language compatibility boundary because registering a
previously unknown bare name can redirect an existing script away from an
external executable.
| Namespace change | Flash v1 compatibility |
|---|---|
| Change implementation, performance, or documentation without changing observable command behavior | Compatible within the language major |
| Add deprecation metadata while preserving behavior and canonical identity | Compatible within the language major |
| Activate a name already reserved at the start of the language major | Compatible within the language major |
| Add a core command, alias, or reservation under a previously unknown name | Requires the next language major |
| Remove or rename a core command; remove or retarget an alias; or release a reservation to external fallback | Requires the next language major |
| Change an entry between core, alias, and reserved, except activation of an existing reservation | Requires the next language major |
| Change carriers, arguments, flags, effects, output, status, or other behavior in a way that can alter a successful program | Requires semantic review and normally the next language major |
An alias, when present in a future manifest, retains its source spelling but
uses the canonical core command's signature and behavior. A reserved bare name
fails before PATH lookup; intentional external execution remains available
through ^name or command name. Runtime execution does not print unsolicited
deprecation warnings. fsh check reports deprecated exact command heads as
CMD001 warnings and reserved exact bare heads as CMD002 errors without
probing PATH.
help includes every core, alias, and reserved entry and exposes lifecycle or
migration details. Executable completion includes core commands and aliases,
with aliases reusing canonical flags, but excludes reserved names. which
returns ordered records with this schema:
{
name: Path,
kind: "internal" | "alias" | "reserved" | "external" | "missing",
target: String | null,
path: Path | null
}
target contains an alias's canonical command or a reserved entry's suggested
replacement; other results use null. Only an external result populates
path. Reserved and missing results select final status 1; internal, alias, and
external results are successful.
Forcing external execution
Prefix a command with ^ to bypass the internal-command registry:
^ls -la
The caret is Flash syntax and is not included in the external process argument vector.
Use command when the executable name is selected at runtime:
let program = "compiler"
let arguments = ["--mode", "release"]
command $program ...$arguments
command also bypasses the internal registry.
Direct process launch
Flash starts external executables directly. It constructs a native executable path, argument vector, environment, working directory, and descriptor map without passing rendered command text through /bin/sh.
Consequently:
- spaces inside one argument remain inside that argument;
- pipeline and redirection characters produced by interpolation remain data;
- an interpolated string cannot introduce another command;
- external commands receive exactly the arguments produced by Flash expansion.
Running another shell is always explicit:
^sh -c "external shell source"
The quoted source in this example is interpreted by sh, not by Flash.
Executable lookup
A command name containing / is treated as a path and is not searched through PATH:
^./tools/check
^/usr/bin/example
A bare external name is searched through the inherited PATH entries in source order:
^example
Empty PATH entries do not mean the current working directory. Use an explicit relative path such as ./example when that behavior is intended.
Failure to locate an executable is a runtime error. Flash does not manufacture a successful process launch with a synthetic exit status such as 127.
Argument cardinality
Each ordinary command word produces exactly one argument:
let label = "two words"
command "example" $label
The value of $label remains one argument.
Expand a list into separate arguments with explicit spread syntax:
let flags = ["--verbose", "--check"]
command "example" ...$flags
An empty list contributes no arguments. An empty string contributes one empty argument.
Wildcard characters are literal in ordinary words:
command "example" "*.fsh"
Use glob and spread the resulting list when filesystem matching is required:
let scripts = glob("scripts/**/*.fsh")
command "example" ...$scripts
glob accepts a string or path pattern and returns a sorted list of native path
values. *, ?, and character classes match within one component; a complete
** component recurses without following directory symlinks. Leading-dot names
require an explicitly leading dot in that component, and no matches produces an
empty list rather than the original pattern.
For the complete expansion rules and eligible argument value types, see Commands and argument expansion.
Command substitution
Command substitution captures standard output as text or exact bytes:
let directory = "$(pwd)"
let explicit_text = $(text: ^program --version)
let payload = $(bytes: ^program --binary)
The $(...), $(text: ...), and $(bytes: ...) forms evaluate one foreground
command or conditional chain. They do not start an intermediate shell.
Successful text capture:
- reads standard output only;
- requires valid UTF-8;
- removes trailing LF and CRLF line-ending sequences;
- preserves other whitespace and newlines;
- produces exactly one
String; - never reparses or splits the captured text.
An empty capture therefore produces one empty string rather than no value.
Byte capture reads the same reached standard output under the same limit and
status rules, but produces exactly one Bytes value. The capture operation
never decodes, trims, displays, serializes, reparses, or splits the data. A byte
capture is not eligible for implicit command-word insertion; bind or pass it as
a value and cross to text only through an explicit decoding boundary.
Standard error remains inherited unless the source redirects it:
let version = "$(^program --version 2> version-errors.log)"
A nonzero command exit still produces captured output paired with its actual status. It does not become a runtime error merely because the command was unsuccessful.
Capture is bounded by the session capture limit. If the output exceeds that limit, Flash continues draining and reaping the started processes before returning a capture-limit runtime error. This prevents the memory bound from causing a pipe deadlock.
Invalid UTF-8 similarly produces a runtime error rather than silently replacing
bytes. Use $(bytes: ...) when arbitrary binary output must be preserved.
Pipelines
The | operator connects one stage to the next:
^producer | ^consumer
External stages exchange byte streams through operating-system pipes. Flash starts the required stages before waiting for their completion, allowing producers and consumers to run concurrently.
Standard error pipelines
Use |& to merge a stage's standard output and standard error into one byte stream:
^program |& ^consumer
The merged stream is not tagged. Its byte order is the order produced by the underlying writes.
|& requires a byte-oriented stage whose standard output and standard error can be redirected. Structured runtime values are not implicitly encoded or merged into a byte stream.
Structured stages
Internal commands may exchange structured carriers:
- one
Value; - a lazy
ValueStream; - a
ByteStream; - no pipeline payload.
For example:
ls
| where {|entry| $entry.type == "file"}
| select name size
| sort name
The structured records in this pipeline are not converted to terminal-formatted text between stages.
Explicit representation changes
Cross representation boundaries explicitly:
open users.json
| from json
| where {|user| $user.active}
| select name email
| to json
| save active-users.json
Common conversion families are:
| Command | Boundary |
|---|---|
decode |
Byte stream to textual values |
encode |
Textual values to bytes |
from |
Serialized bytes to structured values |
to |
Structured values to serialized bytes |
collect |
Lazy value stream to one materialized list |
Flash rejects incompatible pipeline edges rather than guessing a conversion.
Explicit byte boundaries may occur more than once in one pipeline. Flash v1 permits carrier-compatible shapes such as:
^producer
| decode utf8
| where {|text| true}
| encode utf8
| ^transform
| decode utf8
| where {|text| true}
The external stages remain byte-oriented, while each internal island keeps its structured values inside the runtime.
Interactive rendering of a record, list, or table is for human inspection. It is not a stable serialization format. Use an explicit encoder or formatter when a file or external process requires bytes.
Alternating mixed pipelines
Flash executes every carrier-compatible alternating shape without capturing an external segment into memory. External children start first, then maximal internal segments drain concurrently through bounded operating-system pipes. Structured values and lazy pull closures stay inside the internal segment that owns them; only byte descriptors cross a concurrent segment boundary.
Internal stage preparation remains source ordered. Session commands therefore apply cwd and child-environment changes deterministically even when segment drains overlap. Lazy closure changes are visible within their segment and merge in segment source order only after the complete pipeline succeeds.
On normal completion, every source stage contributes exactly one status leaf.
A check immediately after an external stage forwards its bytes before
waiting, then checks that stage's real completion status after all bytes have
drained. A failed check, runtime error, output failure, or wait failure prevents
pending cwd, environment, closure, and status changes from committing. Output
already written, files already changed, and completed process effects are not
rolled back.
Failure or explicit exit stops later preparation, closes owned endpoints,
terminates and waits live external children, and restores terminal ownership.
When independent stages fail concurrently, the earliest source-stage failure
is reported. A downstream consumer closing early remains ordinary pipeline
backpressure cleanup rather than fabricated success or an implicit status
conversion.
Pipeline completion
Every normally completed stage has its own status. A multi-stage pipeline additionally produces an aggregate status whose stage list remains in source order.
By default, the last stage selects the aggregate result. When the session's pipefail option is enabled, the rightmost unsuccessful stage selects it instead. pipefail changes status selection only; it does not stop, reorder, or serialize pipeline stages.
All required stages are reaped before a completed aggregate status is returned.
Redirections
Flash supports source-ordered descriptor redirections.
| Form | Meaning |
|---|---|
< file |
Open file for standard input |
> file |
Create or truncate file for standard output |
>> file |
Create or append to file for standard output |
n< file |
Open file for descriptor n |
n> file |
Create or truncate file for descriptor n |
n>> file |
Create or append to file for descriptor n |
n>&m |
Duplicate the current descriptor m onto n |
n>&- |
Close descriptor n |
Examples:
^program < input.dat > output.dat
^program > output.log 2> error.log
^program >> combined.log 2>&1
A descriptor number must be adjacent to the operator. These forms are different:
^program 2> error.log
^program 2 > output.log
The first redirects descriptor 2. The second passes 2 as an argument and redirects descriptor 1.
Source order matters
Redirections are applied from left to right:
^program > combined.log 2>&1
Both standard output and standard error end at combined.log.
^program 2>&1 > output.log
Standard error keeps the destination that standard output had before the later > action. Only standard output ends at output.log.
Descriptor duplication refers to the mapping that exists at that exact point. It is not deferred until process launch.
Pipelines and local redirections
Pipeline descriptor assignments are installed before a stage's local redirections. A local redirection can therefore replace a pipeline endpoint:
^program 2> errors.log |& ^consumer
Only the program's standard output reaches consumer.
^program > output.log |& ^consumer
Only standard error reaches consumer.
^producer | ^consumer < input.dat
The consumer reads input.dat instead of the pipe.
The same rule applies to an internal byte-producing segment tail. If its final stdout route is a local file, Flash writes the bytes there and closes the unused pipeline writer, so the downstream external stage observes EOF. A final inherited, read-only, or closed stdout binding that the internal executor cannot write is rejected during preflight.
An upstream process whose pipe no longer has a reader retains its real completion result, including a possible broken-pipe signal. This is not reported as a redirection setup failure.
Redirect targets
A redirect target uses the same one-word expansion rule as a normal command argument:
let output = "build output.log"
^program > $output
The value remains one path. It is not split at the space.
Redirect targets do not support implicit wildcard expansion or list spreading.
Preparation and failure
Flash completes expansion and execution preflight before it opens redirect targets or starts pipeline stages. Once file actions begin, however, they are not a filesystem transaction.
For example, an earlier > action may create or truncate a file even when a later redirection for the same stage fails. Flash closes resources and cancels or reaps already started sibling stages, but it does not pretend to roll back completed filesystem effects.
A failed open, descriptor duplication, descriptor assignment, or descriptor close is a runtime error. The affected command is not represented as a normal nonzero process status.
Flash does not implicitly provide POSIX here-documents, here-strings, &>, or a noclobber mode through these operators.
Statuses and failures
Flash distinguishes normal unsuccessful completion from structural failure.
| Outcome | Meaning | Selects ||? |
|---|---|---|
Successful Status |
Command completed successfully | No |
Unsuccessful Status |
Command completed with a nonzero code or signal | Yes |
| Runtime error | Evaluation, planning, resolution, I/O, or platform operation failed | No |
| Cancellation | Evaluation was interrupted or cancelled | No |
| Parse failure | Source could not be executed | Not evaluated |
Process exit and output contract
The fsh process preserves normally completed command results and uses
separate statuses for launcher misuse and shell-owned failure. CI callers can
rely on the following matrix without parsing diagnostic prose:
| Executable outcome | Process status | Output contract |
|---|---|---|
| Help, version, successful check or format, or a script with no completed command | 0 | Requested help/version text uses stdout; the other cases are silent |
| Launcher misuse | 2 | Exactly one newline-terminated fsh: message on stderr |
Completed command with code 0..=255 |
Exact command code | No automatic status message |
Completed command with numeric signal 1..=127 |
128 + signal |
No automatic status message |
| Completed status that cannot be represented by those rules | 1 | One shell-boundary report on stderr |
| Parse, module, runtime, source-loading, platform, or output failure | 1 | A diagnostic or shell report on stderr |
| Interactive end of input | 0 | No exit diagnostic |
Interactive exit CODE |
Exact requested code | No exit diagnostic |
| Fatal interactive editor, reporting, or platform failure | 1 | A fatal report when stderr remains usable |
Codes 1 and 2 remain valid ordinary command results. A command that really exits with either code is silent unless the program itself wrote output; it is not reclassified as a shell failure or launcher misuse. Resolution and spawn failures did not complete as child processes, so Flash reports them as runtime errors instead of manufacturing command-not-found 127 or not-executable 126.
Program bytes use stdout. Source diagnostics, shell-owned reports, loading errors, and failing background-job reports use stderr. Source diagnostics are deterministic, contain no color or terminal-control bytes, and end with a newline. Required writes and flushes are checked. A failed stdout write or flush becomes status 1 and is reported on stderr when possible; a failed stderr write or flush is never reported recursively through stderr.
Pipeline aggregation and pipefail select the completed status before this
process mapping occurs. Script background jobs are joined first: their failure
reports use stderr in job-identity order, and the first failing job in that
order retains the established final-status precedence. Interactive diagnostics
remain recoverable, while a fatal interactive failure hangs up session-owned
jobs before returning status 1.
Conditional execution
Use && to continue after success:
^build && ^deploy
Use || to continue after an unsuccessful status:
^primary || ^fallback
&& binds more tightly than ||:
^build && ^deploy || ^report-failure
The fallback in this example runs when either the build is unsuccessful or the deployment is unsuccessful.
A skipped branch is not expanded or planned. Its command substitutions, redirects, file opens, and process launches do not occur.
Runtime errors and cancellation abort the chain. They are not treated as false statuses and do not activate ||.
Status conditions
A command or pipeline can be used as an if or while condition:
if ^program --probe {
^program --run
} else {
^fallback
}
A successful status acts as true. A nonzero exit or signal status acts as false.
Pure expression conditions must evaluate to Bool. Flash does not treat zero, an empty string, null, or an empty collection as implicitly false.
Converting failure into an error
Use check when an unsuccessful status must propagate as a runtime error:
^build | check
check:
- requires an upstream stage;
- accepts any pipeline carrier;
- forwards the carrier unchanged;
- inspects the completed upstream status;
- produces a structured
UnsuccessfulStatuserror when that status is unsuccessful.
It does not convert parse failures, command-resolution failures, redirection failures, decoding errors, cancellation, or other runtime errors. Those outcomes already propagate through their own error paths.
Output delivered before a streaming failure or unsuccessful checked completion remains observable.
Catching and raising runtime errors
Use a try statement when a script can recover from a runtime failure:
try {
^build | check
} catch error {
^log $error.category $error.message
}
The catch binding is immutable and exists only inside its block. The catch
starts with the lexical bindings, logical working directory, child environment,
session options, and current status from before the try. Successful changes
made by the catch then commit normally. Bytes already written, filesystem
changes, and child-process effects remain observable.
throw "message" raises a source-anchored user error. throw $error rethrows
an existing Error with its source, labels, nested-call frames, cause, and
optional status intact. Throwing any other value is itself a typed runtime
error.
Only runtime errors are caught. In particular:
- a nonzero or signalled command completion remains an ordinary
Statusuntil a reachedcheckconverts it; - cancellation and explicit
exitbypass the catch and retain their cleanup behavior; and - parse or analysis failures, stopped-job control, and fatal output/editor failures never become catch values.
The complete Error member and equality contract is in the
Language Guide.
Explicit script exit
Use exit to request script termination:
exit
exit 2
An explicit code must be an ASCII decimal value from 0 through 255.
Without an argument, exit uses the current representable command code. It uses zero when no status exists and one when the current completion cannot be represented as an ordinary exit code.
The session boundary performs required background-job cleanup before the fsh process exits.
Background execution
A trailing & backgrounds the complete conditional chain:
^long-running-program &
^prepare && ^process || ^report-failure &
The launch returns immediately with a successful launch status once the job has been published. Its later completion does not asynchronously replace the current foreground status.
Each background chain receives one stable, nonzero Flash job identity. A language job identity is distinct from operating-system process and process-group identifiers.
Redirect background output
A background job can write while later script statements are running. Redirect output when interleaving would be undesirable:
^long-running-program > task.log 2>&1 &
Background jobs do not receive foreground terminal ownership. Programs that require interactive terminal input should normally remain foreground jobs.
Script lifetime
A script does not orphan jobs that it started. Before the script ends, Flash joins its remaining background jobs on every exit path, including:
- normal end of source;
- an explicit
exit; - a runtime failure.
A stopped background job is continued before the script waits for it. Flash then waits for all of its members to reach terminal completion.
When every joined job succeeds, the script retains its foreground result. When a background job fails, that failure participates in the final script exit result; the first failing job in job-identity order is selected.
The join does not impose an automatic timeout or destructive escalation. A program that repeatedly stops or refuses to terminate can therefore keep script shutdown open. Termination should be requested explicitly when the script owns such a process.
Job commands
Flash provides five internal commands for addressable jobs:
| Command | Purpose |
|---|---|
jobs |
Produce a structured snapshot of addressable jobs |
fg |
Move an eligible job to the foreground |
bg |
Continue a stopped job in the background |
wait |
Wait for selected jobs |
kill |
Send a selected signal to job process groups |
A job reference has the exact form %n, where n is a nonzero decimal Flash job identity:
%1
%12
Bare numbers, %+, %-, signs, and suffixed forms are not aliases.
Inspecting jobs
jobs accepts no arguments and produces one structured record per addressable job:
jobs
Records include stable fields for:
job;state;placement;group;command;status;signal.
Because jobs produces a ValueStream, it can participate in a structured pipeline:
jobs
| where {|job| $job.state == "stopped"}
| select job command signal
Reading the snapshot does not resume jobs, acknowledge pending notices, or remove completed records.
Waiting
With no arguments, wait waits for all addressable non-failed jobs selected at invocation time:
wait
Wait for specific jobs by identity:
wait %2 %5
Explicit targets are processed in source order. A stopped selected job is continued in background placement before waiting.
The command returns the first unsuccessful selected aggregate status, or the last selected status when all selected jobs succeed.
Continuing a stopped job
Continue the newest stopped job:
bg
Continue one specific job:
bg %3
bg does not transfer the terminal to the job.
Foregrounding a job
Move the newest eligible stopped or running background job to the foreground:
fg
Select a specific job:
fg %3
fg requires a real foreground terminal. The shell transfers terminal ownership to the process group and restores it before the next prompt or session action.
This command is primarily useful in an interactive session. Non-interactive automation should generally use wait, redirection, and explicit signal delivery instead.
Sending signals
kill requires at least one explicit target:
kill %3
The default signal is termination. A selector may choose another supported group-directed signal:
kill --hangup %3
kill --interrupt %3
kill --terminate %3
kill --kill %3
kill --stop %3
kill --continue %3
Multiple targets are processed in source order:
kill --terminate %2 %4
Destructive escalation is explicit. Flash does not silently replace a failed graceful request with --kill.
Interactive job control
Running fsh without a script starts an interactive session.
On a terminal with job-control capabilities:
- one external pipeline runs in one operating-system process group;
- a foreground job owns the terminal while it runs;
- Flash restores terminal ownership before drawing the next prompt;
- terminal-generated interrupts target the foreground process group;
- background jobs remain outside foreground terminal ownership;
- stopped and completed job notices are displayed at prompt-safe boundaries.
A top-level foreground chain consisting of exactly one all-external pipeline can be retained as an addressable stopped job when suspended from the terminal. It can later be inspected with jobs, continued with bg, or resumed with fg.
More complex execution shapes, including mixed internal/external pipelines and longer conditional chains, do not claim identical suspend-and-retain behavior. Flash preserves cleanup and terminal ownership rather than pretending that every internal execution island can be suspended like an external process group.
Leaving an interactive session
When live jobs exist, the first interactive exit attempt is refused and lists those jobs. A second consecutive attempt proceeds by continuing stopped jobs, sending hang-up to their process groups, and waiting for completion.
Submitting another command between the two attempts resets the refusal. This ensures that the warning always describes the current job table.
No implicit deadline or destructive fallback is applied during interactive shutdown. A process that ignores hang-up can delay exit; use an explicit kill --kill %n only when destructive termination is intended.
Interactive input control
In the interactive editor:
Ctrl-Ccancels the current edit buffer and starts a fresh prompt;Ctrl-Don an empty buffer requests end of input;- parse and runtime diagnostics return control to the same session;
- lexical scope, environment, working directory, options, and current status survive recoverable diagnostics.
These editor behaviors do not apply to non-interactive script input.
Portability and operational boundaries
Language versus platform behavior
Flash language parsing, expansion, values, control flow, and status rules are platform-independent contracts. Process execution, files, native paths, signals, process groups, terminal ownership, clocks, and configuration directories cross an explicit platform boundary.
A target may support ordinary foreground execution while lacking a more advanced capability such as terminal ownership or process groups. Flash should report or deliberately degrade the affected capability rather than silently claim full job-control behavior.
Native paths and arguments
Flash source is UTF-8, but operating-system paths, environment values, and external argument units are preserved through the native platform representation where supported.
The script-argument interface is deliberately narrower: every operand after the
script path must decode as UTF-8 before the program is loaded. Arbitrary native
path and external-process argument values do not silently become script
String values.
A value containing a null byte cannot cross an external process, environment, or filesystem boundary that rejects it. Such conversion failures are runtime errors rather than string truncation.
External program availability
A script should not assume that every development-host utility is installed in FlashOS or on another host.
Prefer:
- Flash internal commands for behavior owned by the language;
- explicit executable paths when the deployment layout guarantees them;
whichwhen a script needs to inspect command resolution;- clear failure handling for optional external tools.
Successful execution on macOS or Linux does not by itself establish that the same external executable or platform capability exists in a FlashOS image.
No implicit shell compatibility
Flash deliberately does not provide:
- POSIX source compatibility;
- implicit whitespace splitting;
- implicit wildcard expansion;
- aliases that rewrite source tokens;
- an
evaloperation that reparses strings as code; - automatic execution of project-local startup files;
- hidden routing through a host default shell.
These boundaries are part of the scripting model rather than optional safety modes.
V1 compatibility and migration
Flash 1.0 is the first supported compatibility baseline. Pre-v1 Flash source, configuration, CLI behavior, editor or session state, and internal serialized or protocol data are not promised to remain compatible. Flash v1 therefore does not provide an automatic source converter, compatibility mode, migration alias, or deprecation period solely for unreleased behavior.
Migrate a pre-v1 program by rewriting it to the current Language and Scripting
guides, then run fsh format --check and fsh check before execution. Move
interactive configuration and history to the flash/ locations documented
above; pre-v1 files under older flashshell/ locations are not discovered.
Use --no-config or --no-history to isolate stale startup state. From v1.0
onward, the documented language-major and built-in-namespace policies govern
compatibility.
Related documentation
- Language Guide — Syntax, values, bindings, expressions, commands, expansion, functions, function metadata, modules, and structured pipelines.
- Architecture — Parser, runtime, platform, process, and CLI boundaries.
- Development — Build, test, lint, fuzz, checker, formatter, language-server gates, and verification procedures for the Flash workspace.
- Flash overview — Component role, design boundaries, and integration with FlashOS.
- FlashOS Getting Started — Build and boot a FlashOS image.
- FlashOS Verification — Distinguish host checks, target builds, image validation, and runtime evidence.