Zsh Parameter Expansion Flags: Join, Split, Quote, and Transform Safely
Use Zsh parameter-expansion flags for list transformations while preserving word boundaries and avoiding unsafe re-evaluation.
Zsh parameter expansion can transform arrays and strings at the point where a value is expanded. Flags inside a parameter expansion can join array elements, split a scalar, quote output, remove one quoting layer, normalize case, or eliminate duplicate elements. These operations are powerful because they keep the work inside the shell, but they also make it easy to create a command line whose word boundaries differ from what the programmer intended.
The core rule is to decide whether the next command needs one string or multiple arguments. An array should usually remain an array. Joining is a serialization decision, and splitting is parsing. Quoting flags can produce shell-escaped text, but text that looks quoted is not the same as a safe argument vector and must not be passed to eval as a shortcut.
Preserve arrays when the consumer accepts arguments
Zsh arrays are lists of words. If a command accepts one argument per item, expand the array in a quoted array context rather than joining it into a string and splitting it later. Quoted array expansion preserves spaces and empty elements. This is safer and simpler than inventing a delimiter and escaping every occurrence of that delimiter in data.
items=("first file.txt" "" "report*.csv")
print -rl -- "${(@)items}"
command printf '[%s]\n' "${(@)items}"
The at-sign flag preserves each array member as a separate word within double quotes. An empty element remains an empty argument, and a wildcard stays literal data. In an ordinary unquoted context, Zsh’s default array behavior differs from other shells, but relying on implicit splitting makes code harder to review. Use the explicit array form at command boundaries.
A command may have a different contract. If it expects one comma-separated value, joining can be appropriate. If it expects a filename list, keep the items separate. Do not join a list merely to print it and then feed the printed result to another command; the representation may lose empty values, separator characters, or embedded newlines.
Join elements only for a defined text format
The join flag combines array words with a specified separator. Its output is one string and should be treated as such. Define whether empty items are meaningful, whether the separator can occur in an item, and how the downstream parser handles escaping before choosing a delimiter.
items=("alpha" "two words" "gamma")
joined="${(j:,:)items}"
print -r -- "$joined"
This example produces a comma-delimited display value. It is suitable for a human-readable summary where commas inside values are either absent or understood. It is not a reversible encoding for arbitrary strings. For structured interchange, use a format with a real escaping and parsing library rather than assuming a join operation creates a safe protocol.
Joining can occur implicitly in contexts that require one word, which is another reason to make the intended shape explicit. Inspect the number of arguments received by a test helper when changing from an array to a scalar. A reliable test prints each argument with a visible boundary marker and includes empty strings, spaces, wildcard characters, and the delimiter itself.
Split scalars with an explicit separator
The split flag divides a scalar into array words using a specified separator. It is distinct from the shell’s ordinary word-splitting rules. A delimiter is data: choose it explicitly, and decide how repeated delimiters and leading or trailing separators should behave. Avoid treating a generic comma-splitting operation as a full CSV parser because quoted fields, embedded newlines, and escaped commas require a grammar.
csv='alpha,beta,"two, words"'
fields=("${(@s:,:)csv}")
print -rl -- "${(@)fields}"
The example demonstrates a basic delimiter split, not CSV parsing. It will not understand that the comma inside quoted text belongs to a field. If the input is genuine CSV, use a CSV-aware tool or parser. The shell should not silently reinterpret a structured format with a simpler rule.
For newline-delimited text, the f flag splits at newline characters. It still cannot preserve NUL bytes because shell variables cannot contain NUL. It also does not make arbitrary line-oriented data safe when records may themselves contain line breaks. Match the representation to the source’s guarantees.
Quote and unquote with a clear boundary
The q flag quotes substituted words in a shell-reusable textual representation. The Q flag removes one level of quotes from the result. These are transformations of text, not a replacement for arrays or an input-validation strategy. If the next operation is a command invocation, keep the original values in array elements and pass the array directly.
Use quoted text only when a documented consumer explicitly expects a shell-style encoded representation. Do not concatenate user input into a command string and then evaluate that string. A rendered quote sequence can be misleading to a human and can be affected by the exact quoting mode, locale, or later expansion context.
The z flag performs shell-like lexical splitting of a string while taking quoting in that value into account. It does not execute commands, but it can be useful when the input is intentionally a command-line representation. Treat the result as parsed syntax with a known grammar, not as trusted code. If a program accepts a list of arguments, accept the list directly rather than asking users to encode it in shell syntax.
value='two words'
quoted=${(q)value}
print -r -- "$quoted"
The example produces a representation suitable for inspection or a documented text format. It deliberately does not evaluate the result. Keeping the original value and the encoded text in separate variables helps prevent accidental confusion between data and executable shell input.
Normalize values without losing their type
The u flag removes duplicate elements from an array while keeping the first occurrence. Case conversion flags can transform values, and other flags can sort or pad them. These are useful for display or normalization, but the operation can affect identifiers and file names. Do not uppercase or lowercase data unless the domain defines case-insensitive equivalence.
items=("blue" "green" "blue" "red")
unique=("${(u)items[@]}")
uppercase=("${(@U)unique}")
print -rl -- "${(@)uppercase}"
Do not remove duplicates from a list if order or repeated entries represent meaningful intent. A package list, a command-line argument list, and a sequence of operations are not sets merely because duplicates look untidy. Document whether the first or last duplicate wins and test it with representative input.
For path lists, distinguish textual uniqueness from canonical path identity. Removing identical strings does not collapse two symlink paths to the same directory. Resolving paths can also change behavior if symbolic-link spelling is significant. Choose the equality model that the application requires and avoid assuming a parameter flag performs filesystem canonicalization.
Expansion order and quoting context matter
Zsh processes parameter flags as part of a larger expansion pipeline. Some flags alter the result before word splitting, joining, filename generation, or later modifiers. Quoting the overall expansion changes whether the result stays one word or fans out into multiple arguments. Combining several flags can therefore produce subtle behavior even when each flag appears simple in isolation.
Start with one transformation per expression. Assign the result to a named array or scalar, inspect it, and then pass it to the next operation. This creates a place to test intermediate state and avoids a nested expansion that reviewers cannot reason about quickly. Use a function when the same transformation needs to be applied consistently.
Keep parameter expansion flags separate from command substitution and arithmetic expansion. A flag that re-evaluates parameter syntax can also cause command and arithmetic substitutions to be examined. Such re-evaluation is especially easy to misuse when data comes from a file or a user. Prefer explicit transformations that do not turn data back into shell syntax.
A practical test matrix
Test every transformation with an empty scalar, an empty array, one ordinary value, an element with spaces, a wildcard, the selected delimiter, a repeated delimiter, a newline, and duplicate values. Print argument counts and visible boundaries, not just a joined line. Run tests under a clean Zsh with the same options as production.
For joined formats, round-trip only if the encoding rules are documented and the parser has matching tests. For split formats, verify the exact behavior for empty leading, interior, and trailing fields. For quote flags, test what a receiving program actually gets rather than inspecting the display string by eye.
Use ShellCheck or a syntax checker where supported, but do not expect generic shell tooling to validate every Zsh parameter flag. Run behavioral tests under the Zsh version you support. The Zsh manual is the authoritative reference for flag semantics and expansion order.
Operational checklist
Keep arrays as arrays at command boundaries. Join only for a defined text format, split only with an explicit grammar, and treat quote flags as textual transformations rather than as a reason to use eval. Test empty values, delimiters, spaces, wildcard characters, and duplicate entries under the target Zsh version.
Zsh expansion flags are most maintainable when they express one clear operation at a time. Make the intermediate type visible, preserve argument boundaries, and never confuse a shell-escaped string with a safe programmatic argument vector.
Related:
- How Shell Expansion and Globbing Actually Work
- Shell Scripting Pitfalls: Quoting, Word Splitting, and Why $var Isn’t Always Safe
Sources: