Words
A word is one atom: a bare word, a literal '…', an interpolation "…",
or a splice ($name, $[…], !{…}). Whitespace separates words. Two
atoms with nothing between them are a parse error, never two arguments and
never one. Text is built in exactly one place, inside "…", and that
boundary is visible in the source. There is no gluing.
The rule
§3.5 of the spec lists the characters that end a bare word: ', ", $,
!, and the structural punctuation. That list is right and stays. What it
describes, though, is where an atom ends, and an atom is not yet a word: the
rule above adds the word level. Where the grammar itself attaches one form to
another — an index after a variable or a forced block ($h[k], !{f}[k]), a
spread before a list (...$xs), a redirect before its target (>file) —
adjacency is a production and is unaffected. Everywhere else, an atom
touching an atom is refused:
--prefix=$d error: `--prefix=` and `$d` touch
--prefix='/opt/my dir' error: `--prefix=` and `'/opt/my dir'` touch
$dir/file error: `$dir` and `/file` touch
$n.txt error: `$n` and `.txt` touch
'a'"b"'c' error: `'a'` and `"b"` touchThe diagnostic names both readings, because the parser cannot know which was
meant: insert a space if these are two arguments; write "--prefix=$d" if
they are one. The suggested spelling is built from the parsed atoms, not by
wrapping the source text in quotes: C:\tmp\$leaf must come back as
"C:\\tmp\\$leaf", since a bare word processes no escapes and an
interpolation does.
Two things that look like the same question are not. a\ b is two words,
a\ and b: backslash is an ordinary character so that C:\Users\x is one
word, and the space between them is a real separator. And sh’s
'it'\''s' — what git, Python’s shlex.quote, and Ansible emit for an
argument containing an apostrophe — is an unterminated string here, as it
always was; see The -c door below.
Why
Passing a value and constructing text are different operations, and the
source should show which one is happening. $x passes a value of any type;
"$x" renders it as text, and a list is a type error there. A rule under
which $x keeps its type but $x/ or $x'' silently becomes text makes
contact between fragments an implicit conversion operator, with its own
three-way typing (a bare numeral is a number, a lone splice is its value,
anything compound is text). ral has one conversion, interpolation, and one
syntax for it.
Removing word splitting removes sh’s worst hazard, not every hazard. What
makes --prefix=$d dangerous in sh is that $d splits after substitution,
so gluing a split thing is a lottery and everyone quotes defensively. ral
never splits: with d = 'x y', echo $d passes one argument. That makes
gluing harmless, and it is tempting to conclude that gluing is therefore
fine. But a missing space between two quoted arguments would then be valid
concatenation, and no external command’s typing can tell the resulting wrong
string from an intended one. printf '%s\n'"$x" would be one format argument
and no data. The rule above catches that before anything runs.
Partial quoting must never become house style. Once fragments glue,
“quote the parts that need it” is legal, '--prefix='$d and 'a'"b" are
idiomatic, and there is no principled line between the readable
--prefix='/opt/my dir' and the unreadable 'a'"b"'c'. That is where the
quoting folklore of the last fifty years comes from, not from gluing itself.
Keeping a quoted string a whole word keeps the discipline: separate
arguments; put constructed text inside an interpolation.
The failures that motivated this were silent. An agent with sh habits
writes $dir/file constantly and got [/tmp] [/x] without a word of warning;
the system prompt carried a line telling it to quote composite paths. A
recoverable error before execution, with the correct spelling in it, is the
fix for silence. Whether the retry it costs is a measurable tax on an agent
is a prediction; silent wrong arguments are a fact.
Alternatives
Touching fragments form one word, meaning their interpolation. The sh
rule minus splitting: --prefix=$d is "--prefix=$d", 'a'"b"'c' is
"abc", 1'2' is the text 12. It reuses interpolation’s semantics and the
IR already has the node. It was declined for the three reasons above, and
because the slogan under which it is attractive, a word is an interpolation
and quotes only admit whitespace, leaks everywhere it is pressed: a bare
fragment processes no escapes while "…" does; quotes admit : before
space, , inside […], ... at word start, and #, not only whitespace;
'…' suppresses interpolation rather than admitting anything; tilde needs
the notion bare fragment at word start, which a string has not; and a lone
bare fragment is a literal (1 is an Int, 3.10 is 3.1) while "1" is
text, so the slogan is false for exactly the words that are not
interpolations. An honest statement is a standalone atom keeps its meaning;
adjacent fragments build one string by interpolation’s scalar conversions;
quoting and postfix syntax keep their own rules — implementable, and not
one rule.
A bare word is a double-quoted string without the quotes; a quote is a
boundary. Splices are terms, quotes are delimiters of literal text, so
$dir/file, $n.txt, --prefix=$d, x=$[1+1], >$dir/out are one word
meaning what they mean inside "…", while any quote touching anything
('a'"b", --prefix='/opt/my dir', 1'2') is an error with the canonical
spelling as the hint. The bare-word scanner becomes the string scanner with
different terminators and escapes off. This is the strongest alternative: it
admits the most common sh spelling and none of the partial-quoting ones. It
was declined because it is typed atoms plus unquoted interpolation with
rules the spec would have to state one by one (below), because it admits only
part of the sh habit (--prefix="$dir" still fails, so whole-word quoting
must still be learned, from fewer spellings), and because its justifying
analogy — a already means "a" — is false already: echo hi runs a
command and "echo" hi is a type error, 007 is a number and "007" is
text.
Quotes and $ ordinary inside a word. The simplest rule of all: splices
and quotes are recognised only at word start, so --prefix=$d is the literal
bare word --prefix=$d. It fails silently in exactly the same situations,
with nothing for a diagnostic to hook on. Dominated.
An explicit string-only concatenation operator. Operands must be strings, nothing is implicitly rendered. Clean, and redundant: interpolation is that operation.
Hybrids — glue only after =, only when one fragment is literal, only
for --flag= — keep the quoting dispute and add exceptions. Refused on sight.
Footguns
Every case below was found while weighing the alternatives. Each is either an argument against gluing, a rule a gluing design would have to state, or a sharp edge of the chosen rule that its diagnostic must respect.
Silent today, loud under the rule. --prefix=$d → [--prefix=] [x y];
--prefix='/opt/my dir' → [--prefix=] [/opt/my dir]; $p/x with
p = "/tmp" → [/tmp] [/x]; $n.txt → [7] [.txt]; 'a'"b"'c' → three
arguments. foo[1] was already loud: an index attaches to the one word
before it, and a literal has nothing to index (T0063).
Greedy identifiers. - is an identifier character, so $n-1, $dir-old,
$name-v2 look up n-1, dir-old, name-v2. True inside "…" today and
under every word rule; the undefined-variable error should suggest $(n)-1.
Negation. Under any gluing rule -$n is the text -7, not an Int, and
negation is $[-$n]; let m = -$n type-checks as a String and fails only
when used arithmetically. Under the chosen rule it is an error at the parse.
An empty splice is not neutral. Under gluing, 007$empty is the string
007 while 007 is the number 7, and $xs$empty with a list is a type
error. “Appending nothing” changes type and admissibility.
Spelling or value. A numeral fragment glued as source text keeps its
spelling (007'x' → 007x) while a bound numeral glues as its value
(let n = 007; $n'x' → 7x). Numeral classification would have to
distinguish whole bare words from fragments. See
numerals-denote-numbers.
Missing separators become programs. printf '%s\n'"$x" is one format
argument; $x$y is one string where two arguments were meant; the only
gluing case that is a genuine convenience, '$HOME/'$leaf, is
"\$HOME/$leaf" here.
Where a word starts, a comment starts. # begins a comment only at the
start of a word. Under a rule where $x#note is one word, it prints
v#note; today it prints v. Changing the word rule moves the comment
boundary.
! changes role. ! ends a bare word today, so foo!bar is foo and
a force of bar. A rule that scans bare words as strings makes it the text
foo!bar and a!$b a splice: Hello! would work, and an existing program
would silently change meaning.
Indexing inside a string. Outside a string !{f}[k] indexed while
inside one "!{f}[k]" was the forced value followed by the text [k],
and $(name) indexed outside but not inside. A rule that scans bare
words as strings would have had to pick one. The chosen rule let the
two be reconciled instead: $(name) takes nothing after it anywhere,
every other splice takes [key] anywhere, and $(red)[x] outside a
string is a touching error whose one-word reading, "$(red)[x]", is
the colour-and-text the string rule gives.
Command heads. A string is not invocable ("$d/printf" hi is T0011), so a
glued head $cmd/bin hi would fail the same way. Fine, but it means gluing
does not deliver the sh habit for computed heads either, and nobody should
try to recover the bare/path/value head classification of §6.2 by inspecting
the assembled string.
Nested quotes. “A quote is a boundary” can only hold at the outer level:
x=$['a b'] is one word, the inner quote belonging to the splice. One more
rule to state.
The hint and backslashes. A bare word has no escapes and an interpolation
has, so the suggested "…" spelling is assembled from parsed atoms with
backslashes escaped, never by quoting the source text.
a\ b. Two words, by the backslash-is-ordinary rule that keeps Windows
paths whole. Not an error, and not what sh means. Documented, not fixable
without reintroducing an escape character.
The -c door
A login shell’s -c receives whatever sshd’s clients send — git-upload-pack 'it'\''s', rsync and scp invocations, ssh host cmd — and that is sh:
'it'\''s', but also &&, 2>&1, $HOME, "$@", backticks. Gluing quotes
would make the apostrophe case work and leave everything else broken, which
is the least predictable outcome of all. No word rule short of being sh fixes
this; it is a decision about the -c door (forward non-tty -c to
/bin/sh, as ral-sh does, or document that ral does not accept sh over
ssh, as fish does), and it is orthogonal to how ral’s own words are read.
Where it lives
The atom list is continues_bare_word and the quote and splice scanners in
core/src/syntax/lexer.rs; the word rule is checked once at the end of every unit
(Parser::end_unit, with touches_after), in every position, and the
grammar’s attachments (^, ..., !, an index) must touch;
the diagnostic is P0002, with a two-reading help; its sibling P0003
refuses two atoms with space between them where one value is wanted, a
collection entry. Spec
§3.5 states the rule after “Quote a word when there is any doubt”; ral.md
no longer needs to tell the agent that a composite path is one quoted word,
because the parser tells it, with the spelling.