One tilde rule
~ abbreviates $HOME, the current user’s home directory, at the start of
a word — a bare word or a double-quoted string — when it stands alone or is
followed by /. Anywhere else ~ is an ordinary character. '…' and
#'…'# are verbatim, and \~ joins the escape set so a double-quoted string
can begin with a literal ~.
Context
Each of these failed for a reason only the implementation knew:
ls "~/To process at work"handedlsa literal~: the quotes a path with spaces needs were the quotes that switched~off.let h = ~tried to execute the home directory: a bare~was a path head wherever it stood.let x = $[foo]ranfoo, andlet h = $[~]executed the home directory:$[…]leaves no node behind, so the parser re-classified the word inside it as a head.ls "$CWD/x"failed under home:$CWDwas~-abbreviated, so the string carried a~nothing expanded, andcdre-expanded~in string data to keepcd $CWDworking.
What was decided
- A word and a string read
~alike. A leading~in a double-quoted string is a splice of the same store read a bare~is (Register::Tilde), followed by ordinary content, so"~/x"and"$HOME/x"are one and the same. $HOMEis a register. A reserved register like$USER, it readsHOME, orUSERPROFILEwhere appropriate, from the effective environment, sowithin [env: [HOME: …]]is honoured;~is its abbreviation.let HOME = …is refused likelet CWD = ….~useris gone.~bob/xis an ordinary word, a relative path spelled~bob/x, andfoo~baris one word. Another user’s home, viagetpwnam(3), was a timesharing convenience, unresolvable on Windows, and the reason"~5 minutes"could not be left literal under a uniform rule.- A value is not a command.
~/bin/toolis still a path head that runs the program. A bare~, a literal word (42,true) and a$[…]block are value heads wherever they stand:let h = ~binds the home directory, and~ foo,42 foo,true fooare the checker’s T0011 (“value of type … cannot be used as a command head”), not “command not found”. $CWDis absolute. It is the logical working directory, unabbreviated. Presentation is a builtin:abbreviate-home <path>folds a leading home directory to~, and a prompt wanting the short form writes"!{abbreviate-home $CWD} ❯ ".cdresolves its argument as every path builtin does, through the shell’s Resolver: path-prefix sigils decoded, anchored against the effective cwd. Its private tilde rewrite is gone, as isdetach’s.
Two layers
Two readings of ~ remain, deliberately:
- syntax —
~at a word’s or a double-quoted string’s start, a store read fixed by the parser; - data — the path-prefix sigil language of policy entries,
globpatterns and the Resolver, decoded where ral itself consumes a path string.
Externals always receive strings verbatim: ls '~/x' hands ls the bytes
~/x, while cd '~/x' goes home, because cd consumes its argument as a
path.
Alternatives considered
- Leave
"~"literal, on the bash precedent. It does not transfer. In bash double quotes mean suppress expansion, so refusing"~"is coherent there; ral’s double quotes mean only that spaces do not separate and splices are allowed. With no splitting to suppress, a leading~losing its meaning inside them had no justification once the literal case has'…'. - Make
~a value in head position too. It loses~/bin/toolfor nothing$HOMEdoes not give. - Widen
$(…)to$(~).$(name)is “a name, delimited”; a disjunction nobody would guess. - Leave
$ENV[HOME]as the answer. The most common environment datum, reached through a map index, Unix-only (Windows bindsUSERPROFILE), and learned by failing first. - Seed
$HOMElexically. env-is-dynamic-only removed that seed because it was a second copy that drifted. A register is a read of the one authority, so that decision’s rationale stands while its surface sentence, “a bare$HOMEis now an undefined variable”, is amended.
What is deleted
~user, andget_user_homewith its fabricated/home/<name>;Unexpandable: an unsetHOMEis the one way~has no answer, soTildePath::expandanswersOption;cwd_string, the abbreviating reader behind$CWD;cd’s anddetach’s private tilde rewrites;- the runtime hint for a
~that “reached the command unexpanded”, drafted and not landed: the only~an external now sees is one its author wrote as text.
See surface-syntax for the lexing and head
classification, and docs/SPEC.md §3.5, §4.2, §6.2 and §10.2 for the language
statement.
Where
core/src/path/tilde.rs (TildePath::parse, TildePath::expand,
abbreviate_home), core/src/syntax/lexer.rs (scan_bare_word,
scan_double_quoted), core/src/syntax/parser.rs (parse_head),
core/src/types/shell/cwd.rs
(apply_chdir).
Amended 2026-10-05.
$HOMEis not a register:homeis a nullary builtin and~abbreviates it, still through its own IR node (CompKind::Tilde).$CWDand$USERwent the same way, so"$HOME/x"is spelled"~/x"and"!{abbreviate-home !{cwd}} ❯ "is the short prompt (ambient-reads-are-builtins).
Amended 2026-10-20.
~andhomenow share one spelling,TildePath::shown, which folds the expanded path throughral_core::path::shownso a Windows home reads/-separated (paths-are-shown-with-slash).Context::require_homereturns the raw home, unsetHOMEbeing its one error; each caller shows it.