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 filesmemory- show memory statsperf- 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.
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.
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.* |