fortsh Public
Code
Use Git or checkout with SVN using the web URL.
No matching headings.
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 (
armfortas,gfortran8+, orflang-new) - 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 by default, armfortas when selected explicitly |
The Makefile has an armfortas profile; flang-new remains the safe default and auto-enables the C string library. |
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 mutationFORTSH_NO_BRACKETED_PASTE=1— disableESC[?2004hemit (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 filesmemory- show memory statsperf- show performance metricshelp <builtin>- detailed help for any builtindefun- function definition helper
Every builtin has detailed help: help cd, help export, help trap, etc.
macOS ARM64 Notes
Apple Silicon builds still default to flang-new (LLVM) with the existing C interop workarounds. The Makefile now also understands an explicit FC=armfortas path for pure-Fortran bring-up work.
Install flang-new via brew install flang for the default path. See COMPILER_NOTES.md for the full story on compiler bugs and workarounds.
Key differences from Linux builds:
- C string library auto-enabled only on the flang-new fallback path
- 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.
Links
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. scratch