@@ -3,63 +3,94 @@ |
| 3 | 3 | |
| 4 | 4 | A shell written in Fortran. Because we can. |
| 5 | 5 | |
| 6 | | -> **Warning - macOS ARM64 Users**: Due to compiler limitations on apple silicon, command lines are limited to 127 characters. All features work (history, tab completion, syntax highlighting, etc.), but you can't type commands longer than 127 bytes. This is a fundamental limitation of both available compilers: gfortran has 7+ critical bugs (stack corruption, heap corruption, segfaults), while flang-new has a 127-byte string operation limit to prevent heap corruption. We see flang-new as the lesser evil. For details, see COMPILER_NOTES.md. |
| 7 | | - |
| 8 | 6 | ## Status |
| 9 | 7 | |
| 10 | | -**POSIX compliance**: 3,776/3,776 tests passing across 25 test suites |
| 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 |
| 11 | 12 | **bash compatibility**: ~99% |
| 12 | 13 | **Chance you'll miss the other 1%**: Low |
| 13 | 14 | |
| 14 | 15 | Turns out you can write a pretty decent shell in Fortran. Who knew. |
| 15 | 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 | + |
| 16 | 39 | ## What Works |
| 17 | 40 | |
| 18 | 41 | Pretty much everything: |
| 19 | 42 | |
| 20 | 43 | - All POSIX required features |
| 21 | 44 | - All the bash stuff people actually use |
| 22 | | -- Job control |
| 23 | | -- History with Ctrl+R |
| 24 | | -- Tab completion |
| 25 | | -- History suggestions a la fish |
| 26 | | -- fuzzy matching a la fish |
| 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 |
| 27 | 49 | - Arrays (indexed and associative) |
| 28 | | -- Parameter expansion (`${var#stuff}`, etc.) |
| 50 | +- Full parameter expansion (`${var#pattern}`, `${var//find/replace}`, `${var^^}`, etc.) |
| 29 | 51 | - Process substitution (`<(cmd)`, `>(cmd)`) |
| 30 | | -- Brace expansion (`{1..10}`) |
| 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; }`) |
| 31 | 57 | - Regex matching with capture groups (`BASH_REMATCH`) |
| 32 | | -- Vi mode (if you're into that) |
| 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) |
| 33 | 61 | |
| 34 | 62 | ## What Doesn't Work |
| 35 | 63 | |
| 36 | 64 | - Some advanced vi mode features (yank/put, marks) |
| 37 | | -- Nested brace expansion (who uses this?) |
| 38 | 65 | - Your expectations, probably |
| 39 | 66 | - More?! |
| 40 | 67 | |
| 41 | 68 | ## Building |
| 42 | 69 | |
| 43 | 70 | Requires: |
| 44 | | -- A Fortran 2018 compiler (gfortran 8+, or LLVM flang-new for macOS ARM64) |
| 71 | +- A Fortran 2018 compiler (gfortran 8+, or flang-new for macOS ARM64) |
| 45 | 72 | - GNU Make |
| 46 | | -- POSIX system (Linux, BSD, macOS) |
| 73 | +- A C compiler (gcc or clang) |
| 74 | +- POSIX system (Linux, macOS) |
| 47 | 75 | - Realistic expectations |
| 48 | 76 | |
| 49 | | -**macOS ARM64 Note**: Use `brew install llvm` to get flang-new. See the warning at the top or COMPILER_NOTES.md for details. |
| 50 | | - |
| 51 | 77 | ```bash |
| 52 | | -git clone https://github.com/FortranGoingOnForty/fortsh.git |
| 53 | | -cd fortsh |
| 54 | | -make |
| 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/ |
| 55 | 82 | ``` |
| 56 | 83 | |
| 57 | | -Binary lands in `bin/fortsh`. Shocking, I know. |
| 84 | +### Platform Matrix |
| 58 | 85 | |
| 59 | | -```bash |
| 60 | | -sudo make install # /usr/local/bin |
| 61 | | -make dev-install # ~/.local/bin |
| 62 | | -``` |
| 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`. |
| 63 | 94 | |
| 64 | 95 | ## Using It |
| 65 | 96 | |
@@ -101,7 +132,7 @@ Type a directory path, press Enter. That's it. |
| 101 | 132 | ~/Documents/ # Go to ~/Documents |
| 102 | 133 | ``` |
| 103 | 134 | |
| 104 | | -Works with Tab completion. |
| 135 | +Works with Tab completion. Valid directories highlight green. |
| 105 | 136 | |
| 106 | 137 | ### Keybindings |
| 107 | 138 | |
@@ -113,7 +144,7 @@ Works with Tab completion. |
| 113 | 144 | | Alt+Shift+Left | Previous directory | |
| 114 | 145 | | Alt+Shift+Right | Next directory | |
| 115 | 146 | |
| 116 | | -**Fuzzy search:** |
| 147 | +**Fuzzy search (requires fzf):** |
| 117 | 148 | |
| 118 | 149 | | Key | Action | |
| 119 | 150 | |-----|--------| |
@@ -129,7 +160,7 @@ Works for commands, paths, variables, and command-specific options. |
| 129 | 160 | ### Syntax Highlighting |
| 130 | 161 | |
| 131 | 162 | Colors update as you type: |
| 132 | | -- Green = valid commands |
| 163 | +- Green = valid commands and directory paths |
| 133 | 164 | - Red = invalid commands |
| 134 | 165 | - Cyan = numbers |
| 135 | 166 | - Yellow = strings |
@@ -187,6 +218,11 @@ text="Hello World" |
| 187 | 218 | echo ${text^^} # HELLO WORLD |
| 188 | 219 | echo ${text,,} # hello world |
| 189 | 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 |
| 190 | 226 | ``` |
| 191 | 227 | |
| 192 | 228 | ### Arrays (Both Kinds) |
@@ -262,6 +298,15 @@ mkdir -p project/{src,test,docs}/{main,utils} |
| 262 | 298 | touch file{1..100}.txt |
| 263 | 299 | ``` |
| 264 | 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 | + |
| 265 | 310 | ### Arithmetic |
| 266 | 311 | |
| 267 | 312 | ```bash |
@@ -278,6 +323,11 @@ for ((i=0; i<5; i++)); do |
| 278 | 323 | echo "Count: $i" |
| 279 | 324 | done |
| 280 | 325 | |
| 326 | +# Multi-variable |
| 327 | +for ((i=0, j=10; i<j; i++, j--)); do |
| 328 | + echo "$i $j" |
| 329 | +done |
| 330 | + |
| 281 | 331 | # Inline increment |
| 282 | 332 | count=0 |
| 283 | 333 | echo $((count++)) # 0 (post-increment) |
@@ -327,6 +377,19 @@ ps aux | grep fortsh | grep -v grep | awk '{print $2}' | xargs kill |
| 327 | 377 | command1 | command2 || echo "Pipeline failed with status $?" |
| 328 | 378 | ``` |
| 329 | 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 | + |
| 330 | 393 | ### Job Control |
| 331 | 394 | |
| 332 | 395 | ```bash |
@@ -352,11 +415,6 @@ echo "Job completed with status $?" |
| 352 | 415 | ### Control Flow (The Tricky Bits) |
| 353 | 416 | |
| 354 | 417 | ```bash |
| 355 | | -# C-style for loop with multiple vars |
| 356 | | -for ((i=0, j=10; i<j; i++, j--)); do |
| 357 | | - echo "$i $j" |
| 358 | | -done |
| 359 | | - |
| 360 | 418 | # Case with multiple patterns |
| 361 | 419 | case $input in |
| 362 | 420 | *.txt|*.md) |
@@ -453,13 +511,21 @@ trap - INT |
| 453 | 511 | ## Testing |
| 454 | 512 | |
| 455 | 513 | ```bash |
| 456 | | -make test-all # everything (integration + parity + POSIX) |
| 457 | | -make test-posix # POSIX compliance suite (3,776 tests) |
| 458 | | -make test-parity # bash parity tests |
| 459 | | -make test-integration # integration tests |
| 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 |
| 460 | 519 | make check # comprehensive build checks |
| 461 | 520 | ``` |
| 462 | 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 | + |
| 463 | 529 | Interactive PTY tests (Python/pexpect): |
| 464 | 530 | ```bash |
| 465 | 531 | cd tests/interactive |
@@ -478,156 +544,71 @@ All of them: `:`, `.`, `break`, `cd`, `continue`, `echo`, `eval`, `exec`, `exit` |
| 478 | 544 | |
| 479 | 545 | ### bash Compatible |
| 480 | 546 | |
| 481 | | -The useful ones: `[[`, `alias`, `bg`, `command`, `compgen`, `complete`, `declare`, `fc`, `fg`, `history`, `jobs`, `kill`, `let`, `local`, `printenv`, `shopt`, `source`, `unalias`, `which` |
| 547 | +The useful ones: `[[`, `alias`, `bg`, `command`, `compgen`, `complete`, `coproc`, `declare`, `fc`, `fg`, `history`, `jobs`, `kill`, `let`, `local`, `printenv`, `shopt`, `source`, `unalias`, `which` |
| 482 | 548 | |
| 483 | 549 | ### fortsh Specific |
| 484 | 550 | |
| 485 | 551 | - `config` - manage config files |
| 486 | 552 | - `memory` - show memory stats |
| 487 | 553 | - `perf` - show performance metrics |
| 554 | +- `help <builtin>` - detailed help for any builtin |
| 555 | +- `defun` - function definition helper |
| 488 | 556 | |
| 489 | | -Because why not. |
| 490 | | - |
| 491 | | -## macOS & Apple Silicon |
| 557 | +Every builtin has detailed help: `help cd`, `help export`, `help trap`, etc. |
| 492 | 558 | |
| 493 | | -Apple Silicon has been an adventure. Both available Fortran compilers have serious issues on ARM64, so fortsh uses a combination of compiler selection, C interop workarounds, and platform-specific code paths to produce a functional shell. For the full story, see `COMPILER_NOTES.md`. |
| 494 | | - |
| 495 | | -### The Compiler Situation |
| 496 | | - |
| 497 | | -| Platform | Compiler | Status | |
| 498 | | -|----------|----------|--------| |
| 499 | | -| Linux | gfortran | Primary target, no issues | |
| 500 | | -| macOS Intel | gfortran | Works with `-frecursive` | |
| 501 | | -| macOS ARM64 | flang-new (LLVM) | Required — gfortran has 7+ critical bugs | |
| 559 | +## macOS ARM64 Notes |
| 502 | 560 | |
| 503 | | -**Why not gfortran on Apple Silicon?** It has at least 8 confirmed bugs that make it unusable: |
| 561 | +Both Fortran compilers have issues on Apple Silicon. fortsh uses flang-new (LLVM) with C interop workarounds. The Makefile handles everything automatically. |
| 504 | 562 | |
| 505 | | -1. Stack corruption on arrays >600KB |
| 506 | | -2. Deferred-length allocatable strings lose their length descriptor |
| 507 | | -3. `intent(out)` subroutine return epilogue segfaults |
| 508 | | -4. Allocatable string assignment corrupts the heap |
| 509 | | -5. Automatic finalization crashes |
| 510 | | -6. Substring slicing (`buffer(:length)`) segfaults |
| 511 | | -7. Empty string assignment (`buffer = ''`) corrupts the heap |
| 512 | | -8. `flush()` in tight loops corrupts the heap |
| 563 | +Install flang-new via `brew install flang`. See `COMPILER_NOTES.md` for the full story on compiler bugs and workarounds. |
| 513 | 564 | |
| 514 | | -Install flang-new via `brew install llvm`. The Makefile auto-detects ARM64 and switches compilers. |
| 515 | | - |
| 516 | | -### The flang-new 128-Byte Limit |
| 517 | | - |
| 518 | | -flang-new is far more stable, but has one glaring limitation: string buffers larger than 128 bytes cause heap corruption on substring operations and direct assignments. This means **command lines are limited to 127 characters** on Apple Silicon. |
| 519 | | - |
| 520 | | -Allocating strings >128 bytes works fine. Operating on them doesn't. We tried a "shadow buffer" pattern (1024-byte storage, 128-byte working buffer) — still limited to 128 effective bytes. |
| 521 | | - |
| 522 | | -### C String Library Workaround |
| 523 | | - |
| 524 | | -To mitigate flang-new's string bugs, fortsh includes a C string library (`src/c_interop/fortsh_strings.c`) that performs string operations outside the Fortran runtime. This is **auto-enabled on macOS ARM64** and provides: |
| 525 | | - |
| 526 | | -- Safe substring extraction (the operation that crashes flang-new) |
| 527 | | -- Buffer manipulation (insert, delete, append) without heap corruption |
| 528 | | -- Fortran-to-C string conversion with proper indexing translation |
| 529 | | - |
| 530 | | -The `buffer_ops.f90` abstraction layer routes string operations through either native Fortran (Linux) or the C library (macOS ARM64) transparently. |
| 531 | | - |
| 532 | | -Build flags: |
| 533 | | -```bash |
| 534 | | -make # auto-enables C strings on ARM64 |
| 535 | | -make NO_C_STRINGS=1 # force native Fortran strings (will crash on ARM64) |
| 536 | | -``` |
| 537 | | - |
| 538 | | -### Platform-Specific Code Paths |
| 539 | | - |
| 540 | | -Beyond the compiler, macOS differs from Linux in ways that required workarounds throughout the codebase: |
| 541 | | - |
| 542 | | -**Terminal I/O:** |
| 543 | | -- `termios_t` struct is 72 bytes on macOS vs 60 on Linux (8-byte vs 4-byte `tcflag_t`) |
| 544 | | -- Control character array (`NCCS`) is 20 on macOS vs 32 on Linux |
| 545 | | -- `TIOCGWINSZ` ioctl constant differs (`0x40087468` vs `0x5413`) |
| 546 | | -- Terminal size detection uses `tput` on macOS (direct ioctl crashes flang-new) vs ioctl on Linux |
| 547 | | - |
| 548 | | -**Signal numbers:** |
| 549 | | -- `SIGTSTP`: 18 on macOS, 20 on Linux |
| 550 | | -- `SIGCHLD`: 20 on macOS, 17 on Linux |
| 551 | | -- `SIGCONT`: 19 on macOS, 18 on Linux |
| 552 | | -- macOS does NOT ignore `SIGTSTP` (breaks `waitpid` by auto-reaping children) |
| 553 | | - |
| 554 | | -**File system:** |
| 555 | | -- `stat_t` is 96 bytes on macOS vs 144 on Linux, with different field ordering |
| 556 | | -- macOS has `st_birthtimespec` (birth time) — Linux does not |
| 557 | | -- `open()` flags differ: `O_CREAT` is `0x200` on macOS vs `0x40` on Linux |
| 558 | | - |
| 559 | | -**Other:** |
| 560 | | -- BSD `ps` doesn't support `--no-headers` (macOS uses `pid= -o comm=` format instead) |
| 561 | | -- Fortran `block` constructs crash flang-new — variables hoisted to subroutine scope |
| 562 | | -- Substring temporaries on allocatable strings trigger heap corruption — char-by-char copy used instead |
| 563 | | -- `mode_t` not passed correctly through Fortran C binding — C wrapper (`fd_wrapper.c`) casts explicitly |
| 564 | | - |
| 565 | | -### macOS ARM64 Build |
| 566 | | - |
| 567 | | -```bash |
| 568 | | -brew install llvm |
| 569 | | -git clone https://github.com/FortranGoingOnForty/fortsh.git |
| 570 | | -cd fortsh |
| 571 | | -make # auto-detects ARM64, uses flang-new + C string library |
| 572 | | -``` |
| 573 | | - |
| 574 | | -You'll see: |
| 575 | | -``` |
| 576 | | -Using flang-new on macOS ARM64 |
| 577 | | -C string library ENABLED - workaround for flang-new >128 byte bug |
| 578 | | -``` |
| 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) |
| 579 | 569 | |
| 580 | 570 | ## Known Issues |
| 581 | 571 | |
| 582 | | -- **macOS ARM64**: 127-character command line limit (flang-new string bug, see above) |
| 572 | +- macOS ARM64 has a 127-character command line limit (flang-new string buffer constraint). All features work, but long one-liners need to be broken up or put in scripts. See `COMPILER_NOTES.md`. |
| 583 | 573 | - Slower than bash for large scripts (it's Fortran, not a miracle worker) |
| 584 | | -- Some regex patterns with spaces need escaping (affects ~0.1% of use cases) |
| 585 | 574 | - Unicode support varies by system locale |
| 586 | 575 | - Will not make you coffee |
| 587 | 576 | |
| 588 | | -## Why? |
| 589 | | - |
| 590 | | -Why not? |
| 591 | | - |
| 592 | | -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. |
| 593 | | - |
| 594 | | -It's actually usable now. We're as surprised as you are. |
| 595 | | - |
| 596 | 577 | ## Project Structure |
| 597 | 578 | |
| 598 | 579 | ``` |
| 599 | 580 | src/ |
| 600 | | -├── common/ # Types, errors, perf monitoring |
| 601 | | -├── system/ # OS interface, signals, jobs |
| 602 | | -├── parsing/ # Lexer, parser, glob |
| 603 | | -├── execution/ # Command execution, builtins |
| 604 | | -├── scripting/ # Variables, control flow, expansion |
| 605 | | -├── io/ # Readline, redirection |
| 581 | +├── common/ # Types, errors, string pool, perf monitoring |
| 582 | +├── system/ # OS interface (POSIX syscalls), signals |
| 583 | +├── parsing/ # Lexer, grammar parser, AST, glob |
| 584 | +├── execution/ # AST executor, builtins, job control, pipelines |
| 585 | +├── scripting/ # Variables, expansion, control flow, completion |
| 586 | +├── io/ # Readline (~9000 lines), heredoc, fd redirection |
| 587 | +├── c_interop/ # C FFI: string ops, fd wrapper, terminal size |
| 606 | 588 | └── fortsh.f90 # Main REPL loop |
| 607 | 589 | ``` |
| 608 | 590 | |
| 609 | | -## Documentation |
| 591 | +~70,000 lines of Fortran, fully self-contained with no external Fortran library dependencies. |
| 610 | 592 | |
| 611 | | -See `docs/` for: |
| 612 | | -- `SHELL_PARITY_STATUS_2025_10_12.md` - current feature status |
| 613 | | -- Implementation docs for specific features |
| 614 | | -- POSIX compliance tracking |
| 615 | | - |
| 616 | | -Or just run `help` in the shell. |
| 593 | +## Why? |
| 617 | 594 | |
| 618 | | -## Contributing |
| 595 | +Why not? |
| 619 | 596 | |
| 620 | | -Found a bug? Cool, file an issue. |
| 621 | | -Want to add a feature? Check it's not already there (spoiler: it might be). |
| 622 | | -Want to make it faster? Please do. |
| 597 | +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. |
| 623 | 598 | |
| 624 | | -This started as a research project and somehow became production-ready. Contributions welcome. |
| 599 | +It's actually usable now. We're as surprised as you are. |
| 625 | 600 | |
| 626 | 601 | ## Standards |
| 627 | 602 | |
| 628 | 603 | POSIX.1-2017 (IEEE Std 1003.1-2017) |
| 629 | 604 | bash 5.x for extensions |
| 630 | 605 | |
| 606 | +## Contributing |
| 607 | + |
| 608 | +Found a bug? Cool, file an issue. |
| 609 | +Want to add a feature? Check it's not already there (spoiler: it might be). |
| 610 | +Want to make it faster? Please do. |
| 611 | + |
| 631 | 612 | ## License |
| 632 | 613 | |
| 633 | 614 | MIT. See LICENSE file. |