Modern POSIX-compliant shell written in Fortran 2018
A production-ready shell demonstrating that Fortran can handle modern systems programming. Achieves ~99% bash compatibility with all POSIX.1-2017 required features, plus modern UX features like syntax highlighting, autosuggestions, and fuzzy search. Built from ~70,000 lines of Fortran across 116 source files.
All POSIX.1-2017 required features implemented
Parameter expansion, arrays, regex, process substitution
Real-time highlighting as you type
Fish-style suggestions from history and paths
All POSIX builtins plus bash and fortsh extensions
Search files, directories, history with Ctrl-F, Alt-J
Full vi keybindings with set -o vi, emacs by default
Zero-copy string pooling minimizes allocations
| Target | Description |
|---|---|
make | Debug build with assertions |
make release | Optimized production build (-O2) |
make clean | Remove build artifacts |
make test | Run test suite |
Must use LLVM Flang instead of gfortran due to compiler bugs:
Note: Command lines limited to 127 characters on ARM64 (compiler limitation).
On first interactive run, fortsh offers to create default configuration files.
Accept to create ~/.fortshrc with sensible defaults.
| Flag | Description |
|---|---|
-c 'cmd' | Execute command string |
-n | Syntax check only (no execution) |
-l, --login | Login shell mode |
-i | Force interactive mode |
--version | Show version |
--help | Show help |
Fortsh reads configuration in order, matching bash's sourcing behavior:
/etc/fortsh/profile - System-wide login config~/.fortsh_profile - User login config/etc/fortsh/fortshrc - System-wide config~/.fortshrc - User config#!/usr/bin/env fortsh
# Prompt customization with escape sequences
export PS1='\u@\h:\w\$ '
export PS2='> '
export PS4='+ ' # xtrace prefix
# History settings
export HISTFILE=~/.fortsh_history
export HISTSIZE=1000
export HISTFILESIZE=2000
export HISTCONTROL=ignoredups
# Aliases
alias ll='ls -lah'
alias la='ls -A'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'
# Shell options
set -o vi # Vi editing mode (or: set -o emacs)
set -o pipefail # Pipeline fails if any command fails
shopt -s nullglob # Non-matching globs expand to empty
shopt -s globstar # Enable ** for recursive matching
# Functions
mkcd() {
mkdir -p "$1" && cd "$1"
}
# PATH additions
export PATH="$HOME/.local/bin:$PATH" | Option | Type | Default | Description |
|---|---|---|---|
HISTFILE | path | ~/.fortsh_history | Location of history file |
HISTSIZE | int | 1000 | Commands to keep in memory |
HISTFILESIZE | int | 2000 | Commands to keep on disk |
HISTCONTROL | string | ignoredups | Control history recording: ignorespace, ignoredups, ignoreboth, erasedups |
| Option | Type | Default | Description |
|---|---|---|---|
$0 | string | - | Shell or script name |
$1..$9 | string | - | Positional parameters (arguments) |
$@ | array | - | All positional parameters as separate words |
$* | string | - | All positional parameters as single string |
$# | int | - | Number of positional parameters |
$? | int | - | Exit status of last command |
$$ | int | - | Process ID of shell |
$! | int | - | Process ID of last background job |
$PPID | int | - | Parent process ID |
| Escape | Meaning |
|---|---|
\u | Username |
\h | Hostname (short) |
\H | Hostname (full) |
\w | Working directory (~ for home) |
\W | Working directory basename |
\$ | # if root, $ otherwise |
\t | Time (HH:MM:SS) |
\d | Date (Day Mon Date) |
\n | Newline |
\e | Escape character (for ANSI colors) |
| Variable | Purpose |
|---|---|
PATH | Directories to search for commands |
HOME | User's home directory |
PWD | Current working directory |
OLDPWD | Previous working directory |
IFS | Internal Field Separator (default: space, tab, newline) |
SHELL | Path to current shell |
Fortsh (Fortran Shell) is a modern, feature-rich POSIX-compliant shell written entirely in Fortran 2018. What started as a research project ("can you even write a shell in Fortran?") has evolved into a production-ready alternative to bash.
Fortsh demonstrates that Fortran 2018, with its modern features like derived types, allocatable arrays, and C interoperability, is capable of building complex system software traditionally written in C. The language's strong typing and array handling actually provide benefits for parsing and string manipulation.
Fortsh follows a classic shell architecture with modern additions for safety and performance.
Input (readline)
↓
Tokenization (lexer_simple.f90)
↓
Parsing (grammar_parser.f90) → AST
↓
Expansion & Substitution
├─ Parameter expansion
├─ Command substitution $()
├─ Process substitution <() >()
└─ Glob expansion
↓
Execution (ast_executor.f90)
↓
Builtin or External Command Fortsh uses a multi-stage parser that first tokenizes input, then builds an AST (Abstract Syntax Tree) for evaluation.
The lexer (lexer_simple.f90) handles:
The grammar parser builds nodes for:
The executor (ast_executor.f90) walks the AST
and evaluates each node type appropriately.
The executor respects shell options like errexit
and properly handles the contexts where errexit should not trigger (conditionals, AND-OR lists,
negated commands, command substitution).
Fortsh uses a string pooling system to minimize memory allocations during execution.
The string_pool.f90 module provides:
Use the memory builtin to inspect current usage:
For debugging memory issues, rebuild without the pool:
Fortsh implements the complete POSIX shell specification plus most bash extensions.
All POSIX.1-2017 required shell features are implemented:
Beyond POSIX, fortsh implements most common bash features:
${var:-default} # Use default if unset
${var:=default} # Assign default if unset
${var:+alt} # Use alt if set
${var:?error} # Error if unset
${#var} # String length
${var#pattern} # Remove shortest prefix
${var##pattern} # Remove longest prefix
${var%pattern} # Remove shortest suffix
${var%%pattern} # Remove longest suffix
${var/old/new} # Replace first match
${var//old/new} # Replace all matches
${var^} # Uppercase first char
${var^^} # Uppercase all
${var,} # Lowercase first char
${var,,} # Lowercase all
${var:offset} # Substring from offset
${var:offset:len} # Substring with length # Indexed arrays
arr=(apple banana cherry)
echo ${arr[0]} # First element
echo ${arr[@]} # All elements
echo ${#arr[@]} # Array length
# Associative arrays (declare -A)
declare -A dict
dict[key]=value
echo ${dict[key]}
echo ${!dict[@]} # All keys # Input process substitution diff <(sort file1) <(sort file2) # Output process substitution command | tee >(grep ERROR > errors.log)
# Regex matching with capture groups
if [[ $str =~ ^([0-9]+)-([a-z]+)$ ]]; then
echo "Number: ${BASH_REMATCH[1]}"
echo "Word: ${BASH_REMATCH[2]}"
fi echo {1..10} # 1 2 3 4 5 6 7 8 9 10
echo {a..z} # a b c ... z
echo {1..10..2} # 1 3 5 7 9 (step)
echo file{1,2,3}.txt # file1.txt file2.txt file3.txt Fortsh includes modern interactive features built-in, without requiring plugins.
Commands, arguments, strings, and operators are highlighted in real-time as you type. Invalid commands show in red. Always enabled in interactive mode.
Fish-style suggestions from history and path completion appear as you type. Press right arrow or End to accept.
Intelligent completion for commands, files, and variables. Shows menu for multiple matches.
Extended directory navigation with stack operations:
| Key | Action |
|---|---|
| Ctrl-A | Beginning of line |
| Ctrl-E | End of line |
| Ctrl-B | Move back one character |
| Ctrl-F | Move forward one character / Fuzzy file search |
| Alt-B | Move back one word |
| Alt-F | Move forward one word |
| Ctrl-K | Kill to end of line |
| Ctrl-U | Kill entire line |
| Ctrl-W | Kill previous word |
| Ctrl-Y | Yank (paste) |
| Ctrl-L | Clear screen |
| Ctrl-R | Reverse history search |
| Ctrl-P / Up | Previous history |
| Ctrl-N / Down | Next history |
Enable with set -o vi
| Key | Action |
|---|---|
| h / l | Move left / right |
| w / b | Move word forward / backward |
| 0 / $ | Beginning / end of line |
| x | Delete character |
| dd | Delete line |
| dw | Delete word |
| cw | Change word |
| j / k | Next / previous history |
| /pattern | Search history |
| Key | Action |
|---|---|
| Ctrl-F | Search files (current directory) |
| Alt-J | Search directories |
| Ctrl-H | Search command history |
| Alt-G | Search git-tracked files |
Fortsh supports both POSIX set options and
bash-style shopt options.
Use set -o option to enable or
set +o option to disable.
Short flags like set -e also work.
| Option | Type | Default | Description |
|---|---|---|---|
-e / errexit | bool | off | Exit immediately on command failure |
-u / nounset | bool | off | Treat unset variables as error |
-o pipefail | bool | off | Pipeline fails if any command fails |
-x / xtrace | bool | off | Print commands before execution |
-v / verbose | bool | off | Print shell input lines |
-n / noexec | bool | off | Syntax check only (no execution) |
-f / noglob | bool | off | Disable filename expansion |
-C / noclobber | bool | off | Prevent > from overwriting files |
-a / allexport | bool | off | Auto-export all variables |
-m / monitor | bool | on (interactive) | Enable job control |
-o vi | bool | off | Vi editing mode |
-o emacs | bool | on | Emacs editing mode |
-o ignoreeof | bool | off | Ignore Ctrl-D for exit |
-o posix | bool | off | Strict POSIX compliance mode |
# Strict mode (recommended for scripts) set -euo pipefail # Debug mode set -x # Show all options set -o
With set -e, the shell exits on command failure.
However, errexit is suppressed in these contexts (per POSIX):
Use shopt -s option to enable or
shopt -u option to disable.
| Option | Type | Default | Description |
|---|---|---|---|
nullglob | bool | off | Non-matching globs expand to empty string |
failglob | bool | off | Non-matching globs cause error |
globstar | bool | off | Enable ** recursive matching |
nocaseglob | bool | off | Case-insensitive glob matching |
nocasematch | bool | off | Case-insensitive case/[[ matching |
extglob | bool | off | Extended glob patterns ?(pat), *(pat), etc. |
dotglob | bool | off | Include dotfiles in glob expansion |
# nullglob - non-matching patterns become empty shopt -s nullglob echo *.xyz # Empty if no .xyz files # globstar - ** matches recursively shopt -s globstar ls **/*.txt # All .txt files in any subdirectory # extglob - extended patterns shopt -s extglob rm !(important).txt # Delete all .txt except important.txt # dotglob - include hidden files shopt -s dotglob echo * # Includes .hidden files
complete -F)#!/usr/bin/env fortsh # Variables (no spaces around =) name="value" number=42 readonly constant="immutable" # Commands and pipes command1 | command2 | command3 # Redirections command > output.txt # Overwrite command >> output.txt # Append command 2>&1 # Redirect stderr to stdout command &> both.txt # Redirect both to file command < input.txt # Input redirection
# Basic here document (variables expanded) cat <<EOF Hello, $USER Today is $(date) EOF # Quoted delimiter (no expansion) cat <<'EOF' $USER is literal EOF # Strip leading tabs with <<- cat <<-EOF This line's tab is removed EOF
# Pass string as stdin grep "pattern" <<< "$variable" # Useful for single-line input read -r word <<< "hello world"
${var:-default} # Use default if unset
${var:=default} # Assign default if unset
${var:+alt} # Use alt if set
${var:?error} # Error if unset
${#var} # String length
${var#pattern} # Remove shortest prefix
${var##pattern} # Remove longest prefix
${var%pattern} # Remove shortest suffix
${var%%pattern} # Remove longest suffix
${var/old/new} # Replace first match
${var//old/new} # Replace all matches
${var^} # Uppercase first char
${var^^} # Uppercase all
${var,} # Lowercase first char
${var,,} # Lowercase all
${var:offset} # Substring from offset
${var:offset:len} # Substring with length # If statement
if [ condition ]; then
statements
elif [ condition2 ]; then
statements
else
statements
fi
# For loop (list)
for item in list; do
echo $item
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo $i
done
# While loop
while condition; do
statements
done
# Until loop
until condition; do
statements
done
# Case statement
case $var in
pattern1) statements ;;
pattern2|pattern3) statements ;;
*) default ;;
esac # File tests
[ -f file ] # Regular file exists
[ -d dir ] # Directory exists
[ -e path ] # Path exists
[ -r file ] # Readable
[ -w file ] # Writable
[ -x file ] # Executable
[ -s file ] # Non-empty file
[ -L link ] # Symbolic link
# String tests
[ -z str ] # Empty string
[ -n str ] # Non-empty string
[ s1 = s2 ] # Strings equal
[ s1 != s2 ] # Strings not equal
# Numeric tests
[ n1 -eq n2 ] # Equal
[ n1 -ne n2 ] # Not equal
[ n1 -lt n2 ] # Less than
[ n1 -le n2 ] # Less than or equal
[ n1 -gt n2 ] # Greater than
[ n1 -ge n2 ] # Greater than or equal
# Extended test (bash-style)
[[ str =~ ^pattern$ ]] # Regex match
[[ str == pattern* ]] # Glob match
echo ${BASH_REMATCH[1]} # Capture group # Function definition
greet() {
local name=$1
local greeting=${2:-"Hello"}
echo "$greeting, $name!"
return 0
}
# Call function
greet "Alice" "Hi" # Hi, Alice!
# Check return status
if greet "Bob"; then
echo "Success"
fi # Indexed array
arr=(apple banana cherry)
arr+=(date) # Append
echo ${arr[0]} # First element
echo ${arr[@]} # All elements
echo ${#arr[@]} # Array length
echo ${!arr[@]} # All indices
# Associative array
declare -A dict
dict[key]=value
dict[name]="John"
echo ${dict[key]}
echo ${!dict[@]} # All keys
unset dict[key] # Remove key # Input substitution - command output as file
diff <(sort file1) <(sort file2)
while read line; do
echo "$line"
done < <(find . -name "*.txt")
# Output substitution - write to command
command | tee >(grep ERROR > errors.log) # Arithmetic expansion
result=$((2 + 3 * 4)) # 14
((count++)) # Increment
((x = y + z)) # Assignment
# let builtin
let "x = 5 + 3"
let "x++"
# Arithmetic for loop
for ((i=0; i<10; i++)); do
echo $i
done Fortsh implements 50+ builtin commands including all POSIX requirements plus bash extensions.
The gfortran compiler has bugs on Apple Silicon. Use LLVM Flang instead:
Check for:
Try running with fortsh -x script.sh to see execution trace.
Escape spaces in character classes:
# Instead of: [[ $str =~ ^[a-z ]+ ]] # May fail # Use: [[ $str =~ ^[a-z\ ]+ ]] # Escape spaces # Or use a variable: pattern='^[a-z ]+' [[ $str =~ $pattern ]]
Check with memory builtin or disable string pool:
Ensure correct locale settings:
Check that you're in interactive mode:
Remember that errexit is disabled in:
# This won't exit: if false; then ...; fi # This will exit: set -e false # Shell exits here
| Dependency | Minimum Version | Notes |
|---|---|---|
| gfortran or flang-new | F2018 standard | Use flang on macOS ARM64 |
| GNU Make | 3.81+ | Standard on most systems |
| GCC (C compiler) | 4.9+ | For C wrapper functions |
help in fortsh for builtin documentationhelp command for specific command helpfortsh --version output