Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

tcsh Variable Expansion: Use Word Lists and Modifiers Deliberately

Handle tcsh's list-valued variables, indexed argv access, quoting modifiers, and path transformations without confusing them with history expansion.

In tcsh, a shell variable is fundamentally a list of zero or more words, not a Bash-style scalar or array hidden behind one syntax. That model affects argument expansion, quoting, and path manipulation. It also creates a critical distinction from environment variables: set manages shell variables, while setenv manages the process environment that child programs inherit.

Variable expansion happens after tcsh has parsed an input line and applied alias substitution, but before each command executes. Unless the expansion is quoted or protected with a modifier such as :q, the resulting words may undergo further command and filename substitution. A value that looks like harmless text when printed can therefore become more than one argument or expand to matching filenames when used unquoted.

Treat variables as word lists

The set builtin assigns shell variables. A variable can contain no words, one word, or several words. The special argv variable represents the shell’s argument list, and tcsh provides indexed forms for reading individual values. This is not identical to Bash’s zero-based array indexing; consult the tcsh manual for the exact subscript rules before translating code.

set roots = ( /srv/app /srv/archive )
echo $roots
echo $roots[1]
echo $#roots

The example shows a multiword list, one indexed entry, and a count. Keep the data in a list instead of encoding several paths into one string and trying to split it later. Paths with spaces need explicit quoting and a clear decision about whether they are one word or several. Use a fixture with unusual filenames when validating any command that consumes list values.

Environment variables live in a separate namespace. printenv displays them, setenv changes them, and unsetenv removes them. An assignment with set does not automatically export a variable to child processes. Conversely, a value in the environment is not automatically the same as a tcsh list variable. Set the intended representation deliberately and check what an external program receives.

Use braces to delimit names and modifiers

The braces in ${name} make a variable boundary explicit when adjacent characters could otherwise be parsed as part of the name. They are especially important when a colon following an expansion could begin a modifier or when literal punctuation follows a path transformation.

set file = /var/log/daemon.out
echo ${file:h}
echo ${file:t}
echo ${file:r}
echo ${file:e}

These modifiers extract a path head, tail, root, and extension respectively. They are useful for composing paths and reporting names without launching an external utility. Validate the behavior for names without a slash, names without an extension, leading-dot files, and paths that end in a slash; a syntactically valid modifier does not guarantee the result is useful for every pathname shape.

tcsh allows more than one modifier in a variable expansion, so a path can be reduced in stages. Apply transformations to one known input and inspect each intermediate result before using it in a destructive command. By default, modifiers operate on the first modifiable word. The :g modifier can request global application where the syntax supports it; do not assume every modifier applies to every word automatically.

Keep expanded data from being reinterpreted

After substitution, values can be subject to later shell processing. The :q modifier quotes the substituted words so subsequent command and filename substitution does not reinterpret their contents. Double quotes have their own behavior: a multiword variable inside double quotes becomes a portion of one word with its value words separated by blanks. The two forms have different effects and should not be treated as interchangeable.

set paths = ( 'report final.txt' '*.log' )
echo ${paths:q}

Here :q is intended to prevent the literal *.log value from becoming an active wildcard during later expansion. Test exact list preservation with values containing whitespace, wildcard characters, dollar signs, quotes, and empty entries. If the value will be passed to an external program, verify its argument vector with a harmless test command rather than relying on terminal display.

Quoting is especially important when values come from command substitution, environment variables, or user input. Do not use eval to force a string into a desired number of arguments. That makes data executable shell syntax and can turn an input-validation bug into command execution. Keep values as tcsh list elements and apply quoting at the point the command is invoked.

Apply string modifiers with a defined scope

tcsh supports modifiers for substitution and transformation, including search-and-replace forms. The :s modifier can replace text in an expanded word; :g can apply supported modifiers globally; and case-conversion modifiers can change the word’s letter case. These are shell expansion operations, not a full regular-expression language. Do not assume that a replacement syntax behaves like sed, Perl, or Bash parameter expansion.

set oldname = /srv/release/app.tar.gz
echo ${oldname:t:r}

The chained :t:r operation first extracts the tail and then removes its extension, yielding the basename without the final suffix. Chaining order matters: changing it changes the intermediate word and the result. For multi-dot names, :r removes the final extension according to tcsh’s path modifier semantics; it does not necessarily remove every suffix a package format uses.

Brace the full variable-and-modifier expression when a literal colon follows it. Otherwise tcsh may interpret that colon as the start of another modifier. This parsing rule also appears in history substitution, but the two features act on different inputs: variable modifiers transform a value, while history modifiers select or transform words from a command-history event.

Inspect empty, missing, and multiword states separately

An empty list, an unset variable, and a variable containing an empty string are not interchangeable states. Shell variables can contain zero or more words; environment variables are strings. A command that expects one filename should reject a missing or multiword value rather than silently choosing the first word. Use the manual’s variable-existence tests and word counts where the distinction matters.

if ( $?target ) then
    if ( $#target == 1 ) then
        echo ${target:q}
    else
        echo 'target must contain exactly one word' >&2
    endif
else
    echo 'target is unset' >&2
endif

The example demonstrates validation before use; adapt the exact condition syntax to the tcsh version and context you support. A multiword path is usually not one valid pathname. Do not rely on numeric operations over a multword value either: tcsh documents that arithmetic uses the first word and treats a null string as zero. Validate the shape before arithmetic rather than allowing truncation to define policy.

Test the value at the external-command boundary

Terminal output can obscure argument boundaries. Use a harmless helper or a system utility with a controlled fixture to confirm how many arguments tcsh passes and what each contains. Include a filename with spaces and wildcard characters. Verify that a value intended as literal text remains literal and that list entries do not collapse into a single argument unexpectedly.

Avoid using echo as a serialization format; implementations and shell parsing can make its output ambiguous. For a one-off interactive check, quote each value deliberately and use a diagnostic command whose behavior is known. For scripts that must preserve arbitrary filenames or binary data, tcsh’s expansion model may not be the right tool; use a language and interface designed to represent that data without shell re-parsing.

Variable modifiers can make short interactive operations concise, especially for path heads, tails, and suffixes. Their safety depends on the value’s list shape, the exact modifier chain, and the subsequent shell expansion stages. Keep these operations transparent: establish the variable’s type and word count, quote data that must remain literal, and validate the final arguments before allowing a command to change files or system state.

One additional review point is the boundary between shell syntax and a variable’s characters. A dollar sign begins an expansion unless escaped in a context where tcsh recognizes that escape; single and double quotes do not have identical substitution behavior. A literal colon adjacent to a transformed value can be read as another modifier unless the expansion is braced. Prefer a short, quoted expansion with visible braces over relying on whitespace or punctuation to terminate a name implicitly.

When porting a command from Bash, do not transliterate ${file%.*} or ${path##*/} mechanically. tcsh’s modifier syntax is a different language with different list behavior and different follow-on substitutions. Write down the intended operation in terms of words and path components, check it against the tcsh manual, and test inputs with no suffix, multiple suffixes, empty values, and spaces. A successful expansion on one ordinary filename is not a portability test.

Related:

Sources:

Comments