Program input and output
A few built-ins connect a program to its process, files, and other programs. They are enough to write tools such as the Cuarzo compiler, its test suite, and its bootstrap:
| Built-in | Result | Behavior |
|---|---|---|
args() |
str[] |
command-line arguments; index 0 is the program |
read_text(path) |
str |
the whole file; it must be UTF-8 without NUL bytes |
try_read_text(path) |
Result[str] |
the same, or an error instead of stopping |
write_text(path, text) |
none | replaces the file with the exact bytes of text |
append_text(path, text) |
none | adds text to the end of the file, creating it if needed |
read_bytes(path) |
bytes |
the whole file, whatever it holds |
try_read_bytes(path) |
Result[bytes] |
the same, or an error instead of stopping |
write_bytes(path, data), append_bytes(path, data) |
none | replaces the file with data, or adds data to its end |
try_write_text(path, text), try_write_bytes(path, data), try_append_text(path, text) |
Result[int] |
the same as the forms without try_, giving the count of bytes written, or an error instead of stopping |
write_atomic(path, data) |
Result[int] |
replaces the file with text or bytes so that it holds the old contents or the new, never part of them |
eprint(...) |
none | like print, but writes to standard error |
exit(status) |
none | ends the program with an integer status |
run(command) |
int |
runs a program and waits for it |
run(command, out, err) |
int |
the same, with output and errors sent to files |
make_dir(path) |
none | creates a directory and any missing parents |
list_dir(path) |
str[] |
the sorted names in a directory |
exists(path) |
bool |
whether a file or directory exists |
is_dir(path) |
bool |
whether a directory exists there |
delete_file(path) |
bool |
deletes a file and says whether it did |
rename_file(from, to) |
bool |
renames or moves a file, replacing to |
current_dir() |
str |
the directory relative paths start from |
change_dir(path) |
bool |
makes path that directory and says whether it could |
temp_dir() |
str |
the directory for temporary files |
temp_file() |
str |
creates a new empty file with a name no other has, in temp_dir(), and gives its path |
process_id() |
i64 |
the id the system gives this process |
find_program(name) |
Option[str] |
the full path of a program, as running it finds it on PATH |
A file that cannot be read or written stops the program with a runtime
error that names it, as in cannot write file: scores.txt: Permission denied;
the try_ forms give that error as a Result instead, for files a program
can live without, such as a game's best scores. Its kind says what went
wrong: "not_found", "permission", "exists", "no_space", or "io",
and "invalid" for text that is not UTF-8. write_atomic writes a new file
beside the old one, makes sure it reached the disk, and renames it over the
old one, as editors must when they save; the file keeps its permissions.
Paths are UTF-8 text, with / or, on Windows, \ between names. On
Windows they reach the system as UTF-16, so names outside ASCII work, and a
path longer than 260 characters works too.
run takes the program and its arguments as a str[], such as
run(["gcc", "--version"]), and passes each argument unchanged, without a
shell. It returns the program's exit status, or -1 when the program cannot
be started. A name with a directory, such as build/tool, is used as given; a
bare name is looked up in the PATH directories only, never in the current
directory. On Windows, run starts native programs only: a name without an
extension means .exe, and other files such as scripts are not started. In
the three-argument form, out and err are file paths that are replaced, and
null keeps that stream shared with the running program. To get what a command
writes as text, use capture from std/process.
str[] arguments = args()
if len(arguments) != 3 then
eprint("usage: copy <input> <output>")
exit(1)
end
write_text(arguments[2], read_text(arguments[1]))A file or directory that cannot be opened, read, written, or created stops the
program with a runtime error that names the path. On Windows, a path given to
the file built-ins should be ASCII; the working directory, temporary files,
arguments, and the environment handle any text. try_read_text instead
returns an error Result, such as
cannot open file for reading: notes.txt, so a program can handle a file
that may be missing.
Terminals, input, and time
These built-ins are the base for interactive command-line programs. The standard library builds colors, prompts, menus, and argument parsing on them.
| Built-in | Result | Behavior |
|---|---|---|
write(text) |
none | writes text without a line end and shows it at once |
ewrite(text) |
none | the same, to standard error |
read_line() |
Option[str] |
the next line of input without its line end, or NONE at the end |
read_input() |
str |
all of standard input, as when another program pipes into this one |
read_input_bytes() |
bytes |
all of standard input as bytes, whatever it holds |
read_stdin(n) |
bytes |
the next n bytes of standard input, fewer only at its end, for protocols that send lengths |
write_out_bytes(data), write_err_bytes(data) |
none | writes bytes to standard output or standard error exactly as they are |
read_key() |
str |
one key, such as "a", "enter", "up", or "ctrl+c"; "" at the end |
key_ready(ms) |
bool |
whether a key is waiting, waiting up to ms milliseconds for one |
term_events(on, mouse), term_mouse(on), term_read_event(ms), term_key_releases() |
the terminal's events as text, under std/term's read_event, which programs use instead |
|
desktop_start(name, id, handler), window_create(title, width, height), window_set(id, key, value), window_get(id, key), and the other desktop_ and window_ built-ins |
the application and its windows, with events as text, under std/desktop, which programs use instead | |
raw_mode(on) |
none | keys arrive one at a time, unechoed, with Ctrl+C as a key |
is_terminal() |
bool |
whether standard output is an interactive terminal |
is_input_terminal() |
bool |
whether standard input is one |
terminal_width(), terminal_height() |
int |
the terminal's size, or 80 by 24 when unknown |
env(name) |
Option[str] |
an environment variable, or NONE when it is not set |
set_env(name, value) |
none | sets an environment variable, for this program and the ones it starts |
unset_env(name) |
none | removes an environment variable |
sleep(ms) |
none | pauses for ms milliseconds |
clock_ms() |
i64 |
milliseconds from a fixed point, for measuring durations |
input_waiting() |
bool |
whether data from another program waits on standard input, a pipe or a terminal, so reading it would not wait; false for a file. Its first call makes standard input unbuffered |
lsp_socket_listen(port), lsp_socket_port(s), lsp_socket_accept(s), lsp_socket_connect(port), lsp_socket_read(s, count), lsp_socket_write(s, data), lsp_socket_close(s) |
a TCP socket on the loopback address, as the language server and its tests use; the networking of phase 14 will supersede them | |
unix_time() |
i64 |
seconds since 1970-01-01 UTC |
unix_time_ns() |
i64 |
nanoseconds since 1970-01-01 UTC, from the wall clock |
env_names() |
str[] |
the names of the variables of the environment, in no order |
local_offset(seconds) |
int |
how many seconds east of UTC local time is at that time since 1970, as the system's time zone says; 0 when it cannot tell, as for times before 1970 on Windows |
random(bound) |
int |
a random integer from 0 to bound - 1 |
random_seed(seed) |
none | makes the random numbers repeat for the same seed |
write_on_exit(text) |
none | text written when the program ends, even after an error; "" for none |
platform() |
str |
"windows", "macos", "linux", "freebsd", "openbsd", "wasi", or "unix" for another POSIX system |
arch() |
str |
the processor: "x86_64", "arm64", "wasm32", "x86", "arm", "riscv64", or "unknown" |
executable_path() |
str |
the running program's own file, as an absolute path with links followed; "" when the system will not say |
start_process(command) |
i64 |
starts a program without waiting and returns its number |
poll_process(p) |
Option[int] |
SOME(status) once it has finished, NONE while it runs |
wait_process(p) |
int |
waits for it and returns its exit status |
read_key names text keys by their character ("a", "ñ") and other keys
by name: enter, tab, backspace, escape, up, down, left,
right, home, end, insert, delete, page_up, page_down, f1 to
f12, shift+tab, ctrl+a to ctrl+z, and modified keys such as
ctrl+right or alt+x. In raw mode each key arrives at once; without it a
terminal delivers keys only after Enter. Raw mode has no effect when input is
not a terminal, and the terminal is always restored when the program exits,
even after a runtime error.
key_ready lets a program such as a game go on when no key is pressed:
if key_ready(100) then waits at most 100 milliseconds. Like read_key, it
is meant for raw mode; input that is not a terminal is always ready.
random seeds itself from the clock; its numbers are for games and samples,
not for secrets.
start_process takes the same arguments as run, so a program can show a
spinner while another one works:
i64 job = start_process(["gcc", "big.c", "-o", "big"])
int frame = 0
while true do
match poll_process(job) do
when SOME(status) then
print("\rdone:", status)
break
when NONE then
write("\r" + to_str("|/-\\"[frame % 4]))
frame += 1
sleep(100)
end
endrun and start_process also take four arguments, (command, input, output, errors), where input is a file the program reads as its standard input.
On Windows, a program turns on the console's support for colors and cursor codes when it starts, exchanges text as UTF-8, and reads its arguments as UTF-8; the console's settings are restored at exit.
Embedding files
embed(path) is the text of a file, read when the program is compiled
rather than when it runs, so the built program carries the text and needs no
file beside it:
str help = embed("help.txt")
print(help)The path must be a string literal, relative to the file that contains the
call, like an import path. The file must be UTF-8 without NUL bytes. Line ends
are normalized to \n, so a program builds the same on every system. The
compiler uses embed to carry its C runtime, which lives as ordinary C files
in compiler/runtime/.
embed_bytes(path) is the bytes of a file, whatever it holds,
such as an image or a font. embed_dir(path) is every file below a
directory, as a map from paths relative to it, with / between names, to
their bytes; the map cannot change. A program can carry a whole web
application this way and serve it from memory:
const map[str, bytes] SITE = embed_dir("web/dist", compress: true, exclude: ["*.map"])
bytes page = SITE["index.html"]Its options are given by name:
| Option | Effect |
|---|---|
compress: true |
stores the files compressed, in the LZ4 block format, and unpacks them when the map is made; a file that would not get smaller is stored as it is |
include: [...] |
takes only the files that match one of the patterns |
exclude: [...] |
leaves out the files that match one of the patterns |
In a pattern, * stands for any characters but /, ? for one, and **
for any characters, / too, so assets/**/*.js is every script below
assets. A pattern without /, such as *.map, is matched against the
name of each file, in any directory. The data is kept in the program as C
string literals of at most 4095 bytes, the longest C99 compilers must accept,
and joined the first time it is used, so megabytes compile quickly.
Build constants and build information
A constant declared with a default can be given another value when the program is built, so one source builds for development and for release:
const bool DEBUG = false
const str API_URL = "http://localhost:8080"
const int WORKERS = 2
when defined(TRACING) do
print("tracing on")
endcuarzo build -D DEBUG -D API_URL=https://example.com -D WORKERS=8 -D TRACING app.cz-D NAME=value sets the constant NAME, in any file of the program, and
-D NAME alone sets a bool to true. The value is read as the
constant's type requires: true or false, a decimal integer (with a -
if it is negative), a number such as 2.5 or 1e-3, one printable ASCII
character, or any text for a str. Other types cannot be set this way, and
a value that does not fit is a compile error at the constant. Either way,
defined(NAME) is then true in when conditions,
also for a name no constant has.
These built-ins tell a program about its build. The first three are written into the program when it is compiled:
| Built-in | Result | Behavior |
|---|---|---|
build_version() |
str |
the text given with --build-version, or "dev" |
build_commit() |
str |
the Git commit of the repository that holds the source, or "" outside one |
build_time() |
i64 |
when the program was built, in seconds since 1970; SOURCE_DATE_EPOCH sets it |
build_optimized() |
bool |
whether the C compiler optimized the program, as --optimize and --release ask |
The same source and options give the same program, byte for byte, as long
as it does not ask for build_time() without SOURCE_DATE_EPOCH, or for
build_commit() from another commit. On Windows the linker is told not to
stamp the program with the time.
--release optimizes the program, as --optimize does, and leaves its
symbols out, so it is smaller. --debug builds it for a debugger, with
symbols, unoptimized, and with its C pointing at the .cz files and lines;
see Debugging. --gui builds a program that opens windows
instead of using a terminal: on Windows it gets no console window, on macOS
it becomes an application bundle, name.app, with its Info.plist, and
elsewhere it is an ordinary program. --resources app.rc links a program
on Windows with a resource script's icon, manifest, and version
information, which windres compiles; --universal builds one program
for both of macOS's processors. cuarzo bundle uses both; see
Projects.
--target <platform>[-<arch>] builds for another system: when platform(...) and the runtime's units choose that system's code, and
the program is built with its cross compiler, the usual one for the
target unless --cc names another (or CUARZO_CC does):
x86_64-linux-gnu-gcc and aarch64-linux-gnu-gcc for Linux,
x86_64-w64-mingw32-gcc and aarch64-w64-mingw32-gcc for Windows, and
osxcross's o64-clang and oa64-clang for macOS. --sysroot <dir>
gives the target's headers and libraries, and points pkg-config at its
packages. --static links everything into the program, so it needs no
shared library: on Linux with musl's musl-gcc when it is installed and
no other compiler is named, and on Windows with MinGW's own libraries
linked in; macOS allows no such program.
cuarzo build --target windows-x86_64 tool.cz # tool.exe, from Linux
cuarzo build --target linux-arm64 --sysroot /srv/arm64 tool.cz
cuarzo build --static --release tool.cz # one file, nothing else needed