Expansion

Shell expansion transforms text before command execution. This page documents all expansion types in fortsh.

Source: src/scripting/expansion.f90 (3,433 lines)

Parameter Expansion

Parameter expansion retrieves and transforms variable values.

Simple Expansion

$var        # Value of var
${var}      # Same, with explicit boundaries

Default Values

SyntaxBehavior
${var-default}Use default if var unset
${var:-default}Use default if var unset OR empty
${var=default}Set var to default if unset
${var:=default}Set var to default if unset OR empty
${var+alternate}Use alternate if var is set
${var:+alternate}Use alternate if var is set AND non-empty
${var?error}Error if var unset
${var:?error}Error if var unset OR empty

Implementation: Lines 375-527 in expansion.f90

# Examples
name=${1:-"Anonymous"}       # Default argument
${DEBUG:+echo "Debug mode"}  # Only if DEBUG set and non-empty
: ${CONFIG:=/etc/app.conf}   # Set default if unset

String Length

${#var}      # Length of var's value
${#array[@]} # Number of array elements
${#}         # Number of positional parameters ($#)

Implementation: Lines 279-311

Substring Extraction

${var:offset}          # From offset to end
${var:offset:length}   # From offset, length characters
  • Offset is 0-indexed
  • Negative offset counts from end (requires space or parentheses)

Implementation: Lines 339-370

str="Hello, World!"
echo ${str:7}      # World!
echo ${str:0:5}    # Hello
echo ${str: -6}    # World! (note the space before -)

Pattern Removal

${var#pattern}   # Remove shortest prefix match
${var##pattern}  # Remove longest prefix match
${var%pattern}   # Remove shortest suffix match
${var%%pattern}  # Remove longest suffix match

Patterns use glob syntax (*, ?, [...]).

Implementation: Prefix removal at lines 313-336, suffix at lines 250-274

file="/path/to/file.txt"
echo ${file##*/}     # file.txt (basename)
echo ${file%/*}      # /path/to (dirname)

url="https://example.com/page"
echo ${url#*://}     # example.com/page
echo ${url%%/*}      # https:

Pattern Substitution

${var/pattern/replacement}   # Replace first match
${var//pattern/replacement}  # Replace all matches
${var/#pattern/replacement}  # Replace if matches at start
${var/%pattern/replacement}  # Replace if matches at end

Implementation: Lines 225-248

text="hello hello hello"
echo ${text/hello/hi}    # hi hello hello
echo ${text//hello/hi}   # hi hi hi

path="/home/user"
echo ${path/#\/home/~}   # ~/user

Case Conversion

${var^}    # Uppercase first character
${var^^}   # Uppercase all characters
${var,}    # Lowercase first character
${var,,}   # Lowercase all characters

Alternative syntax (bash 4.4+):

${var@U}   # All uppercase
${var@L}   # All lowercase
${var@u}   # First char uppercase
${var@l}   # First char lowercase

Implementation: Lines 131-211

name="john doe"
echo ${name^}    # John doe
echo ${name^^}   # JOHN DOE

LOUD="HELLO"
echo ${LOUD,,}   # hello

Transformation

${var@Q}   # Shell-quote the value
${var@E}   # Expand escape sequences

Implementation: Lines 147-170

Array Access

${array[index]}   # Element at index
${array[@]}       # All elements (separate words)
${array[*]}       # All elements (single word with IFS)
${#array[@]}      # Number of elements
${!array[@]}      # All indices/keys

For associative arrays:

${assoc[key]}     # Value for key
${!assoc[@]}      # All keys

Implementation: Lines 48-126


Command Substitution

Execute a command and substitute its output.

$(command)    # Modern syntax (nestable)
`command`     # Legacy syntax (avoid)

Implementation: Lines 2456-2505

today=$(date +%Y-%m-%d)
files=$(ls *.txt)
count=$(wc -l < file.txt)

# Nested
version=$(cat $(which python) | head -1)

Behavior

  • Trailing newlines are removed
  • Output is subject to word splitting unless quoted
  • Can be nested with $(...)

Arithmetic Expansion

Evaluate arithmetic expressions.

$((expression))

Implementation: Lines 2437-2454, arithmetic module

Operators (by precedence)

PrecedenceOperatorsDescription
1? :Ternary conditional
2||Logical OR
3&&Logical AND
4|Bitwise OR
5^Bitwise XOR
6&Bitwise AND
7== !=Equality
8< <= > >=Comparison
9<< >>Bit shift
10+ -Addition, subtraction
11* / %Multiplication, division, modulo
12**Exponentiation
13! - + ~Unary operators
14++ --Increment, decrement

Assignment Operators

var = value
var += value   # var = var + value
var -= value
var *= value
var /= value
var %= value
var &= value
var |= value
var ^= value
var <<= value
var >>= value

Examples

echo $((2 + 3))           # 5
echo $((10 / 3))          # 3 (integer division)
echo $((2 ** 10))         # 1024
echo $((x > 0 ? x : -x))  # absolute value
echo $((0xff))            # 255 (hex)
echo $((010))             # 8 (octal)

Variables in Arithmetic

x=5
echo $((x * 2))           # 10 (no $ needed)
echo $(($x * 2))          # 10 ($ also works)
echo $((x++))             # 5, then x=6
echo $((++x))             # 7

Brace Expansion

Generate arbitrary strings.

Implementation: Lines 2821-3041

List Expansion

{item1,item2,item3}
echo {a,b,c}           # a b c
echo file.{txt,md,sh}  # file.txt file.md file.sh
mkdir -p project/{src,test,docs}

Range Expansion

{start..end}
{start..end..step}
echo {1..5}            # 1 2 3 4 5
echo {5..1}            # 5 4 3 2 1
echo {a..e}            # a b c d e
echo {1..10..2}        # 1 3 5 7 9
echo {01..10}          # 01 02 03 ... 10 (zero-padded)

Combinations

echo {a,b}{1,2}        # a1 a2 b1 b2
echo {1..3}{a..c}      # 1a 1b 1c 2a 2b 2c 3a 3b 3c

Limitations

  • Nested brace expansion is not supported: {a,{b,c}} won't work as expected
  • Alphabetic ranges require single characters

Tilde Expansion

Expand to home directories.

Implementation: Lines 3198-3233

~              # Current user's home ($HOME)
~/path         # Path relative to home
~user          # Home of specified user (NOT IMPLEMENTED)
~+             # Current directory ($PWD)
~-             # Previous directory ($OLDPWD)
cd ~/projects
cat ~/.bashrc

Glob Expansion (Pathname Expansion)

Match filenames with patterns.

Source: src/parsing/glob.f90

PatternMatches
*Zero or more characters (not starting with .)
?Exactly one character
[abc]Any one of a, b, or c
[a-z]Any character in range
[!abc]Any character except a, b, c
[^abc]Same as [!abc]
**Recursive directory matching
ls *.txt              # All .txt files
ls file?.txt          # file1.txt, fileA.txt, etc.
ls [abc]*             # Files starting with a, b, or c
ls **/test*.py        # Recursive search for test*.py

Special Cases

  • Patterns not matching anything return the literal pattern
  • Patterns starting with . must be explicitly matched
  • In [...], ] as first char is literal: []] matches ]
  • - at start or end is literal: [-a] matches - or a

Field Splitting

After expansion, unquoted results are split on IFS.

Implementation: Lines 2615-2727

IFS=:
PATH_ARRAY=($PATH)     # Split PATH on colons

IFS=$'\n'
lines=($(cat file))    # Split on newlines

IFS=
var=$(cat file)        # No splitting (preserve whitespace)

Default IFS: space, tab, newline

Whitespace vs Non-Whitespace IFS

  • Whitespace IFS: consecutive delimiters are collapsed
  • Non-whitespace IFS: each delimiter creates a field
IFS=' '
echo $(echo "a   b")   # "a b" (collapsed)

IFS=:
echo $(echo "a::b")    # "a" "" "b" (empty field preserved)

Quote Removal

After all expansions, quotes are removed:

  • Single quotes: removed entirely
  • Double quotes: removed, contents preserved
  • Backslashes: removed where they escape special characters

Implementation: Lines 2788-2818


Buffer Limits

From the source code:

BufferSize
Parameter expansion result2048 bytes
Individual variables1024 bytes
Pattern matching1024 bytes
Arithmetic expression512 bytes
Brace expansion1024 bytes

Recursion is limited to MAX_RECURSION_DEPTH = 1000 to prevent infinite loops in nested expansions.