| 1 | # FERP Makefile |
| 2 | # Fortran Expression Regular Print - A GNU grep clone |
| 3 | |
| 4 | # Compilers |
| 5 | FC = gfortran |
| 6 | CC = clang |
| 7 | |
| 8 | # Compiler flags |
| 9 | FFLAGS_COMMON = -std=f2008 -Wall -Wextra -pedantic -cpp |
| 10 | FFLAGS_DEBUG = $(FFLAGS_COMMON) -g -O0 -fcheck=all -fbacktrace -Wno-unused-dummy-argument |
| 11 | FFLAGS_RELEASE = $(FFLAGS_COMMON) -O2 -march=native -fopenmp |
| 12 | |
| 13 | CFLAGS_DEBUG = -g -O0 -Wall |
| 14 | CFLAGS_RELEASE = -O2 -march=native |
| 15 | |
| 16 | # Default to debug build (release includes OpenMP) |
| 17 | FFLAGS = $(FFLAGS_DEBUG) |
| 18 | CFLAGS = $(CFLAGS_DEBUG) |
| 19 | |
| 20 | # PCRE2 library (required for -P option) |
| 21 | # Use pkg-config if available, otherwise fall back to defaults |
| 22 | PCRE2_CFLAGS := $(shell pkg-config --cflags libpcre2-8 2>/dev/null) |
| 23 | PCRE2_LIBS := $(shell pkg-config --libs libpcre2-8 2>/dev/null || echo "-lpcre2-8") |
| 24 | LDFLAGS = $(PCRE2_LIBS) |
| 25 | |
| 26 | # Directories |
| 27 | SRC_DIR = src |
| 28 | REGEX_DIR = src/regex |
| 29 | BUILD_DIR = build |
| 30 | BIN_DIR = . |
| 31 | |
| 32 | # Target binary |
| 33 | TARGET = $(BIN_DIR)/ferp |
| 34 | |
| 35 | # Regex source files (in dependency order) |
| 36 | REGEX_SRCS = $(REGEX_DIR)/regex_charclass.f90 \ |
| 37 | $(REGEX_DIR)/regex_types.f90 \ |
| 38 | $(REGEX_DIR)/regex_lexer.f90 \ |
| 39 | $(REGEX_DIR)/regex_parser.f90 \ |
| 40 | $(REGEX_DIR)/regex_nfa.f90 \ |
| 41 | $(REGEX_DIR)/regex_engine.f90 \ |
| 42 | $(REGEX_DIR)/aho_corasick.f90 \ |
| 43 | $(REGEX_DIR)/regex_optimizer.f90 \ |
| 44 | $(REGEX_DIR)/regex_api.f90 \ |
| 45 | $(REGEX_DIR)/pcre_api.f90 |
| 46 | |
| 47 | # C source files (SIMD support) |
| 48 | C_SRCS = $(SRC_DIR)/simd_scan.c |
| 49 | |
| 50 | # Main source files (in dependency order) |
| 51 | MAIN_SRCS = $(SRC_DIR)/ferp_kinds.f90 \ |
| 52 | $(SRC_DIR)/ferp_options.f90 \ |
| 53 | $(SRC_DIR)/ferp_mmap.f90 \ |
| 54 | $(SRC_DIR)/ferp_simd.f90 \ |
| 55 | $(SRC_DIR)/ferp_io.f90 \ |
| 56 | $(SRC_DIR)/ferp_output.f90 \ |
| 57 | $(SRC_DIR)/ferp_dir.f90 \ |
| 58 | $(SRC_DIR)/ferp_search.f90 \ |
| 59 | $(SRC_DIR)/ferp_cli.f90 \ |
| 60 | $(SRC_DIR)/ferp_matcher.f90 \ |
| 61 | $(SRC_DIR)/main.f90 |
| 62 | |
| 63 | # All source files |
| 64 | SRCS = $(REGEX_SRCS) $(MAIN_SRCS) |
| 65 | |
| 66 | # Object files |
| 67 | REGEX_OBJS = $(patsubst $(REGEX_DIR)/%.f90,$(BUILD_DIR)/%.o,$(REGEX_SRCS)) |
| 68 | MAIN_OBJS = $(patsubst $(SRC_DIR)/%.f90,$(BUILD_DIR)/%.o,$(MAIN_SRCS)) |
| 69 | C_OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(C_SRCS)) |
| 70 | OBJS = $(REGEX_OBJS) $(MAIN_OBJS) $(C_OBJS) |
| 71 | |
| 72 | # Default target |
| 73 | all: $(TARGET) |
| 74 | |
| 75 | # Debug build |
| 76 | debug: FFLAGS = $(FFLAGS_DEBUG) |
| 77 | debug: CFLAGS = $(CFLAGS_DEBUG) |
| 78 | debug: clean $(TARGET) |
| 79 | |
| 80 | # Release build |
| 81 | release: FFLAGS = $(FFLAGS_RELEASE) |
| 82 | release: CFLAGS = $(CFLAGS_RELEASE) |
| 83 | release: clean $(TARGET) |
| 84 | |
| 85 | # Create build directory |
| 86 | $(BUILD_DIR): |
| 87 | mkdir -p $(BUILD_DIR) |
| 88 | |
| 89 | # Link target |
| 90 | $(TARGET): $(BUILD_DIR) $(OBJS) |
| 91 | $(FC) $(FFLAGS) -o $@ $(OBJS) $(LDFLAGS) |
| 92 | |
| 93 | # Compile regex source files |
| 94 | $(BUILD_DIR)/%.o: $(REGEX_DIR)/%.f90 | $(BUILD_DIR) |
| 95 | $(FC) $(FFLAGS) -J$(BUILD_DIR) -I$(BUILD_DIR) -c $< -o $@ |
| 96 | |
| 97 | # Compile main source files |
| 98 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.f90 | $(BUILD_DIR) |
| 99 | $(FC) $(FFLAGS) -J$(BUILD_DIR) -I$(BUILD_DIR) -c $< -o $@ |
| 100 | |
| 101 | # Compile C source files |
| 102 | $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) |
| 103 | $(CC) $(CFLAGS) -c $< -o $@ |
| 104 | |
| 105 | # Regex module dependencies (note: some depend on ferp_kinds for pattern_len function) |
| 106 | $(BUILD_DIR)/regex_charclass.o: |
| 107 | $(BUILD_DIR)/regex_types.o: $(BUILD_DIR)/regex_charclass.o |
| 108 | $(BUILD_DIR)/regex_lexer.o: $(BUILD_DIR)/regex_types.o $(BUILD_DIR)/ferp_kinds.o |
| 109 | $(BUILD_DIR)/regex_parser.o: $(BUILD_DIR)/regex_types.o |
| 110 | $(BUILD_DIR)/regex_nfa.o: $(BUILD_DIR)/regex_types.o $(BUILD_DIR)/regex_charclass.o $(BUILD_DIR)/regex_parser.o |
| 111 | $(BUILD_DIR)/regex_engine.o: $(BUILD_DIR)/regex_types.o |
| 112 | $(BUILD_DIR)/aho_corasick.o: $(BUILD_DIR)/ferp_kinds.o |
| 113 | $(BUILD_DIR)/regex_optimizer.o: $(BUILD_DIR)/regex_types.o $(BUILD_DIR)/regex_charclass.o $(BUILD_DIR)/aho_corasick.o $(BUILD_DIR)/ferp_kinds.o |
| 114 | $(BUILD_DIR)/regex_api.o: $(BUILD_DIR)/regex_types.o $(BUILD_DIR)/regex_lexer.o $(BUILD_DIR)/regex_parser.o $(BUILD_DIR)/regex_nfa.o $(BUILD_DIR)/regex_engine.o $(BUILD_DIR)/regex_optimizer.o $(BUILD_DIR)/ferp_kinds.o |
| 115 | $(BUILD_DIR)/pcre_api.o: $(BUILD_DIR)/ferp_kinds.o |
| 116 | |
| 117 | # Main module dependencies |
| 118 | $(BUILD_DIR)/ferp_options.o: $(BUILD_DIR)/ferp_kinds.o |
| 119 | $(BUILD_DIR)/ferp_simd.o: $(BUILD_DIR)/simd_scan.o |
| 120 | $(BUILD_DIR)/ferp_mmap.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_simd.o |
| 121 | $(BUILD_DIR)/ferp_io.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_mmap.o |
| 122 | $(BUILD_DIR)/ferp_output.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_options.o |
| 123 | $(BUILD_DIR)/ferp_dir.o: $(BUILD_DIR)/ferp_kinds.o |
| 124 | $(BUILD_DIR)/ferp_search.o: $(BUILD_DIR)/ferp_kinds.o |
| 125 | $(BUILD_DIR)/ferp_cli.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_options.o |
| 126 | $(BUILD_DIR)/ferp_matcher.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_options.o $(BUILD_DIR)/ferp_io.o $(BUILD_DIR)/ferp_output.o $(BUILD_DIR)/ferp_search.o $(BUILD_DIR)/regex_api.o $(BUILD_DIR)/pcre_api.o |
| 127 | $(BUILD_DIR)/main.o: $(BUILD_DIR)/ferp_kinds.o $(BUILD_DIR)/ferp_options.o $(BUILD_DIR)/ferp_cli.o $(BUILD_DIR)/ferp_io.o $(BUILD_DIR)/ferp_dir.o $(BUILD_DIR)/ferp_matcher.o |
| 128 | |
| 129 | # Clean build artifacts |
| 130 | clean: |
| 131 | rm -rf $(BUILD_DIR) |
| 132 | rm -f $(TARGET) |
| 133 | |
| 134 | # Install (optional) |
| 135 | install: release |
| 136 | cp $(TARGET) /usr/local/bin/ |
| 137 | ln -sf /usr/local/bin/ferp /usr/local/bin/frep |
| 138 | |
| 139 | # Uninstall |
| 140 | uninstall: |
| 141 | rm -f /usr/local/bin/ferp |
| 142 | rm -f /usr/local/bin/frep |
| 143 | |
| 144 | # Run tests |
| 145 | test: $(TARGET) |
| 146 | @echo "=== Basic matching tests ===" |
| 147 | @echo "hello world" | ./ferp "hello" && echo "PASS: literal match" |
| 148 | @echo "hello world" | ./ferp "goodbye" || echo "PASS: no match (exit 1)" |
| 149 | @echo "hello world" | ./ferp -i "HELLO" && echo "PASS: case insensitive" |
| 150 | @echo "hello world" | ./ferp -v "goodbye" && echo "PASS: invert match" |
| 151 | @echo "=== BRE metacharacter tests ===" |
| 152 | @echo "hello world" | ./ferp "hel.o" && echo "PASS: dot metachar" |
| 153 | @echo "helllo" | ./ferp "hel*o" && echo "PASS: star quantifier" |
| 154 | @echo "heo" | ./ferp "hel*o" && echo "PASS: star zero matches" |
| 155 | @echo "hello" | ./ferp "^hello" && echo "PASS: start anchor" |
| 156 | @echo "hello" | ./ferp "hello$$" && echo "PASS: end anchor" |
| 157 | @echo "hello" | ./ferp "^hello$$" && echo "PASS: both anchors" |
| 158 | @echo "=== Character class tests ===" |
| 159 | @echo "abc" | ./ferp "[abc]" && echo "PASS: char class" |
| 160 | @echo "abc" | ./ferp "[a-z]" && echo "PASS: char range" |
| 161 | @echo "ABC" | ./ferp "[A-Z]" && echo "PASS: uppercase range" |
| 162 | @echo "123" | ./ferp "[0-9]" && echo "PASS: digit range" |
| 163 | @echo "!" | ./ferp "[^a-z]" && echo "PASS: negated class" |
| 164 | @echo "]test" | ./ferp "[]a-z]" && echo "PASS: ] at class start" |
| 165 | @echo "-test" | ./ferp "[-az]" && echo "PASS: - at class start" |
| 166 | @echo "test-" | ./ferp "[az-]" && echo "PASS: - at class end" |
| 167 | @echo "=== POSIX class tests ===" |
| 168 | @echo "abc" | ./ferp "[[:alpha:]]" && echo "PASS: [:alpha:]" |
| 169 | @echo "123" | ./ferp "[[:digit:]]" && echo "PASS: [:digit:]" |
| 170 | @echo "abc123" | ./ferp "[[:alnum:]]" && echo "PASS: [:alnum:]" |
| 171 | @echo " " | ./ferp "[[:space:]]" && echo "PASS: [:space:]" |
| 172 | @echo "=== BRE grouping and quantifier tests ===" |
| 173 | @echo "abab" | ./ferp '\(ab\)*' && echo "PASS: BRE group with star" |
| 174 | @echo "aaa" | ./ferp 'a\{2,3\}' && echo "PASS: BRE bounded {2,3}" |
| 175 | @echo "aa" | ./ferp 'a\{2\}' && echo "PASS: BRE exact {2}" |
| 176 | @echo "aaaa" | ./ferp 'a\{2,\}' && echo "PASS: BRE minimum {2,}" |
| 177 | @echo "=== Word boundary tests ===" |
| 178 | @echo "hello world" | ./ferp '\<hello' && echo "PASS: word start \\<" |
| 179 | @echo "hello world" | ./ferp 'world\>' && echo "PASS: word end \\>" |
| 180 | @echo "hello world" | ./ferp '\<world\>' && echo "PASS: word boundaries" |
| 181 | @echo "=== ERE tests ===" |
| 182 | @echo "hello" | ./ferp -E "hel+" && echo "PASS: ERE plus" |
| 183 | @echo "helo" | ./ferp -E "hel?o" && echo "PASS: ERE question" |
| 184 | @echo "heo" | ./ferp -E "hel?o" && echo "PASS: ERE question zero" |
| 185 | @echo "cat" | ./ferp -E "cat|dog" && echo "PASS: ERE alternation" |
| 186 | @echo "dog" | ./ferp -E "cat|dog" && echo "PASS: ERE alternation 2" |
| 187 | @echo "ab" | ./ferp -E "(ab)+" && echo "PASS: ERE group with plus" |
| 188 | @echo "abab" | ./ferp -E "(ab){2}" && echo "PASS: ERE bounded {2}" |
| 189 | @echo "aaa" | ./ferp -E "a{2,3}" && echo "PASS: ERE bounded {2,3}" |
| 190 | @echo "=== Output option tests ===" |
| 191 | @echo "hello world" | ./ferp -o "wor" | grep -q "^wor$$" && echo "PASS: -o only matching" |
| 192 | @echo "hello world hello" | ./ferp -o "hello" | wc -l | grep -q "2" && echo "PASS: -o multiple matches" |
| 193 | @printf "hello\nworld\n" | ./ferp -b "world" | grep -q "^6:" && echo "PASS: -b byte offset" |
| 194 | @echo "=== Context line tests ===" |
| 195 | @printf "a\nb\nmatch\nd\ne\n" | ./ferp -A 2 "match" | wc -l | grep -q "3" && echo "PASS: -A after context" |
| 196 | @printf "a\nb\nmatch\nd\ne\n" | ./ferp -B 2 "match" | wc -l | grep -q "3" && echo "PASS: -B before context" |
| 197 | @printf "a\nb\nmatch\nd\ne\n" | ./ferp -C 1 "match" | wc -l | grep -q "3" && echo "PASS: -C both context" |
| 198 | @printf "a\nb\nmatch\nd\ne\n" | ./ferp -2 "match" | wc -l | grep -q "5" && echo "PASS: -NUM context shorthand" |
| 199 | @echo "=== Edge case tests ===" |
| 200 | @echo "" | ./ferp "" && echo "PASS: empty pattern on empty" |
| 201 | @echo "hello" | ./ferp "" && echo "PASS: empty pattern matches" |
| 202 | @echo "a+b" | ./ferp "a+b" && echo "PASS: BRE + is literal" |
| 203 | @echo "a|b" | ./ferp "a|b" && echo "PASS: BRE | is literal" |
| 204 | @echo "=== Recursive search tests ===" |
| 205 | @./ferp -r "module" src/ | grep -q "ferp_kinds" && echo "PASS: -r recursive search" |
| 206 | @./ferp -r "module" src/ | grep -q "regex_types" && echo "PASS: -r searches subdirs" |
| 207 | @./ferp -r --include="*.f90" "module" src/ | grep -q "ferp_kinds" && echo "PASS: --include filter" |
| 208 | @./ferp -r --exclude="*output*" "module" src/ | grep -qv "ferp_output" && echo "PASS: --exclude filter" |
| 209 | @./ferp -r --exclude-dir="regex" "module" src/ | grep -qv "regex_types" && echo "PASS: --exclude-dir filter" |
| 210 | @echo "=== Binary file tests ===" |
| 211 | @printf 'hello\x00world\n' > /tmp/ferp_binary_test.txt |
| 212 | @./ferp "hello" /tmp/ferp_binary_test.txt | grep -q "Binary file" && echo "PASS: binary file detection" |
| 213 | @./ferp -a "hello" /tmp/ferp_binary_test.txt | grep -q "hello" && echo "PASS: -a treats binary as text" |
| 214 | @./ferp -I "hello" /tmp/ferp_binary_test.txt; test $$? -eq 1 && echo "PASS: -I skips binary" |
| 215 | @rm -f /tmp/ferp_binary_test.txt |
| 216 | @echo "=== Color mode tests ===" |
| 217 | @./ferp --color=always "module" src/ferp_kinds.f90 | grep -q '\[01;31m' && echo "PASS: --color=always outputs ANSI" |
| 218 | @./ferp --color=never "module" src/ferp_kinds.f90 | grep -qv '\[01;31m' && echo "PASS: --color=never no ANSI" |
| 219 | @./ferp --color=auto "module" src/ferp_kinds.f90 | grep -qv '\[01;31m' && echo "PASS: --color=auto no ANSI when piped" |
| 220 | @echo "=== PCRE tests ===" |
| 221 | @echo "hello world" | ./ferp -P "hello" | grep -q "hello" && echo "PASS: -P basic match" |
| 222 | @echo "foobar" | ./ferp -P 'foo(?=bar)' | grep -q "foobar" && echo "PASS: -P positive lookahead" |
| 223 | @echo "foobaz" | ./ferp -P 'foo(?!bar)' | grep -q "foobaz" && echo "PASS: -P negative lookahead" |
| 224 | @echo "foobar" | ./ferp -P '(?<=foo)bar' | grep -q "foobar" && echo "PASS: -P positive lookbehind" |
| 225 | @echo "bazbar" | ./ferp -P '(?<!foo)bar' | grep -q "bazbar" && echo "PASS: -P negative lookbehind" |
| 226 | @echo "abcabc" | ./ferp -P '(?:abc)+' | grep -q "abcabc" && echo "PASS: -P non-capturing group" |
| 227 | @echo "test123" | ./ferp -P '\d+' | grep -q "test123" && echo "PASS: -P digit class" |
| 228 | @echo "test123more456" | ./ferp -P -o '\d+' | head -1 | grep -q "123" && echo "PASS: -P -o only matching" |
| 229 | @echo "HELLO" | ./ferp -P -i 'hello' | grep -q "HELLO" && echo "PASS: -P -i case insensitive" |
| 230 | @echo "Hello123" | ./ferp -P -o '\p{L}+' | grep -q "Hello" && echo "PASS: -P Unicode \\p{L} letter class" |
| 231 | @echo "Hello123" | ./ferp -P -o '\p{N}+' | grep -q "123" && echo "PASS: -P Unicode \\p{N} number class" |
| 232 | @echo "café" | ./ferp -P '\p{L}+' | grep -q "café" && echo "PASS: -P Unicode accented chars" |
| 233 | @echo "CAFÉ" | ./ferp -P -i 'café' | grep -q "CAFÉ" && echo "PASS: -P Unicode case folding" |
| 234 | @echo "=== All tests complete! ===" |
| 235 | |
| 236 | # Help |
| 237 | help: |
| 238 | @echo "FERP Makefile targets:" |
| 239 | @echo " all - Build ferp (debug mode)" |
| 240 | @echo " debug - Build with debug flags" |
| 241 | @echo " release - Build with optimization" |
| 242 | @echo " test - Run basic tests" |
| 243 | @echo " clean - Remove build artifacts" |
| 244 | @echo " install - Install to /usr/local/bin" |
| 245 | @echo " uninstall- Remove from /usr/local/bin" |
| 246 | @echo " help - Show this message" |
| 247 | |
| 248 | .PHONY: all debug release clean install uninstall test help |