markdown · 17018 bytes Raw Blame History

fortsh

(noun) : something clever never

A shell written in Fortran. Because we can.

Status

CI: All green across x86_64 Linux, ARM64 Linux, and macOS ARM64 (Apple Silicon) POSIX compliance: 3,632+ tests passing across 23 POSIX suites Builtin tests: 850+ passing | Integration tests: 482 passing | Stress tests: 204 passing Interactive PTY tests: 180+ passing bash compatibility: ~99% Chance you'll miss the other 1%: Low

Turns out you can write a pretty decent shell in Fortran. Who knew.

Install

Homebrew (macOS / Linux):

brew install FortranGoingOnForty/tap/fortsh

AUR (Arch Linux):

yay -S fortsh

From source:

git clone https://github.com/FortranGoingOnForty/fortsh.git
cd fortsh
make release
sudo make install    # /usr/local/bin

Binary lands in bin/fortsh. Shocking, I know.

What Works

Pretty much everything:

  • All POSIX required features
  • All the bash stuff people actually use
  • Job control (fg, bg, jobs, wait)
  • History with Ctrl+R and autosuggestions
  • Tab completion for commands, paths, and variables
  • Syntax highlighting as you type
  • Native text selection with Shift+Arrow + system clipboard (pbcopy / xclip / wl-copy / xsel)
  • Arrays (indexed and associative)
  • Full parameter expansion (${var#pattern}, ${var//find/replace}, ${var^^}, etc.)
  • Process substitution (<(cmd), >(cmd))
  • Brace expansion ({1..10..2}, {a,b}{1,2})
  • C-style for loops (for ((i=0; i<10; i++)))
  • ANSI-C quoting ($'\t\n\e[31m')
  • Indirect expansion (${!ref}, ${!ref:-fallback})
  • Coprocesses (coproc { cmd; })
  • Regex matching with capture groups (BASH_REMATCH)
  • Vi and Emacs editing modes
  • Per-builtin help texts (help cd, help export, etc.)
  • fzf integration (file browser, history search, directory jump, git browser)
  • Bracketed paste mode (large pastes land atomically)

What Doesn't Work

  • Some advanced vi mode features (yank/put, marks)
  • Your expectations, probably
  • More?!

Building

Requires:

  • A Fortran 2018 compiler (gfortran 8+, or flang-new for macOS ARM64)
  • GNU Make
  • A C compiler (gcc or clang)
  • POSIX system (Linux, macOS)
  • Realistic expectations
make                # dev build (debug symbols, -O0)
make release        # production build (optimized, stripped)
make debug          # debug build with bounds checking
make clean          # remove build/ and bin/

Platform Matrix

Platform Compiler Notes
Linux x86_64 gfortran Primary target
Linux aarch64 gfortran Auto-enables C stat helpers for struct layout differences
macOS Intel gfortran Works with -frecursive
macOS ARM64 flang-new (LLVM) Required -- gfortran has 7+ critical bugs. Auto-enables C string library. Install via brew install flang.

The Makefile auto-detects your platform and selects the right compiler and flags. Just run make.

Using It

fortsh              # Interactive mode
fortsh script.sh    # Run a script
fortsh -c 'cmd'     # Run a command

It works like bash. If it doesn't, that's a bug.

Configuration

Login shell reads: /etc/fortsh/profile, ~/.fortsh_profile Interactive shell reads: /etc/fortsh/fortshrc, ~/.fortshrc Logout runs: ~/.fortsh_logout

First run offers to create default configs. Or don't. I'm not your boss.

Modern Shell Features

fish and zsh have some nice things. We have them too now.

Autosuggestions

Greyed-out suggestions appear as you type:

  • History-based (commands you've run)
  • Path-based (file/directory completions)
  • Accept with Right Arrow or Ctrl-F

cd-less Navigation

Type a directory path, press Enter. That's it.

/tmp/              # Navigate to /tmp
../                # Go up
~/Documents/       # Go to ~/Documents

Works with Tab completion. Valid directories highlight green.

Keybindings

Directory navigation:

Key Action
Alt+Shift+Up Go to parent directory
Alt+Shift+Left Previous directory
Alt+Shift+Right Next directory

Fuzzy search (requires fzf):

Key Action
Ctrl-F Search files
Alt-J Search directories
Ctrl-H Search history
Alt-G Search git files

Text selection (live since v1.7.0 — works like a GUI editor in the terminal):

Key Action
Shift+Left / Shift+Right Extend selection by character
Shift+Home / Shift+End Extend selection to line start / end
Shift+Up / Shift+Down Extend selection line-wise (Home / End on single-line prompt)
Ctrl+Shift+Left / Ctrl+Shift+Right Extend selection by word
Alt+Shift+B / Alt+Shift+F Extend selection by word (emacs-native alias)
any plain motion (Left, Home, Alt+b, ...) Collapse selection — char-motions snap to the appropriate edge
Ctrl+W or Ctrl+X Cut selection (writes to kill buffer + system clipboard)
Alt+W Copy selection (kill buffer + system clipboard, no delete)
Ctrl+Y Paste from kill buffer (deletes selection first if active)
Ctrl+V Paste from system clipboard (falls back to kill buffer if no tool)
typing a printable char Replaces the selection in place (type-over)
Backspace / Delete Removes the entire selection

System clipboard bridge auto-detects pbcopy (macOS), wl-copy (Wayland), xclip or xsel (X11) at startup. If none are installed, cut/copy still work via the in-session kill buffer.

Env flags:

  • FORTSH_DEBUG_SELECTION=1 — dump selection state to stderr on each mutation
  • FORTSH_NO_BRACKETED_PASTE=1 — disable ESC[?2004h emit (terminal-compat triage)

Tab Completion

Works for commands, paths, variables, and command-specific options.

Syntax Highlighting

Colors update as you type:

  • Green = valid commands and directory paths
  • Red = invalid commands
  • Cyan = numbers
  • Yellow = strings
  • Grey = comments

History

Persists across sessions. Only saves interactive commands (not scripts or .fortshrc).

  • Ctrl-R: search history
  • Up/Down: navigate history

Configuration in ~/.fortshrc:

export HISTFILE=~/.fortsh_history
export HISTSIZE=1000
export HISTFILESIZE=2000
export HISTCONTROL=ignoredups

Examples

Basic Variables

name="fortsh"
echo ${name}                    # fortsh
echo ${name:-default}           # fortsh (or default if unset)
echo ${name%sh}                 # fort (remove shortest suffix match)

Parameter Expansion (The Full Monty)

path="/usr/local/bin/fortsh"

# Length
echo ${#path}                   # 21

# Substring
echo ${path:0:4}                # /usr

# Remove prefix/suffix
echo ${path#*/}                 # usr/local/bin/fortsh
echo ${path##*/}                # fortsh (remove longest prefix)
echo ${path%/*}                 # /usr/local/bin
echo ${path%%/*}                # (remove longest suffix - empty)

# Replace
echo ${path/local/opt}          # /usr/opt/bin/fortsh
echo ${path//o/0}               # /usr/l0cal/bin/f0rtsh (replace all)

# Case conversion
text="Hello World"
echo ${text^^}                  # HELLO WORLD
echo ${text,,}                  # hello world
echo ${text^}                   # Hello World (first char)

# Indirect expansion
ref="path"
echo ${!ref}                    # /usr/local/bin/fortsh (value of $path)
echo ${!ref:-fallback}          # works with modifiers too

Arrays (Both Kinds)

# Indexed arrays
fruits=(apple banana cherry)
echo ${fruits[0]}               # apple
echo ${fruits[@]}               # apple banana cherry
echo ${#fruits[@]}              # 3
fruits+=(date)                  # append
echo ${fruits[@]:1:2}           # banana cherry (slice)

# Associative arrays (yes, really)
declare -A config
config[host]=localhost
config[port]=8080
config[user]=admin

echo ${config[host]}            # localhost
echo ${!config[@]}              # host port user (keys)
echo ${#config[@]}              # 3 (count)

for key in "${!config[@]}"; do
    echo "$key = ${config[$key]}"
done

Process Substitution (Actually Works)

# Compare directory listings
diff <(ls dir1) <(ls dir2)

# Multiple inputs
paste <(seq 1 5) <(seq 6 10)

# Output substitution
echo "test" | tee >(wc -c) >(wc -w) >/dev/null

Regex with Capture Groups

# Email parsing
if [[ "user@example.com" =~ ^([^@]+)@([^.]+)\.(.+)$ ]]; then
    echo "User: ${BASH_REMATCH[1]}"      # user
    echo "Domain: ${BASH_REMATCH[2]}"    # example
    echo "TLD: ${BASH_REMATCH[3]}"       # com
fi

# Version string parsing
version="v3.14.159-beta"
if [[ $version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-(.+))?$ ]]; then
    major=${BASH_REMATCH[1]}    # 3
    minor=${BASH_REMATCH[2]}    # 14
    patch=${BASH_REMATCH[3]}    # 159
    suffix=${BASH_REMATCH[5]}   # beta
fi

Brace Expansion

echo {1..10}                    # 1 2 3 4 5 6 7 8 9 10
echo {a..z}                     # a b c ... z
echo {1..20..2}                 # 1 3 5 7 9 11 13 15 17 19
echo {10..1..2}                 # 10 8 6 4 2
echo {a,b,c}{1,2}               # a1 a2 b1 b2 c1 c2

# Practical use
mkdir -p project/{src,test,docs}/{main,utils}
touch file{1..100}.txt

ANSI-C Quoting

echo $'tab:\there'              # tab:	here
echo $'line1\nline2'            # line1 (newline) line2
echo $'it\'s fine'              # it's fine
echo $'\e[31mred\e[0m'          # red (in color)

Arithmetic

x=5
y=3

echo $((x + y))                 # 8
echo $((x * y))                 # 15
echo $((x ** y))                # 125 (exponentiation)
echo $((x % y))                 # 2 (modulo)

# C-style for loops
for ((i=0; i<5; i++)); do
    echo "Count: $i"
done

# Multi-variable
for ((i=0, j=10; i<j; i++, j--)); do
    echo "$i $j"
done

# Inline increment
count=0
echo $((count++))               # 0 (post-increment)
echo $count                     # 1

Here Documents

# Basic heredoc
cat <<EOF
Line 1
Line 2 with $variables expanded
EOF

# Quoted delimiter (no expansion)
cat <<'EOF'
$variables not expanded
EOF

# Here string (shorthand)
grep pattern <<<"search this text"

# Indented heredoc
if true; then
    cat <<-EOF
	This leading tab is stripped
	So is this one
	EOF
fi

Command Substitution & Pipes

# Capture output
current_dir=$(pwd)
file_count=$(ls | wc -l)

# Nested substitution
echo "Found $(grep pattern $(find . -name '*.txt') | wc -l) matches"

# Complex pipelines
ps aux | grep fortsh | grep -v grep | awk '{print $2}' | xargs kill

# Pipeline with error handling
command1 | command2 || echo "Pipeline failed with status $?"

Coprocesses

# Named coproc
coproc WORKER { while read line; do echo "processed: $line"; done; }
echo "hello" >&${WORKER[1]}
read result <&${WORKER[0]}
echo $result    # processed: hello

# Brace group coproc
coproc { cat -n; }

Job Control

# Background job
sleep 10 &
bg_pid=$!
echo "Started job $bg_pid"

# List jobs
jobs

# Bring to foreground
fg %1

# Kill job
kill %1

# Wait for completion
wait $bg_pid
echo "Job completed with status $?"

Control Flow (The Tricky Bits)

# Case with multiple patterns
case $input in
    *.txt|*.md)
        echo "Text file"
        ;;
    [0-9]*)
        echo "Starts with number"
        ;;
    *)
        echo "Something else"
        ;;
esac

# Until loop (less common)
count=0
until [ $count -eq 5 ]; do
    echo $count
    ((count++))
done

# Nested loops with break/continue
for i in {1..3}; do
    for j in {1..3}; do
        [ $i -eq 2 ] && [ $j -eq 2 ] && continue
        echo "$i,$j"
    done
done

Functions with Local Scope

outer_var="global"

my_function() {
    local outer_var="local"    # Shadows global
    local inner_var="only here"

    echo $outer_var            # local
    return 42
}

my_function
exit_code=$?                   # 42
echo $outer_var                # global
echo $inner_var                # (empty - not in scope)

Signal Handling

# Trap signals
trap 'echo "Cleaning up..."; rm -f /tmp/tempfile; exit' INT TERM

# Trap ERR (on command failure)
trap 'echo "Command failed with exit code $?"' ERR

# Trap EXIT (always runs)
trap 'echo "Script finished"' EXIT

# Remove trap
trap - INT

Advanced Test Conditions

# File tests
[ -f file ]                    # Regular file
[ -d dir ]                     # Directory
[ -L link ]                    # Symbolic link
[ -r file ]                    # Readable
[ -w file ]                    # Writable
[ -x file ]                    # Executable
[ file1 -nt file2 ]            # file1 newer than file2

# String tests with [[ ]]
[[ $str =~ pattern ]]          # Regex match
[[ $str == *substring* ]]      # Glob match
[[ -n $str ]]                  # Non-empty
[[ -z $str ]]                  # Empty

# Numeric comparisons
[ $a -eq $b ]                  # Equal
[ $a -lt $b ]                  # Less than
[ $a -ge $b ]                  # Greater or equal

# Logical operators
[[ $a == "x" && $b == "y" ]]   # And
[[ $a == "x" || $b == "y" ]]   # Or
[[ ! $a == "x" ]]              # Not

Testing

make test-posix         # POSIX compliance (~1 min)
make test-posix-full    # all POSIX suites (~3 min)
make test-posix-quick   # fast POSIX, skip coverage (~30s)
make test-bench         # unit bench tests (memory pool, lexer, executor, C strings)
make test-all           # everything including memory pool tests
make check              # comprehensive build checks

Individual test suites:

./tests/builtins/run_builtin_tests.sh --verbose
./tests/builtins/integration/run_integration_tests.sh --verbose
./tests/builtins/test_stress.sh

Interactive PTY tests (Python/pexpect):

cd tests/interactive
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python run_tests.py

Or don't. Live dangerously.

Built-in Commands

POSIX Required

All of them: :, ., break, cd, continue, echo, eval, exec, exit, export, getopts, hash, printf, pwd, read, readonly, return, set, shift, test/[, times, trap, type, ulimit, umask, unset, wait

bash Compatible

The useful ones: [[, alias, bg, command, compgen, complete, coproc, declare, fc, fg, history, jobs, kill, let, local, printenv, shopt, source, unalias, which

fortsh Specific

  • config - manage config files
  • memory - show memory stats
  • perf - show performance metrics
  • help <builtin> - detailed help for any builtin
  • defun - function definition helper

Every builtin has detailed help: help cd, help export, help trap, etc.

macOS ARM64 Notes

Both Fortran compilers have issues on Apple Silicon. fortsh uses flang-new (LLVM) with C interop workarounds. The Makefile handles everything automatically.

Install flang-new via brew install flang. See COMPILER_NOTES.md for the full story on compiler bugs and workarounds.

Key differences from Linux builds:

  • C string library auto-enabled (works around flang-new string buffer limitations)
  • Platform-specific constants for signals, terminal I/O, file flags, and resource limits
  • Builtin output uses C-level write() to respect fd redirections (flang-new's Fortran I/O caches file descriptors)

Known Issues

  • Slower than bash for large scripts (it's Fortran, not a miracle worker)
  • Unicode support varies by system locale
  • Will not make you coffee

Project Structure

src/
├── common/          # Types, errors, string pool, perf monitoring
├── system/          # OS interface (POSIX syscalls), signals
├── parsing/         # Lexer, grammar parser, AST, glob
├── execution/       # AST executor, builtins, job control, pipelines
├── scripting/       # Variables, expansion, control flow, completion
├── io/              # Readline (~9000 lines), heredoc, fd redirection
├── c_interop/       # C FFI: string ops, fd wrapper, terminal size
└── fortsh.f90       # Main REPL loop

~70,000 lines of Fortran, fully self-contained with no external Fortran library dependencies.

Why?

Why not?

More seriously: started as "can you even do this in Fortran?" Turns out yes. Then it became "how far can this go?" Turns out pretty far.

It's actually usable now. We're as surprised as you are.

Standards

POSIX.1-2017 (IEEE Std 1003.1-2017) bash 5.x for extensions

Contributing

Found a bug? Cool, file an issue. Want to add a feature? Check it's not already there (spoiler: it might be). Want to make it faster? Please do.

License

GPL-3.0. See LICENSE file.

Repository: https://github.com/FortranGoingOnForty/fortsh Issues: https://github.com/FortranGoingOnForty/fortsh/issues POSIX Shell Spec: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html


Yes, it's really written in Fortran. Yes, it really works. No, we don't know why either.

View source
1 # fortsh
2 (noun) : something clever never
3
4 A shell written in Fortran. Because we can.
5
6 ## Status
7
8 **CI**: All green across x86_64 Linux, ARM64 Linux, and macOS ARM64 (Apple Silicon)
9 **POSIX compliance**: 3,632+ tests passing across 23 POSIX suites
10 **Builtin tests**: 850+ passing | **Integration tests**: 482 passing | **Stress tests**: 204 passing
11 **Interactive PTY tests**: 180+ passing
12 **bash compatibility**: ~99%
13 **Chance you'll miss the other 1%**: Low
14
15 Turns out you can write a pretty decent shell in Fortran. Who knew.
16
17 ## Install
18
19 **Homebrew** (macOS / Linux):
20 ```bash
21 brew install FortranGoingOnForty/tap/fortsh
22 ```
23
24 **AUR** (Arch Linux):
25 ```bash
26 yay -S fortsh
27 ```
28
29 **From source**:
30 ```bash
31 git clone https://github.com/FortranGoingOnForty/fortsh.git
32 cd fortsh
33 make release
34 sudo make install # /usr/local/bin
35 ```
36
37 Binary lands in `bin/fortsh`. Shocking, I know.
38
39 ## What Works
40
41 Pretty much everything:
42
43 - All POSIX required features
44 - All the bash stuff people actually use
45 - Job control (fg, bg, jobs, wait)
46 - History with Ctrl+R and autosuggestions
47 - Tab completion for commands, paths, and variables
48 - Syntax highlighting as you type
49 - Native text selection with Shift+Arrow + system clipboard (pbcopy / xclip / wl-copy / xsel)
50 - Arrays (indexed and associative)
51 - Full parameter expansion (`${var#pattern}`, `${var//find/replace}`, `${var^^}`, etc.)
52 - Process substitution (`<(cmd)`, `>(cmd)`)
53 - Brace expansion (`{1..10..2}`, `{a,b}{1,2}`)
54 - C-style for loops (`for ((i=0; i<10; i++))`)
55 - ANSI-C quoting (`$'\t\n\e[31m'`)
56 - Indirect expansion (`${!ref}`, `${!ref:-fallback}`)
57 - Coprocesses (`coproc { cmd; }`)
58 - Regex matching with capture groups (`BASH_REMATCH`)
59 - Vi and Emacs editing modes
60 - Per-builtin help texts (`help cd`, `help export`, etc.)
61 - fzf integration (file browser, history search, directory jump, git browser)
62 - Bracketed paste mode (large pastes land atomically)
63
64 ## What Doesn't Work
65
66 - Some advanced vi mode features (yank/put, marks)
67 - Your expectations, probably
68 - More?!
69
70 ## Building
71
72 Requires:
73 - A Fortran 2018 compiler (gfortran 8+, or flang-new for macOS ARM64)
74 - GNU Make
75 - A C compiler (gcc or clang)
76 - POSIX system (Linux, macOS)
77 - Realistic expectations
78
79 ```bash
80 make # dev build (debug symbols, -O0)
81 make release # production build (optimized, stripped)
82 make debug # debug build with bounds checking
83 make clean # remove build/ and bin/
84 ```
85
86 ### Platform Matrix
87
88 | Platform | Compiler | Notes |
89 |----------|----------|-------|
90 | Linux x86_64 | gfortran | Primary target |
91 | Linux aarch64 | gfortran | Auto-enables C stat helpers for struct layout differences |
92 | macOS Intel | gfortran | Works with `-frecursive` |
93 | macOS ARM64 | flang-new (LLVM) | Required -- gfortran has 7+ critical bugs. Auto-enables C string library. Install via `brew install flang`. |
94
95 The Makefile auto-detects your platform and selects the right compiler and flags. Just run `make`.
96
97 ## Using It
98
99 ```bash
100 fortsh # Interactive mode
101 fortsh script.sh # Run a script
102 fortsh -c 'cmd' # Run a command
103 ```
104
105 It works like bash. If it doesn't, that's a bug.
106
107 ## Configuration
108
109 Login shell reads: `/etc/fortsh/profile`, `~/.fortsh_profile`
110 Interactive shell reads: `/etc/fortsh/fortshrc`, `~/.fortshrc`
111 Logout runs: `~/.fortsh_logout`
112
113 First run offers to create default configs. Or don't. I'm not your boss.
114
115 ## Modern Shell Features
116
117 fish and zsh have some nice things. We have them too now.
118
119 ### Autosuggestions
120
121 Greyed-out suggestions appear as you type:
122
123 - History-based (commands you've run)
124 - Path-based (file/directory completions)
125 - Accept with **Right Arrow** or **Ctrl-F**
126
127 ### cd-less Navigation
128
129 Type a directory path, press Enter. That's it.
130
131 ```bash
132 /tmp/ # Navigate to /tmp
133 ../ # Go up
134 ~/Documents/ # Go to ~/Documents
135 ```
136
137 Works with Tab completion. Valid directories highlight green.
138
139 ### Keybindings
140
141 **Directory navigation:**
142
143 | Key | Action |
144 |-----|--------|
145 | Alt+Shift+Up | Go to parent directory |
146 | Alt+Shift+Left | Previous directory |
147 | Alt+Shift+Right | Next directory |
148
149 **Fuzzy search (requires fzf):**
150
151 | Key | Action |
152 |-----|--------|
153 | Ctrl-F | Search files |
154 | Alt-J | Search directories |
155 | Ctrl-H | Search history |
156 | Alt-G | Search git files |
157
158 **Text selection** (live since v1.7.0 — works like a GUI editor in the terminal):
159
160 | Key | Action |
161 |-----|--------|
162 | Shift+Left / Shift+Right | Extend selection by character |
163 | Shift+Home / Shift+End | Extend selection to line start / end |
164 | Shift+Up / Shift+Down | Extend selection line-wise (Home / End on single-line prompt) |
165 | Ctrl+Shift+Left / Ctrl+Shift+Right | Extend selection by word |
166 | Alt+Shift+B / Alt+Shift+F | Extend selection by word (emacs-native alias) |
167 | any plain motion (Left, Home, Alt+b, ...) | Collapse selection — char-motions snap to the appropriate edge |
168 | Ctrl+W or Ctrl+X | Cut selection (writes to kill buffer + system clipboard) |
169 | Alt+W | Copy selection (kill buffer + system clipboard, no delete) |
170 | Ctrl+Y | Paste from kill buffer (deletes selection first if active) |
171 | Ctrl+V | Paste from system clipboard (falls back to kill buffer if no tool) |
172 | typing a printable char | Replaces the selection in place (type-over) |
173 | Backspace / Delete | Removes the entire selection |
174
175 System clipboard bridge auto-detects `pbcopy` (macOS), `wl-copy` (Wayland), `xclip` or `xsel` (X11) at startup. If none are installed, cut/copy still work via the in-session kill buffer.
176
177 Env flags:
178 - `FORTSH_DEBUG_SELECTION=1` — dump selection state to stderr on each mutation
179 - `FORTSH_NO_BRACKETED_PASTE=1` — disable `ESC[?2004h` emit (terminal-compat triage)
180
181 ### Tab Completion
182
183 Works for commands, paths, variables, and command-specific options.
184
185 ### Syntax Highlighting
186
187 Colors update as you type:
188 - Green = valid commands and directory paths
189 - Red = invalid commands
190 - Cyan = numbers
191 - Yellow = strings
192 - Grey = comments
193
194 ### History
195
196 Persists across sessions. Only saves interactive commands (not scripts or .fortshrc).
197
198 - **Ctrl-R**: search history
199 - **Up/Down**: navigate history
200
201 Configuration in `~/.fortshrc`:
202 ```bash
203 export HISTFILE=~/.fortsh_history
204 export HISTSIZE=1000
205 export HISTFILESIZE=2000
206 export HISTCONTROL=ignoredups
207 ```
208
209 ## Examples
210
211 ### Basic Variables
212
213 ```bash
214 name="fortsh"
215 echo ${name} # fortsh
216 echo ${name:-default} # fortsh (or default if unset)
217 echo ${name%sh} # fort (remove shortest suffix match)
218 ```
219
220 ### Parameter Expansion (The Full Monty)
221
222 ```bash
223 path="/usr/local/bin/fortsh"
224
225 # Length
226 echo ${#path} # 21
227
228 # Substring
229 echo ${path:0:4} # /usr
230
231 # Remove prefix/suffix
232 echo ${path#*/} # usr/local/bin/fortsh
233 echo ${path##*/} # fortsh (remove longest prefix)
234 echo ${path%/*} # /usr/local/bin
235 echo ${path%%/*} # (remove longest suffix - empty)
236
237 # Replace
238 echo ${path/local/opt} # /usr/opt/bin/fortsh
239 echo ${path//o/0} # /usr/l0cal/bin/f0rtsh (replace all)
240
241 # Case conversion
242 text="Hello World"
243 echo ${text^^} # HELLO WORLD
244 echo ${text,,} # hello world
245 echo ${text^} # Hello World (first char)
246
247 # Indirect expansion
248 ref="path"
249 echo ${!ref} # /usr/local/bin/fortsh (value of $path)
250 echo ${!ref:-fallback} # works with modifiers too
251 ```
252
253 ### Arrays (Both Kinds)
254
255 ```bash
256 # Indexed arrays
257 fruits=(apple banana cherry)
258 echo ${fruits[0]} # apple
259 echo ${fruits[@]} # apple banana cherry
260 echo ${#fruits[@]} # 3
261 fruits+=(date) # append
262 echo ${fruits[@]:1:2} # banana cherry (slice)
263
264 # Associative arrays (yes, really)
265 declare -A config
266 config[host]=localhost
267 config[port]=8080
268 config[user]=admin
269
270 echo ${config[host]} # localhost
271 echo ${!config[@]} # host port user (keys)
272 echo ${#config[@]} # 3 (count)
273
274 for key in "${!config[@]}"; do
275 echo "$key = ${config[$key]}"
276 done
277 ```
278
279 ### Process Substitution (Actually Works)
280
281 ```bash
282 # Compare directory listings
283 diff <(ls dir1) <(ls dir2)
284
285 # Multiple inputs
286 paste <(seq 1 5) <(seq 6 10)
287
288 # Output substitution
289 echo "test" | tee >(wc -c) >(wc -w) >/dev/null
290 ```
291
292 ### Regex with Capture Groups
293
294 ```bash
295 # Email parsing
296 if [[ "user@example.com" =~ ^([^@]+)@([^.]+)\.(.+)$ ]]; then
297 echo "User: ${BASH_REMATCH[1]}" # user
298 echo "Domain: ${BASH_REMATCH[2]}" # example
299 echo "TLD: ${BASH_REMATCH[3]}" # com
300 fi
301
302 # Version string parsing
303 version="v3.14.159-beta"
304 if [[ $version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-(.+))?$ ]]; then
305 major=${BASH_REMATCH[1]} # 3
306 minor=${BASH_REMATCH[2]} # 14
307 patch=${BASH_REMATCH[3]} # 159
308 suffix=${BASH_REMATCH[5]} # beta
309 fi
310 ```
311
312 ### Brace Expansion
313
314 ```bash
315 echo {1..10} # 1 2 3 4 5 6 7 8 9 10
316 echo {a..z} # a b c ... z
317 echo {1..20..2} # 1 3 5 7 9 11 13 15 17 19
318 echo {10..1..2} # 10 8 6 4 2
319 echo {a,b,c}{1,2} # a1 a2 b1 b2 c1 c2
320
321 # Practical use
322 mkdir -p project/{src,test,docs}/{main,utils}
323 touch file{1..100}.txt
324 ```
325
326 ### ANSI-C Quoting
327
328 ```bash
329 echo $'tab:\there' # tab: here
330 echo $'line1\nline2' # line1 (newline) line2
331 echo $'it\'s fine' # it's fine
332 echo $'\e[31mred\e[0m' # red (in color)
333 ```
334
335 ### Arithmetic
336
337 ```bash
338 x=5
339 y=3
340
341 echo $((x + y)) # 8
342 echo $((x * y)) # 15
343 echo $((x ** y)) # 125 (exponentiation)
344 echo $((x % y)) # 2 (modulo)
345
346 # C-style for loops
347 for ((i=0; i<5; i++)); do
348 echo "Count: $i"
349 done
350
351 # Multi-variable
352 for ((i=0, j=10; i<j; i++, j--)); do
353 echo "$i $j"
354 done
355
356 # Inline increment
357 count=0
358 echo $((count++)) # 0 (post-increment)
359 echo $count # 1
360 ```
361
362 ### Here Documents
363
364 ```bash
365 # Basic heredoc
366 cat <<EOF
367 Line 1
368 Line 2 with $variables expanded
369 EOF
370
371 # Quoted delimiter (no expansion)
372 cat <<'EOF'
373 $variables not expanded
374 EOF
375
376 # Here string (shorthand)
377 grep pattern <<<"search this text"
378
379 # Indented heredoc
380 if true; then
381 cat <<-EOF
382 This leading tab is stripped
383 So is this one
384 EOF
385 fi
386 ```
387
388 ### Command Substitution & Pipes
389
390 ```bash
391 # Capture output
392 current_dir=$(pwd)
393 file_count=$(ls | wc -l)
394
395 # Nested substitution
396 echo "Found $(grep pattern $(find . -name '*.txt') | wc -l) matches"
397
398 # Complex pipelines
399 ps aux | grep fortsh | grep -v grep | awk '{print $2}' | xargs kill
400
401 # Pipeline with error handling
402 command1 | command2 || echo "Pipeline failed with status $?"
403 ```
404
405 ### Coprocesses
406
407 ```bash
408 # Named coproc
409 coproc WORKER { while read line; do echo "processed: $line"; done; }
410 echo "hello" >&${WORKER[1]}
411 read result <&${WORKER[0]}
412 echo $result # processed: hello
413
414 # Brace group coproc
415 coproc { cat -n; }
416 ```
417
418 ### Job Control
419
420 ```bash
421 # Background job
422 sleep 10 &
423 bg_pid=$!
424 echo "Started job $bg_pid"
425
426 # List jobs
427 jobs
428
429 # Bring to foreground
430 fg %1
431
432 # Kill job
433 kill %1
434
435 # Wait for completion
436 wait $bg_pid
437 echo "Job completed with status $?"
438 ```
439
440 ### Control Flow (The Tricky Bits)
441
442 ```bash
443 # Case with multiple patterns
444 case $input in
445 *.txt|*.md)
446 echo "Text file"
447 ;;
448 [0-9]*)
449 echo "Starts with number"
450 ;;
451 *)
452 echo "Something else"
453 ;;
454 esac
455
456 # Until loop (less common)
457 count=0
458 until [ $count -eq 5 ]; do
459 echo $count
460 ((count++))
461 done
462
463 # Nested loops with break/continue
464 for i in {1..3}; do
465 for j in {1..3}; do
466 [ $i -eq 2 ] && [ $j -eq 2 ] && continue
467 echo "$i,$j"
468 done
469 done
470 ```
471
472 ### Functions with Local Scope
473
474 ```bash
475 outer_var="global"
476
477 my_function() {
478 local outer_var="local" # Shadows global
479 local inner_var="only here"
480
481 echo $outer_var # local
482 return 42
483 }
484
485 my_function
486 exit_code=$? # 42
487 echo $outer_var # global
488 echo $inner_var # (empty - not in scope)
489 ```
490
491 ### Signal Handling
492
493 ```bash
494 # Trap signals
495 trap 'echo "Cleaning up..."; rm -f /tmp/tempfile; exit' INT TERM
496
497 # Trap ERR (on command failure)
498 trap 'echo "Command failed with exit code $?"' ERR
499
500 # Trap EXIT (always runs)
501 trap 'echo "Script finished"' EXIT
502
503 # Remove trap
504 trap - INT
505 ```
506
507 ### Advanced Test Conditions
508
509 ```bash
510 # File tests
511 [ -f file ] # Regular file
512 [ -d dir ] # Directory
513 [ -L link ] # Symbolic link
514 [ -r file ] # Readable
515 [ -w file ] # Writable
516 [ -x file ] # Executable
517 [ file1 -nt file2 ] # file1 newer than file2
518
519 # String tests with [[ ]]
520 [[ $str =~ pattern ]] # Regex match
521 [[ $str == *substring* ]] # Glob match
522 [[ -n $str ]] # Non-empty
523 [[ -z $str ]] # Empty
524
525 # Numeric comparisons
526 [ $a -eq $b ] # Equal
527 [ $a -lt $b ] # Less than
528 [ $a -ge $b ] # Greater or equal
529
530 # Logical operators
531 [[ $a == "x" && $b == "y" ]] # And
532 [[ $a == "x" || $b == "y" ]] # Or
533 [[ ! $a == "x" ]] # Not
534 ```
535
536 ## Testing
537
538 ```bash
539 make test-posix # POSIX compliance (~1 min)
540 make test-posix-full # all POSIX suites (~3 min)
541 make test-posix-quick # fast POSIX, skip coverage (~30s)
542 make test-bench # unit bench tests (memory pool, lexer, executor, C strings)
543 make test-all # everything including memory pool tests
544 make check # comprehensive build checks
545 ```
546
547 Individual test suites:
548 ```bash
549 ./tests/builtins/run_builtin_tests.sh --verbose
550 ./tests/builtins/integration/run_integration_tests.sh --verbose
551 ./tests/builtins/test_stress.sh
552 ```
553
554 Interactive PTY tests (Python/pexpect):
555 ```bash
556 cd tests/interactive
557 python -m venv .venv && source .venv/bin/activate
558 pip install -r requirements.txt
559 python run_tests.py
560 ```
561
562 Or don't. Live dangerously.
563
564 ## Built-in Commands
565
566 ### POSIX Required
567
568 All of them: `:`, `.`, `break`, `cd`, `continue`, `echo`, `eval`, `exec`, `exit`, `export`, `getopts`, `hash`, `printf`, `pwd`, `read`, `readonly`, `return`, `set`, `shift`, `test`/`[`, `times`, `trap`, `type`, `ulimit`, `umask`, `unset`, `wait`
569
570 ### bash Compatible
571
572 The useful ones: `[[`, `alias`, `bg`, `command`, `compgen`, `complete`, `coproc`, `declare`, `fc`, `fg`, `history`, `jobs`, `kill`, `let`, `local`, `printenv`, `shopt`, `source`, `unalias`, `which`
573
574 ### fortsh Specific
575
576 - `config` - manage config files
577 - `memory` - show memory stats
578 - `perf` - show performance metrics
579 - `help <builtin>` - detailed help for any builtin
580 - `defun` - function definition helper
581
582 Every builtin has detailed help: `help cd`, `help export`, `help trap`, etc.
583
584 ## macOS ARM64 Notes
585
586 Both Fortran compilers have issues on Apple Silicon. fortsh uses flang-new (LLVM) with C interop workarounds. The Makefile handles everything automatically.
587
588 Install flang-new via `brew install flang`. See `COMPILER_NOTES.md` for the full story on compiler bugs and workarounds.
589
590 Key differences from Linux builds:
591 - C string library auto-enabled (works around flang-new string buffer limitations)
592 - Platform-specific constants for signals, terminal I/O, file flags, and resource limits
593 - Builtin output uses C-level `write()` to respect fd redirections (flang-new's Fortran I/O caches file descriptors)
594
595 ## Known Issues
596
597 - Slower than bash for large scripts (it's Fortran, not a miracle worker)
598 - Unicode support varies by system locale
599 - Will not make you coffee
600
601 ## Project Structure
602
603 ```
604 src/
605 ├── common/ # Types, errors, string pool, perf monitoring
606 ├── system/ # OS interface (POSIX syscalls), signals
607 ├── parsing/ # Lexer, grammar parser, AST, glob
608 ├── execution/ # AST executor, builtins, job control, pipelines
609 ├── scripting/ # Variables, expansion, control flow, completion
610 ├── io/ # Readline (~9000 lines), heredoc, fd redirection
611 ├── c_interop/ # C FFI: string ops, fd wrapper, terminal size
612 └── fortsh.f90 # Main REPL loop
613 ```
614
615 ~70,000 lines of Fortran, fully self-contained with no external Fortran library dependencies.
616
617 ## Why?
618
619 Why not?
620
621 More seriously: started as "can you even do this in Fortran?" Turns out yes. Then it became "how far can this go?" Turns out pretty far.
622
623 It's actually usable now. We're as surprised as you are.
624
625 ## Standards
626
627 POSIX.1-2017 (IEEE Std 1003.1-2017)
628 bash 5.x for extensions
629
630 ## Contributing
631
632 Found a bug? Cool, file an issue.
633 Want to add a feature? Check it's not already there (spoiler: it might be).
634 Want to make it faster? Please do.
635
636 ## License
637
638 GPL-3.0. See LICENSE file.
639
640 ## Links
641
642 Repository: https://github.com/FortranGoingOnForty/fortsh
643 Issues: https://github.com/FortranGoingOnForty/fortsh/issues
644 POSIX Shell Spec: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
645
646 ---
647
648 *Yes, it's really written in Fortran. Yes, it really works. No, we don't know why either.*