Home About Documentation Templates Examples Showcase GitHub
Theme

Scripting · CLI hosts

Run scripts & use the interactive shell.

The same native statement engine powers standalone files with nift script.f and the persistent nift interpreter.

Run a script

nift scripts/report.f
who := read()
print("hello, $[who]")
return 0

A script starts in a fresh root scope. Fallthrough and bare return exit successfully without return output; return expression renders the returned value to stdout. Parse, runtime and I/O errors use stderr and a non-zero status.

Inline programs and interactive continuation

nift -e 'name := "Ada"; print(name)'
nift -c 'name := "Ada"; print(name)'
nift -i -e 'x := 40; x += 2'

-e executes a complete Nift program supplied on the command line; -c is an exact alias. This is intentionally different from nift eval, whose contract is expression-oriented and can emit JSON. Add -i to continue into the interactive shell after a successful inline program or file, using the same live runtime: bindings and named functions created by the initial program remain available. If the initial program fails, Nift exits non-zero instead of entering the shell. Inline programs use <command-line> as cmd; arguments after the source become args, with -- available when an option-looking argument must be literal.

Programs from stdin

printf 'print("hello")\n' | nift -
nift - < scripts/report.f

nift - consumes stdin through EOF as one Nift program. It is intentionally distinct from plain nift, which starts the interactive shell. Stdin programs use <stdin> as their stable cmd identity and preserve parser source locations in diagnostics. Empty stdin is a successful empty program; a NUL byte is rejected explicitly rather than treated as a string terminator. Arguments after - populate args, with -- available for literal option-looking arguments.

Direct invocation is the public interface.

The former nift run ... and nift sh wrappers are removed. Use nift script.f and plain nift.

Executable scripts

There are two deliberately different ways to execute a Nift script. nift script.f is explicit Nift source execution and needs no shebang or execute permission. ./script.f is ordinary executable-process invocation: the OS requires the normal shebang and execute-bit semantics, exactly like Python, Bash or Ruby.

#!/usr/bin/env nift

/*
    Build and deploy the project.
*/
print("hello")
chmod +x deploy.f
./deploy.f

A shebang is treated as a script header only when #! starts the first source line; it is not Nift syntax elsewhere. The leading shebang is stripped, so the same file runs identically through nift deploy.f and ./deploy.f. A Nift script is not special-cased when executed as a path: ./script.f, ./thing.sh, ./thing.py and binaries all go through the same OS process model. ./deploy.f also works from nift and from another .f script, where it is ordinary external-command execution (never in-process evaluation).

$ ./deploy.f staging --force
./deploy.f staging --force
r := run("./deploy.f", "staging", "--force")
print(r.exit_code)

User arguments are available inside the script as an immutable args array (the script path itself is not included): ./deploy.f staging --force gives ["staging", "--force"]. nift deploy.f staging --force supplies the same user arguments without requiring executable permission. --no-process blocks external ./script.f execution (both command-style and run()); launching the script itself is how the parent process started and is not the same operation.

Under a real shebang launch, cmd preserves the script spelling supplied by the OS (for example ./deploy.f), args contains the user arguments, and cwd, environment, exit status and signals retain ordinary process semantics. Standalone execution also provides immutable cmd and args bindings. cmd is the script invocation spelling (for example ./deploy.f), while args contains only user arguments. Use -- to force later option-looking tokens into args, for example nift deploy.f -- --no-process. The interactive shell uses <repl> as its stable cmd identity and an empty args array.

Interactive shell

$ nift
~/project$ x := 10
~/project$ x++
~/project$ x
11
~/project$ ls().prettify()
[
  "README.md",
  "content",
  "docs"
]
~/project$ cd("/tmp")
/tmp$

The shell retains one root lexical environment between commands. Bare expressions display their safe compact value directly, so inspecting x, ls() or another data expression does not require print(). Use .prettify() when an expanded human-readable representation is easier to inspect. Its prompt shows the current working directory followed by $. Paths below the user home directory are shortened from /home/<user>/... to ~/...; on a colour-capable terminal the path is bold green while $ remains in the terminal default colour. cd(path) changes both the process working directory and the next prompt. Use .highlight() on a serializable value to request syntax-coloured compact inspection in an interactive colour-capable REPL; the underlying returned string remains ANSI-free.

Ordinary errors are reported without destroying the persistent session. Multiline input uses the parser-reported statement state: an incomplete prefix (an unclosed block, parenthesis, bracket or quoted string) keeps the shell reading with a continuation prompt, while a complete statement executes and a balanced-but-invalid form is reported without terminating the session.

On an interactive terminal the shell is a small line editor with TAB completion and arrow-key history. TAB completes Nift builtins, user/imported functions and current bindings, PATH executables and filesystem paths (directories gain a trailing /); a second TAB lists the remaining candidates and completed paths are quoted when they contain shell-special characters. The up/down arrows recall the persistent history, Ctrl-C cancels the current line, and Ctrl-D on an empty line exits. The same completion candidates are available non-interactively through nift complete <prefix>.

Processes and command-style shell syntax

Nift 4.4 adds direct process execution with structured results. run() captures an executable's exit code, standard output and standard error; cmd() builds streaming pipelines.

result := run("git", "status", "--short")
print(result.stdout)

upper := cmd("printf", "hello")
    .pipe(cmd("tr", "a-z", "A-Z"))
    .run()

Inside the zero-argument nift shell, command-style syntax follows familiar shell conventions where practical. Nift callables are tried first and otherwise commands fall through to executables on PATH. Unquoted arguments are literal tokens, so rm *.o and cp assets/**/* public/assets/ need no quotes. $[expr] inside a command argument interpolates the evaluated Nift expression, and ; separates commands that run unconditionally.

Executable paths (./x, ../x, /x, dir/x) are ordinary external commands even without arguments — from the shell and from script land alike — so ./scripts/build.f --production runs the executable, never an in-process evaluation. TAB completion discovers these paths naturally (directories gain a trailing /), including paths containing spaces.

rm *.o
git status --short
git log | grep fix > fixes.txt
make && ./tests
who := "world"; echo hello $[who]   # hello world
echo one ; echo two

The shell loads ~/.niftrc as ordinary Nift code at startup. Native setenv()/unsetenv() changes persist for the shell session and are inherited by child processes.

Environment and host introspection

home := getenv("HOME")
all := env()
print(os())       // linux, macos or windows
print(arch())     // x86_64, arm64, x86, arm, wasm32, wasm64 or unknown

env() returns an ordinary object snapshot whose keys and values are strings. Environment names are case-sensitive on POSIX. On Windows they are treated case-insensitively when collapsing duplicate spellings, with the last observed spelling/value retained. os() reports the host operating system; arch() reports the host CPU architecture and is deliberately separate from the v4.5 target-selection contract. Embedded engines with a custom lookup-only environment provider do not silently expose the process environment through env(); snapshot enumeration fails explicitly unless the host boundary can provide it.

nift --platform=android script.f
nift script.f --platform=android
nift build --platform=android
nift script.f -- --platform=literal-argument

platform() reports the selected execution/output platform; the default is native. The global --platform=ID option may appear before or after a script/build command, while -- ends Nift option parsing. This is independent of os()/arch() and distinct from nift init --target=vercel, where --target selects a deployment-provider starter.

Native project build automation

Scripts can drive Nift's own project/build machinery directly, with no subprocess and no shell round-trip. The nearest project (a directory containing .nift/config.json) at or above the working directory is resolved automatically. Every operation returns a structured result object with ok, exit_code, affected (page names touched), errors and duration_seconds; build progress/summary output is suppressed so script stdout stays parseable.

r := build()                     // incremental; also build_all(), build_names(...), build_repair()
print(r.ok)
print(r.affected.join(","))

t := track("about", "About", "templates/main.html")
print(t.ok)
print(tracked().join(","))       // every tracked name
print(status().join(","))        // names that currently need a rebuild
print(project_root())            // the resolved project root
u := untrack("about")
print(u.ok)

Opt-in restrictions

Standalone scripts and the shell run with ordinary OS-user authority by default. Two opt-in restrictions are available for safer automation; neither is an OS security sandbox.

--no-process (also honored via NIFT_NO_PROCESS) denies external process execution on every script-reachable surface: run(), the structured cmd() pipeline API, the shell's external-command fallback, nift eval and run() inside build hooks (nift build --no-process). Nift-native filesystem operations keep working.

nift scripts/report.f --no-process
nift --no-process
nift build --no-process

--fs-root=<path> (or NIFT_FS_ROOT) confines Nift-native filesystem operations (cp/mv/rm/mkdir/touch/cat/open/ls/cd and globs) to that root; paths escaping it are rejected. It restricts only Nift's own filesystem surface — when process execution is enabled, child processes run under their own OS authority, so do not treat this as an OS sandbox.

Background jobs (v4.5)

On POSIX hosts, append & to a command or pipeline to launch it as a background process group. jobs reports stable job IDs and running/stopped/completed state. Structured run()/cmd().run() remains synchronous and separate from interactive shell jobs.

Windows currently reports interactive POSIX-style job control as unsupported rather than emulating process-group semantics it cannot guarantee.

Foreground and stopped jobs

POSIX shells also provide fg [job], bg [job], and wait [job]. Job IDs accept 1 or %1. Foreground commands and pipelines share one process group, so terminal Ctrl-C/Ctrl-Z is delivered to the job rather than the Nift shell. wait without an ID waits for all known live jobs.