markdown · 10803 bytes Raw Blame History

fortsh

A shell written in Fortran. Because we can.

Status

POSIX compliance: 100% bash compatibility: ~99% Chance you'll miss the other 1%: Low

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

What Works

Pretty much everything:

  • All POSIX required features
  • All the bash stuff people actually use
  • Job control
  • History with Ctrl+R
  • Tab completion
  • Arrays (indexed and associative)
  • Parameter expansion (${var#stuff}, etc.)
  • Process substitution (<(cmd), >(cmd))
  • Brace expansion ({1..10})
  • Regex matching with capture groups (BASH_REMATCH)
  • Vi mode (if you're into that)

What Doesn't Work

  • Programmable completion (basic completion works fine)
  • Some advanced vi mode features (yank/put, marks)
  • Nested brace expansion (who uses this?)
  • Your expectations, probably

Building

Requires:

  • A Fortran 2018 compiler (gfortran 8+, ifort 19+)
  • GNU Make
  • POSIX system (Linux, BSD, macOS)
  • Realistic expectations

macOS ARM64 Warning: gfortran has a stack frame corruption bug on Apple Silicon that breaks menu selection mode. Tab completion still works, you just can't use arrow keys to navigate. Blame the compiler, not us.

git clone https://github.com/FortranGoingOnForty/fortsh.git
cd fortsh
make

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

sudo make install    # /usr/local/bin
make dev-install    # ~/.local/bin

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 supervisor.

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)

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

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

# 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 $?"

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)

# C-style for loop with multiple vars
for ((i=0, j=10; i<j; i++, j--)); do
    echo "$i $j"
done

# 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 check

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, 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

Because why not.

Known Issues

  • macOS ARM64: Tab completion menu mode disabled due to gfortran compiler bug. Tab still completes, but you can't arrow through options. Press Tab twice and it'll tell you why. Works fine on Linux and x86 Macs.
  • Slower than bash for large scripts (it's Fortran, not a miracle worker)
  • Some regex patterns with spaces need escaping (affects ~0.1% of use cases)
  • Unicode support varies by system locale
  • Will not make you coffee

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.

Project Structure

src/
├── common/          # Types, errors, perf monitoring
├── system/          # OS interface, signals, jobs
├── parsing/         # Lexer, parser, glob
├── execution/       # Command execution, builtins
├── scripting/       # Variables, control flow, expansion
├── io/              # Readline, redirection
└── fortsh.f90       # Main REPL loop

Documentation

See docs/ for:

  • SHELL_PARITY_STATUS_2025_10_12.md - current feature status
  • Implementation docs for specific features
  • POSIX compliance tracking

Or just run help in the shell.

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.

This started as a research project and somehow became production-ready. Contributions welcome.

Standards

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

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
3 A shell written in Fortran. Because we can.
4
5 ## Status
6
7 **POSIX compliance**: 100%
8 **bash compatibility**: ~99%
9 **Chance you'll miss the other 1%**: Low
10
11 Turns out you can write a pretty decent shell in Fortran. Who knew.
12
13 ## What Works
14
15 Pretty much everything:
16
17 - All POSIX required features
18 - All the bash stuff people actually use
19 - Job control
20 - History with Ctrl+R
21 - Tab completion
22 - Arrays (indexed and associative)
23 - Parameter expansion (`${var#stuff}`, etc.)
24 - Process substitution (`<(cmd)`, `>(cmd)`)
25 - Brace expansion (`{1..10}`)
26 - Regex matching with capture groups (`BASH_REMATCH`)
27 - Vi mode (if you're into that)
28
29 ## What Doesn't Work
30
31 - Programmable completion (basic completion works fine)
32 - Some advanced vi mode features (yank/put, marks)
33 - Nested brace expansion (who uses this?)
34 - Your expectations, probably
35
36 ## Building
37
38 Requires:
39 - A Fortran 2018 compiler (gfortran 8+, ifort 19+)
40 - GNU Make
41 - POSIX system (Linux, BSD, macOS)
42 - Realistic expectations
43
44 **macOS ARM64 Warning**: gfortran has a stack frame corruption bug on Apple Silicon that breaks menu selection mode. Tab completion still works, you just can't use arrow keys to navigate. Blame the compiler, not us.
45
46 ```bash
47 git clone https://github.com/FortranGoingOnForty/fortsh.git
48 cd fortsh
49 make
50 ```
51
52 Binary lands in `bin/fortsh`. Shocking, I know.
53
54 ```bash
55 sudo make install # /usr/local/bin
56 make dev-install # ~/.local/bin
57 ```
58
59 ## Using It
60
61 ```bash
62 fortsh # Interactive mode
63 fortsh script.sh # Run a script
64 fortsh -c 'cmd' # Run a command
65 ```
66
67 It works like bash. If it doesn't, that's a bug.
68
69 ## Configuration
70
71 Login shell reads: `/etc/fortsh/profile`, `~/.fortsh_profile`
72 Interactive shell reads: `/etc/fortsh/fortshrc`, `~/.fortshrc`
73 Logout runs: `~/.fortsh_logout`
74
75 First run offers to create default configs. Or don't. I'm not your supervisor.
76
77 ## Examples
78
79 ### Basic Variables
80
81 ```bash
82 name="fortsh"
83 echo ${name} # fortsh
84 echo ${name:-default} # fortsh (or default if unset)
85 echo ${name%sh} # fort (remove shortest suffix match)
86 ```
87
88 ### Parameter Expansion (The Full Monty)
89
90 ```bash
91 path="/usr/local/bin/fortsh"
92
93 # Length
94 echo ${#path} # 21
95
96 # Substring
97 echo ${path:0:4} # /usr
98
99 # Remove prefix/suffix
100 echo ${path#*/} # usr/local/bin/fortsh
101 echo ${path##*/} # fortsh (remove longest prefix)
102 echo ${path%/*} # /usr/local/bin
103 echo ${path%%/*} # (remove longest suffix - empty)
104
105 # Replace
106 echo ${path/local/opt} # /usr/opt/bin/fortsh
107 echo ${path//o/0} # /usr/l0cal/bin/f0rtsh (replace all)
108
109 # Case conversion
110 text="Hello World"
111 echo ${text^^} # HELLO WORLD
112 echo ${text,,} # hello world
113 echo ${text^} # Hello World (first char)
114 ```
115
116 ### Arrays (Both Kinds)
117
118 ```bash
119 # Indexed arrays
120 fruits=(apple banana cherry)
121 echo ${fruits[0]} # apple
122 echo ${fruits[@]} # apple banana cherry
123 echo ${#fruits[@]} # 3
124 fruits+=(date) # append
125 echo ${fruits[@]:1:2} # banana cherry (slice)
126
127 # Associative arrays (yes, really)
128 declare -A config
129 config[host]=localhost
130 config[port]=8080
131 config[user]=admin
132
133 echo ${config[host]} # localhost
134 echo ${!config[@]} # host port user (keys)
135 echo ${#config[@]} # 3 (count)
136
137 for key in "${!config[@]}"; do
138 echo "$key = ${config[$key]}"
139 done
140 ```
141
142 ### Process Substitution (Actually Works)
143
144 ```bash
145 # Compare directory listings
146 diff <(ls dir1) <(ls dir2)
147
148 # Multiple inputs
149 paste <(seq 1 5) <(seq 6 10)
150
151 # Output substitution
152 echo "test" | tee >(wc -c) >(wc -w) >/dev/null
153 ```
154
155 ### Regex with Capture Groups
156
157 ```bash
158 # Email parsing
159 if [[ "user@example.com" =~ ^([^@]+)@([^.]+)\.(.+)$ ]]; then
160 echo "User: ${BASH_REMATCH[1]}" # user
161 echo "Domain: ${BASH_REMATCH[2]}" # example
162 echo "TLD: ${BASH_REMATCH[3]}" # com
163 fi
164
165 # Version string parsing
166 version="v3.14.159-beta"
167 if [[ $version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)(-(.+))?$ ]]; then
168 major=${BASH_REMATCH[1]} # 3
169 minor=${BASH_REMATCH[2]} # 14
170 patch=${BASH_REMATCH[3]} # 159
171 suffix=${BASH_REMATCH[5]} # beta
172 fi
173 ```
174
175 ### Brace Expansion
176
177 ```bash
178 echo {1..10} # 1 2 3 4 5 6 7 8 9 10
179 echo {a..z} # a b c ... z
180 echo {1..20..2} # 1 3 5 7 9 11 13 15 17 19
181 echo {10..1..2} # 10 8 6 4 2
182 echo {a,b,c}{1,2} # a1 a2 b1 b2 c1 c2
183
184 # Practical use
185 mkdir -p project/{src,test,docs}/{main,utils}
186 touch file{1..100}.txt
187 ```
188
189 ### Arithmetic
190
191 ```bash
192 x=5
193 y=3
194
195 echo $((x + y)) # 8
196 echo $((x * y)) # 15
197 echo $((x ** y)) # 125 (exponentiation)
198 echo $((x % y)) # 2 (modulo)
199
200 # C-style for loops
201 for ((i=0; i<5; i++)); do
202 echo "Count: $i"
203 done
204
205 # Inline increment
206 count=0
207 echo $((count++)) # 0 (post-increment)
208 echo $count # 1
209 ```
210
211 ### Here Documents
212
213 ```bash
214 # Basic heredoc
215 cat <<EOF
216 Line 1
217 Line 2 with $variables expanded
218 EOF
219
220 # Quoted delimiter (no expansion)
221 cat <<'EOF'
222 $variables not expanded
223 EOF
224
225 # Here string (shorthand)
226 grep pattern <<<"search this text"
227
228 # Indented heredoc
229 if true; then
230 cat <<-EOF
231 This leading tab is stripped
232 So is this one
233 EOF
234 fi
235 ```
236
237 ### Command Substitution & Pipes
238
239 ```bash
240 # Capture output
241 current_dir=$(pwd)
242 file_count=$(ls | wc -l)
243
244 # Nested substitution
245 echo "Found $(grep pattern $(find . -name '*.txt') | wc -l) matches"
246
247 # Complex pipelines
248 ps aux | grep fortsh | grep -v grep | awk '{print $2}' | xargs kill
249
250 # Pipeline with error handling
251 command1 | command2 || echo "Pipeline failed with status $?"
252 ```
253
254 ### Job Control
255
256 ```bash
257 # Background job
258 sleep 10 &
259 bg_pid=$!
260 echo "Started job $bg_pid"
261
262 # List jobs
263 jobs
264
265 # Bring to foreground
266 fg %1
267
268 # Kill job
269 kill %1
270
271 # Wait for completion
272 wait $bg_pid
273 echo "Job completed with status $?"
274 ```
275
276 ### Control Flow (The Tricky Bits)
277
278 ```bash
279 # C-style for loop with multiple vars
280 for ((i=0, j=10; i<j; i++, j--)); do
281 echo "$i $j"
282 done
283
284 # Case with multiple patterns
285 case $input in
286 *.txt|*.md)
287 echo "Text file"
288 ;;
289 [0-9]*)
290 echo "Starts with number"
291 ;;
292 *)
293 echo "Something else"
294 ;;
295 esac
296
297 # Until loop (less common)
298 count=0
299 until [ $count -eq 5 ]; do
300 echo $count
301 ((count++))
302 done
303
304 # Nested loops with break/continue
305 for i in {1..3}; do
306 for j in {1..3}; do
307 [ $i -eq 2 ] && [ $j -eq 2 ] && continue
308 echo "$i,$j"
309 done
310 done
311 ```
312
313 ### Functions with Local Scope
314
315 ```bash
316 outer_var="global"
317
318 my_function() {
319 local outer_var="local" # Shadows global
320 local inner_var="only here"
321
322 echo $outer_var # local
323 return 42
324 }
325
326 my_function
327 exit_code=$? # 42
328 echo $outer_var # global
329 echo $inner_var # (empty - not in scope)
330 ```
331
332 ### Signal Handling
333
334 ```bash
335 # Trap signals
336 trap 'echo "Cleaning up..."; rm -f /tmp/tempfile; exit' INT TERM
337
338 # Trap ERR (on command failure)
339 trap 'echo "Command failed with exit code $?"' ERR
340
341 # Trap EXIT (always runs)
342 trap 'echo "Script finished"' EXIT
343
344 # Remove trap
345 trap - INT
346 ```
347
348 ### Advanced Test Conditions
349
350 ```bash
351 # File tests
352 [ -f file ] # Regular file
353 [ -d dir ] # Directory
354 [ -L link ] # Symbolic link
355 [ -r file ] # Readable
356 [ -w file ] # Writable
357 [ -x file ] # Executable
358 [ file1 -nt file2 ] # file1 newer than file2
359
360 # String tests with [[ ]]
361 [[ $str =~ pattern ]] # Regex match
362 [[ $str == *substring* ]] # Glob match
363 [[ -n $str ]] # Non-empty
364 [[ -z $str ]] # Empty
365
366 # Numeric comparisons
367 [ $a -eq $b ] # Equal
368 [ $a -lt $b ] # Less than
369 [ $a -ge $b ] # Greater or equal
370
371 # Logical operators
372 [[ $a == "x" && $b == "y" ]] # And
373 [[ $a == "x" || $b == "y" ]] # Or
374 [[ ! $a == "x" ]] # Not
375 ```
376
377 ## Testing
378
379 ```bash
380 make check
381 ```
382
383 Or don't. Live dangerously.
384
385 ## Built-in Commands
386
387 ### POSIX Required
388
389 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`
390
391 ### bash Compatible
392
393 The useful ones: `[[`, `alias`, `bg`, `command`, `declare`, `fc`, `fg`, `history`, `jobs`, `kill`, `let`, `local`, `printenv`, `shopt`, `source`, `unalias`, `which`
394
395 ### fortsh Specific
396
397 - `config` - manage config files
398 - `memory` - show memory stats
399 - `perf` - show performance metrics
400
401 Because why not.
402
403 ## Known Issues
404
405 - **macOS ARM64**: Tab completion menu mode disabled due to gfortran compiler bug. Tab still completes, but you can't arrow through options. Press Tab twice and it'll tell you why. Works fine on Linux and x86 Macs.
406 - Slower than bash for large scripts (it's Fortran, not a miracle worker)
407 - Some regex patterns with spaces need escaping (affects ~0.1% of use cases)
408 - Unicode support varies by system locale
409 - Will not make you coffee
410
411 ## Why?
412
413 Why not?
414
415 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.
416
417 It's actually usable now. We're as surprised as you are.
418
419 ## Project Structure
420
421 ```
422 src/
423 ├── common/ # Types, errors, perf monitoring
424 ├── system/ # OS interface, signals, jobs
425 ├── parsing/ # Lexer, parser, glob
426 ├── execution/ # Command execution, builtins
427 ├── scripting/ # Variables, control flow, expansion
428 ├── io/ # Readline, redirection
429 └── fortsh.f90 # Main REPL loop
430 ```
431
432 ## Documentation
433
434 See `docs/` for:
435 - `SHELL_PARITY_STATUS_2025_10_12.md` - current feature status
436 - Implementation docs for specific features
437 - POSIX compliance tracking
438
439 Or just run `help` in the shell.
440
441 ## Contributing
442
443 Found a bug? Cool, file an issue.
444 Want to add a feature? Check it's not already there (spoiler: it might be).
445 Want to make it faster? Please do.
446
447 This started as a research project and somehow became production-ready. Contributions welcome.
448
449 ## Standards
450
451 POSIX.1-2017 (IEEE Std 1003.1-2017)
452 bash 5.x for extensions
453
454 ## License
455
456 MIT. See LICENSE file.
457
458 ## Links
459
460 Repository: https://github.com/FortranGoingOnForty/fortsh
461 Issues: https://github.com/FortranGoingOnForty/fortsh/issues
462 POSIX Shell Spec: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
463
464 ---
465
466 *Yes, it's really written in Fortran. Yes, it really works. No, we don't know why either.*