markdown · 15473 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
  • 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)

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

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

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