Mert Özen Her Satırda Daha İleri
Geliştirici Araçları Regular Expressions — #04

Lookaround, Inline Flag'ler ve Unicode: Lookahead, Lookbehind, (?i), \p{L} ve Türkçe Karakterler

Mert Özen 15 Kas 2026 15 dk 4 görüntülenme
Lookaround, Inline Flag'ler ve Unicode: Lookahead, Lookbehind, (?i), \p{L} ve Türkçe Karakterler

Lookahead ve lookbehind karakter tüketmeden bir pozisyonun önünü ya da arkasını kontrol eder; değişken uzunluklu lookbehind .NET ve JavaScript'te çalışır, PCRE2'de sınırlı, ripgrep'te hiç yoktur. Inline flag'leri ve JavaScript'in (?i:…) modifier'larını, \p{L} ile v flag'ini, .NET'te IgnoreCase'in kültüre bağlı olduğu için ıssız ile ISSIZ'in eşleşip eşleşmediğinin ortama göre değiştiğini gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), Node.js 26.13 (LTS), grep 3.12 (GNU), PCRE2 (Perl uyumlu regex library'si) 10.49, ripgrep 15.2.0 ve Python 3.14 ile denenmiştir. grep ve rg komutları Linux'ta, WSL'de (Windows içindeki Linux ortamı) ya da Git Bash'te çalışır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) kataloğunda bazı yayıncılar kitap adlarını büyük harfle gönderiyor: ÇALIKUŞU, İNCE MEMED, Robinson Crusoe'nun Türkçe baskısı için ROBINSON CRUSOE: ISSIZ BİR ADADA 28 YIL. Müşteri arama kutusuna küçük harfle çalıkuşu, ince memed ya da ıssız yazıyor. API arama terimini Regex.Escape ile kaçışlayıp RegexOptions.IgnoreCase ile arıyor.

Kod geliştiricinin Türkçe Windows'unda üç kitabı da buluyor. Aynı kod production'da bir Linux container'ında çalışıyor. Orada kaç sonuç döner? Angular tarafında aynı arama /ıssız/iu gibi bir pattern'le yapılsaydı tarayıcı ne bulurdu?

Lookahead ve Lookbehind: Karakter Tüketmeyen Kontroller

Lookaround, bulunduğu pozisyondan ileriye ya da geriye bakıp bir koşulu kontrol eder, ama hiçbir karakteri eşleşmeye katmaz. Dört biçimi var: (?=...) ileride bu var, (?!...) ileride bu yok, (?<=...) geride bu var, (?<!...) geride bu yok. Birinci yazıdaki anchor'lar gibi bir pozisyonla eşleşirler; farkları, koşulu senin yazman.

using System.Text.RegularExpressions;

// lookahead: arkasından sayının sonuna kadar üçlü rakam grupları gelen pozisyon
Console.WriteLine(Regex.Replace("Total: 1234567 TRY, 950 items", @"(?<=\d)(?=(?:\d{3})+\b)", "."));

// lookbehind: "price:"dan sonraki tutar, "price:" eşleşmeye girmeden
Console.WriteLine(Regex.Match("price: 450 TRY", @"(?<=price:\s*)\d+").Value);

// negative lookahead: debug satırı olmayan Payments satırları
string log = File.ReadAllText("data/app.log");
foreach (Match m in Regex.Matches(log, @"^(?!.* DBG ).*Bookstore\.Payments.*$", RegexOptions.Multiline))
    Console.WriteLine(m.Value.TrimEnd('\r'));

// lookahead'lerle kurulmuş bir kompozisyon kuralı
Regex rule = new(@"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^\w\s]).{8,}$");
foreach (string candidate in new[] { "Password1!", "Bookstore2026!", "correct horse battery staple" })
    Console.WriteLine($"{candidate,-30} {rule.IsMatch(candidate)}");
$ dotnet run dotnet/lookaround.cs
Total: 1.234.567 TRY, 950 items
450
2026-11-15T10:42:07Z INF Bookstore.Payments Payment 4711 authorized for BKS-2026-000123
2026-11-15T10:42:09Z WRN Bookstore.Payments Payment 4712 retried for BKS-2026-000124
2026-11-15T10:42:12Z ERR Bookstore.Payments Payment 4713 declined for BKS-2026-000126
Password1!                     True
Bookstore2026!                 True
correct horse battery staple   False
$ node js/lookaround.mjs
Total: 1.234.567 TRY, 950 items
450
2026-11-15T10:42:07Z INF Bookstore.Payments Payment 4711 authorized for BKS-2026-000123
2026-11-15T10:42:09Z WRN Bookstore.Payments Payment 4712 retried for BKS-2026-000124
2026-11-15T10:42:12Z ERR Bookstore.Payments Payment 4713 declined for BKS-2026-000126

İlk satırdaki pattern'i okuyalım. (?<=\d) "solumda bir rakam var", (?=(?:\d{3})+\b) "sağımda üçün katı kadar rakam var ve sonra sayı bitiyor". İkisinin aynı anda doğru olduğu pozisyonlar binlik ayırıcının gireceği yerler. Pattern hiçbir karakter tüketmediği için replace yalnızca bu pozisyonlara nokta ekliyor; 950'de solunda rakam olup sağında üçün katı kadar rakam kalan bir pozisyon olmadığı için dokunulmadı.

İkinci satır lookbehind'ın en yaygın kullanımı: bir etiketin arkasındaki değeri, etiketi eşleşmeye katmadan almak. Aynı şeyi bir grup ile price:\s*(\d+) yazıp Groups[1] okuyarak da yapabilirsin; lookbehind'ın avantajı, eşleşmenin kendisinin doğrudan aradığın değer olması. Bu da özellikle replace'te ve grep -o'da işe yarıyor.

"X İçeren Ama Y İçermeyen Satır"

Negative lookahead'in en pratik kullanımı log aramak. ^(?!.* DBG ).*Bookstore\.Payments.*$ şunu söylüyor: satır başında dur, bu satırın herhangi bir yerinde DBG olmadığını kontrol et, sonra satırın Bookstore.Payments içerdiğini doğrula. Lookahead satır başında, hiçbir karakter tüketmeden çalıştığı için satırın tamamına bakabiliyor. Komut satırında bunun iki yolu var:

$ cat shell/lookaround.sh
#!/usr/bin/env bash
# PCRE2: grep -P'de lookaround çalışır
grep -P '^(?!.* DBG ).*Bookstore\.Payments' data/app.log
echo
# POSIX ERE'de lookaround yok: pipe'taki iki grep aynı işi yapar
grep -E 'Bookstore\.Payments' data/app.log | grep -v ' DBG '
echo
# \K o ana kadar eşleşeni çıktıdan atar
grep -oP 'Payment \K[0-9]+(?= declined)' data/app.log
$ bash shell/lookaround.sh
2026-11-15T10:42:07Z INF Bookstore.Payments Payment 4711 authorized for BKS-2026-000123
2026-11-15T10:42:09Z WRN Bookstore.Payments Payment 4712 retried for BKS-2026-000124
2026-11-15T10:42:12Z ERR Bookstore.Payments Payment 4713 declined for BKS-2026-000126

2026-11-15T10:42:07Z INF Bookstore.Payments Payment 4711 authorized for BKS-2026-000123
2026-11-15T10:42:09Z WRN Bookstore.Payments Payment 4712 retried for BKS-2026-000124
2026-11-15T10:42:12Z ERR Bookstore.Payments Payment 4713 declined for BKS-2026-000126

4713

Komut satırında ikinci yol çoğu zaman daha okunur: her grep tek bir şey söylüyor ve POSIX (Unix araçlarının uyduğu standart) tanımlı her ortamda çalışıyor. Lookahead'li tek pattern ise .NET'te ya da JavaScript'te tek bir Regex nesnesiyle iş görmen gerektiğinde değerli: bir log viewer'ın filtre kutusu, bir alert kuralı gibi. Son satırdaki \K PCRE2'ye özgü: o ana kadar eşleşen kısmı sonuçtan atıyor, değişken uzunluklu lookbehind'ın yapamadığı yerlerde de çalışıyor.

Parola Kuralı Regex'i Neden Kötü Bir Politika?

Lookahead'lerle kurulan kompozisyon kuralı, regex öğreticilerinin sevdiği bir örnek: en az bir büyük harf, bir küçük harf, bir rakam, bir sembol ve 8 karakter. Yukarıdaki çıktı bu kuralın neyi ödüllendirdiğini gösteriyor: Password1! ve Bookstore2026! geçiyor, dört kelimelik uzun bir parola geçmiyor. İlk ikisi tahmin listelerinin en başındaki türden parolalar; kural, kullanıcıları tam da bu kalıba itiyor.

NIST'in (ABD'nin ulusal standartlar enstitüsü) 2025'te yayımlanan 800-63B-4 dijital kimlik rehberi, karakter türü karışımı isteyen kompozisyon kurallarını açıkça yasaklıyor ve parola tek başına kullanılıyorsa en az 15 karakter istiyor. Doğru kontrol uzunluk ve sızmış parola listesine karşı kontroldür; ayrıntısı Web Güvenliği (OWASP Top 10) serisinin "OWASP A07: Authentication Failures (1)" yazısında. Lookahead'in burada öğrettiği şey regex syntax'ı, politika değil.

Lookbehind Her Flavor'da Aynı Değil

Lookahead bütün backtracking engine'lerinde aynı. Lookbehind ise "geriye ne kadar bakabilirim?" sorusuna farklı cevaplar veriyor. .NET ve JavaScript her uzunlukta lookbehind'ı kabul ediyor; yukarıdaki (?<=price:\s*) iki tarafta da çalıştı. Diğerleri:

$ cat shell/lookbehind.sh
#!/usr/bin/env bash
# sınırsız lookbehind: PCRE2 reddeder
grep -oP '(?<=price:\s*)[0-9]+' data/prices.txt
# sınırlı, değişken uzunluklu lookbehind: PCRE2 10.43+
grep -oP '(?<=price:\s{0,3})[0-9]+' data/prices.txt
# \K: o ana kadar eşleşeni unut
grep -oP 'price:\s*\K[0-9]+' data/prices.txt
# ripgrep'in varsayılan engine'inde lookaround yok, -P PCRE2'ye geçer
rg -o '(?<=price: )[0-9]+' data/prices.txt
rg -oP '(?<=price:\s{0,3})[0-9]+' data/prices.txt
$ bash shell/lookbehind.sh
grep: length of lookbehind assertion is not limited
450
450
450
450
rg: regex parse error:
    (?:(?<=price: )[0-9]+)
       ^^^^
error: look-around, including look-ahead and look-behind, is not supported

Consider enabling PCRE2 with the --pcre2 flag, which can handle backreferences
and look-around.
450
450
$ python3 python/lookbehind.py
(?<=price: )\d+ ['450']
(?<=price:\s*)\d+ re.error: look-behind requires fixed-width pattern
Lookbehind.NETJavaScriptPCRE2 10.49 (grep -P)Python 3.14ripgrep (varsayılan)grep -E
Sabit uzunluk (?<=price: )varvarvarvaryokyok
Sınırlı (?<=price:\s{0,3})varvarvaryokyokyok
Sınırsız (?<=price:\s*)varvaryokyokyokyok

PCRE2 değişken uzunluklu lookbehind'ı 10.43 sürümünde ekledi; varsayılan üst sınır 255 karakter, ama sınırın pattern'den anlaşılması gerekiyor. \s* sınırsız olduğu için reddedildi. ripgrep'in varsayılan engine'i ise lookaround'u hiç desteklemiyor; sebebi backreference'ta gördüğümüzle aynı, beşinci yazıda RE2 (Google'ın backtracking yapmayan regex library'si) ailesiyle anlatacağız. .NET'te yazdığın bir pattern'i grep -P ile log'da denemek istediğinde ilk takılacağın yer genellikle burası; \K çoğu durumda iyi bir karşılık.

Inline Flag'ler: (?i), (?m), (?s), (?x), (?n)

Option'ları pattern'in dışından vermek yerine pattern'in içine yazabilirsin. Bu, pattern'in bir config dosyasında ya da veritabanında durduğu ve kodun option veremediği durumlarda işe yarar. .NET'te beş harf var: i büyük/küçük harf duyarsız, m multiline, s noktanın satır sonunu da yakalaması, x pattern içindeki boşlukları yok sayıp # yorumlarına izin vermek, n yalnızca isimli grupları yakalamak. (?i) bulunduğu yerden grubun sonuna kadar, (?i:...) yalnızca parantezin içinde geçerli, (?-i) kapatır:

using System.Text.RegularExpressions;

string level = "2026-11-15T10:42:12Z err Bookstore.Payments Payment 4713 declined";

// (?i) bütün pattern için, (?i:...) tek bir parça için, (?-i) yeniden kapatmak için
Console.WriteLine(Regex.IsMatch(level, @"(?i)ERR bookstore\.payments"));
Console.WriteLine(Regex.IsMatch(level, @"(?i:ERR) bookstore\.payments"));
Console.WriteLine(Regex.IsMatch(level, @"(?i)ERR (?-i)Bookstore\.Payments"));

// (?x) pattern'deki boşlukları yok sayar ve # yorumlarına izin verir; (?n) yalnızca isimli grupları yakalar
Regex order = new(@"(?xn)
    ^BKS -
    (?<year> [0-9]{4} ) -    # year
    ([0-9]{6}) $             # sequence, not captured because of n
");
Match m = order.Match("BKS-2026-000123");
Console.WriteLine($"{m.Success} groups={m.Groups.Count} year={m.Groups["year"].Value}");

// .NET'te \w ve \p{L} Türkçe harfleri de kapsar
Console.WriteLine($"{Regex.IsMatch("Çalıkuşu", @"^\w+$")} {Regex.IsMatch("Çalıkuşu", @"^\p{L}+$")} {Regex.IsMatch("Çalıkuşu", @"^\p{Lu}\p{Ll}+$")}");
$ dotnet run dotnet/inline-flags.cs
True
False
True
True groups=2 year=2026
True True True

İkinci satır False, çünkü (?i:...) yalnızca ERR'yi kapsıyor; bookstore büyük/küçük harf duyarlı kaldı ve log'daki Bookstore ile tutmadı. (?x)'li pattern'deki # year gibi satırlar pattern'in kendi yorumları; engine onları yok sayıyor. (?n) sayesinde sıra numarası grubu yakalanmadı, Groups.Count 2 (grup 0 ve year). Büyük pattern'leri okunur tutmanın yollarını sekizinci yazıda ayrıca ele alacağız.

JavaScript'te durum farklı. Flag'ler pattern'in sonuna yazılıyor ve bütün pattern için geçerli. Pattern'in bir kısmına flag vermek, Eylül 2025'te bütün güncel tarayıcılara gelen modifiers özelliğiyle mümkün oldu; ama yalnızca i, m, s için ve yalnızca parantezli (?i:...) biçiminde:

$ node js/modifiers.mjs
/(?i:ERR) Bookstore\.Payments/       true
/ERR (?-i:Bookstore)\.payments/i     true
/ERR (?-i:bookstore)\.payments/i     false
/(?i)ERR Bookstore\.Payments/        SyntaxError: Invalid regular expression: /(?i)ERR Bookstore\.Payments/: Invalid group
/(?x:ERR)/                           SyntaxError: Invalid regular expression: /(?x:ERR)/: Invalid group
/(?i-i:ERR)/                         SyntaxError: Invalid regular expression: /(?i-i:ERR)/: Repeated flag in flag group

.NET'te yazılmış (?i)... ile başlayan bir pattern JavaScript'e taşınınca syntax hatası veriyor. JavaScript'te x flag'i hiç yok; n'nin karşılığı da yok. Taşınabilir bir pattern istiyorsan flag'leri pattern'in dışında, her platformun kendi yolundan ver.

İş.NET RegexOptions.NET inlineJavaScript flagJavaScript modifier
Büyük/küçük harf duyarsızIgnoreCase(?i)i(?i:...)
^/$ her satırdaMultiline(?m)m(?m:...)
Nokta satır sonunu da yakalarSingleline(?s)s(?s:...)
Boşluk ve # yorumuIgnorePatternWhitespace(?x)yokyok
Yalnız isimli gruplarExplicitCapture(?n)yokyok

Unicode: \p{L}, Script'ler ve JavaScript'in v Flag'i

Birinci yazıda \w ve \d'nin flavor'a göre farklı alfabeler kullandığını gördük. Hangi alfabeyi kastettiğini açıkça söylemenin yolu Unicode property'leri: \p{L} herhangi bir harf, \p{Lu} büyük harf, \p{Ll} küçük harf, \p{Nd} ondalık rakam. Kategori isimleri her yerde aynı. Alfabe (script) söz konusu olunca yazım değişiyor. Bookstore kataloğunda Rusça baskılar da var:

$ cat data/titles.txt
Dune
Война и мир
Çalıkuşu
Anna Karenina
Анна Каренина
$ dotnet run dotnet/unicode-properties.cs
\p{IsCyrillic}       Война и мир | Анна Каренина
^\p{Lu}\p{Ll}+$      Dune | Çalıkuşu
\p{Script=Cyrillic}  Invalid pattern '\p{Script=Cyrillic}' at offset 10. Incomplete \p{X} character escape.
\p{Cyrillic}         Invalid pattern '\p{Cyrillic}' at offset 12. Unknown property 'Cyrillic'.
$ node js/unicode-properties.mjs
\p{Script=Cyrillic}  Война и мир | Анна Каренина
\p{sc=Cyrl}          Война и мир | Анна Каренина
^\p{Lu}\p{Ll}+$      Dune | Çalıkuşu
\p{IsCyrillic}       SyntaxError: Invalid regular expression: /\p{IsCyrillic}/u: Invalid property name
$ cat shell/unicode.sh
#!/usr/bin/env bash
# Unicode script property'si: PCRE2 ve ripgrep
grep -P '\p{Cyrillic}' data/titles.txt
rg '\p{Script=Cyrillic}' data/titles.txt
# POSIX ERE'de \p yok: ters bölü bir uyarıyla yok sayılır
grep -E '\p{L}' data/titles.txt
# harf kategorileri: bir büyük harf ve ardından küçük harfler
grep -P '^\p{Lu}\p{Ll}+$' data/titles.txt
rg '^\p{Lu}\p{Ll}+$' data/titles.txt
# Türkçe harflerle \w
printf 'Çalıkuşu\n' | grep -P '^\w+$'
printf 'Çalıkuşu\n' | rg '^\w+$'
$ bash shell/unicode.sh
Война и мир
Анна Каренина
Война и мир
Анна Каренина
grep: warning: stray \ before p
Dune
Çalıkuşu
Dune
Çalıkuşu
Çalıkuşu
Çalıkuşu

.NET script değil Unicode bloğu adı kullanıyor: \p{IsCyrillic} U+0400 ile U+04FF arasındaki bloğu kapsıyor; Kiril alfabesinin bu bloğun dışında kalan ek karakterleri için \p{IsCyrillicSupplement} gibi başka bloklar var. JavaScript \p{Script=Cyrillic} ya da kısa hâli \p{sc=Cyrl} istiyor ve bunun için u ya da v flag'i şart. PCRE2 \p{Cyrillic}, ripgrep \p{Cyrillic} ve \p{Script=Cyrillic} yazımlarını kabul ediyor; grep -E'de \p yok. Script çıktısının altındaki satırlar grep -P ve ripgrep'te ^\p{Lu}\p{Ll}+$ ve \w'nun Türkçe harflerle davranışı.

JavaScript'te iki ayrıntı daha var. u flag'i olmadan \p{L} bir property değil, düz p{L} metni; hata da vermiyor. ES2024 ile gelen v flag'i ise u'nun üzerine character class içinde küme işlemleri ve "string property'leri" ekliyor:

// JavaScript'te \w ASCII kalır; \p{L} için u ya da v flag'i gerekir
console.log(/^\w+$/u.test("Çalıkuşu"), /^\p{L}+$/u.test("Çalıkuşu"), /^\p{Lu}\p{Ll}+$/u.test("Çalıkuşu"));

// u olmadan \p{L}, "p{L}" harflerinden ibarettir
console.log(/\p{L}/.test("p{L}"), /\p{L}/.test("Dune"));

// v flag'i: class içinde küme farkı ve kesişimi
const nonAsciiLetter = /[\p{L}--[a-zA-Z]]/v;
console.log(nonAsciiLetter.test("Dune"), nonAsciiLetter.test("Çalıkuşu"));
console.log(/^[\p{Script=Cyrillic}&&\p{Lu}]/v.test("Война и мир"));

// string property'leri yalnızca v ile var
console.log(/^\p{RGI_Emoji}$/v.test("👍🏽"), /^\p{Emoji}$/u.test("👍🏽"));
$ node js/unicode.mjs
false true true
true false
false true
true
true false

[\p{L}--[a-zA-Z]] "ASCII (temel İngilizce karakter seti) dışında kalan harf" demek; Türkçe karakter içeren bir başlığı ayırmanın kısa bir yolu. && kesişim: "Kiril ve büyük harf". Son satırdaki ten rengi değiştiricili başparmak emojisi iki code point'ten oluşuyor; \p{Emoji} tek code point'e baktığı için tutmuyor, \p{RGI_Emoji} ise birden fazla code point'ten oluşan dizileri de tanıyor. Code point ayrıntıları Number System'ler ve Data Representation serisinin Unicode yazısında.

Türkçe İ/ı ve Büyük/Küçük Harf Duyarsız Arama

Türkçede dört ayrı i var: I ve ı bir çift, İ ve i başka bir çift. Unicode'un varsayılan kuralları ise I ile i'yi eşleştiriyor. Tahmindeki aramayı kültür vererek çalıştıralım:

using System.Globalization;
using System.Text.RegularExpressions;

string[] catalog = ["ÇALIKUŞU", "İNCE MEMED", "ROBINSON CRUSOE: ISSIZ BİR ADADA 28 YIL"];
string[] terms = ["çalıkuşu", "ince memed", "ıssız"];

// komut satırından kültür adı: tr-TR, en-US ya da "" (invariant)
if (args.Length > 0)
    CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(args[0]);
Console.WriteLine($"CurrentCulture: \"{CultureInfo.CurrentCulture.Name}\"");

foreach (string term in terms)
{
    Regex re = new(Regex.Escape(term), RegexOptions.IgnoreCase);
    Console.WriteLine($"  {term,-11} regex: {catalog.Count(re.IsMatch)}  tr-TR CompareInfo: {catalog.Count(t => ContainsTr(t, term))}");
}

// regex'siz, kültüre duyarlı arama: kültür açıkça veriliyor, ortamdan gelmiyor
static bool ContainsTr(string text, string term) =>
    CultureInfo.GetCultureInfo("tr-TR").CompareInfo.IndexOf(text, term, CompareOptions.IgnoreCase) >= 0;
$ dotnet run dotnet/turkish-search.cs -- tr-TR
CurrentCulture: "tr-TR"
  çalıkuşu    regex: 1  tr-TR CompareInfo: 1
  ince memed  regex: 1  tr-TR CompareInfo: 1
  ıssız       regex: 1  tr-TR CompareInfo: 1
$ dotnet run dotnet/turkish-search.cs -- en-US
CurrentCulture: "en-US"
  çalıkuşu    regex: 0  tr-TR CompareInfo: 1
  ince memed  regex: 1  tr-TR CompareInfo: 1
  ıssız       regex: 0  tr-TR CompareInfo: 1
$ dotnet run dotnet/turkish-search.cs -- ""
CurrentCulture: ""
  çalıkuşu    regex: 0  tr-TR CompareInfo: 1
  ince memed  regex: 0  tr-TR CompareInfo: 1
  ıssız       regex: 0  tr-TR CompareInfo: 1
$ DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 dotnet run dotnet/turkish-search.cs
CurrentCulture: ""
Unhandled exception. System.Globalization.CultureNotFoundException: Only the invariant culture is supported in globalization-invariant mode. See https://aka.ms/GlobalizationInvariantMode for more information. (Parameter 'name')
tr-tr is an invalid culture identifier.

Tahminin .NET cevabı: sonuç, Regex nesnesi oluşturulduğu andaki CurrentCulture'a bağlı. Türkçe kültürde üçü de bulundu. en-US'de yalnızca ince memed bulundu; ı ile I bu kültürde eşleşmiyor. Invariant kültürde hiçbiri bulunmadı. Alpine ve Ubuntu chiseled tabanlı .NET container image'ları ICU'yu (Unicode ve kültür verisini sağlayan library) içermiyor ve bu image'lardaki uygulamalar globalization-invariant modda çalışıyor; kültür bilgisi hiç yok ve tr-TR istemek son satırdaki gibi exception fırlatıyor. Windows'ta aynı denemeyi PowerShell'de $env:DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ile yapabilirsin. RegexOptions.CultureInvariant ise kültürü ortamdan almak yerine her zaman invariant kuralları kullanır; dosya yolu, URL şeması gibi sistem tanımlayıcılarında doğru seçim bu, Türkçe metin aramada değil.

JavaScript'te i flag'i hiçbir kültüre bakmıyor ve Türkçe çiftleri hiç tanımıyor:

$ node js/turkish-search.mjs
çalıkuşu    iu: 0  tr-TR lower: 1
ince memed  iu: 0  tr-TR lower: 1
ıssız       iu: 0  tr-TR lower: 1

Çözüm, iki tarafı da toLocaleLowerCase("tr-TR") ile küçültüp flag'siz aramak. Komut satırında PCRE2 10.45 ile gelen (*TURKISH_CASING) ve GNU grep'in locale kuralları aynı işi görüyor:

$ cat data/catalog-tr.txt
ÇALIKUŞU
İNCE MEMED
ROBINSON CRUSOE: ISSIZ BİR ADADA 28 YIL
INCE
$ cat shell/turkish-casing.sh
#!/usr/bin/env bash
# PCRE2'nin büyük/küçük harf duyarsız eşleşmesi Türkçe değil Unicode kurallarını izler
grep -iP 'ince' data/catalog-tr.txt
# pattern'in başında (*TURKISH_CASING): PCRE2 10.45+
grep -iP '(*TURKISH_CASING)ince' data/catalog-tr.txt
grep -iP '(*TURKISH_CASING)ıssız' data/catalog-tr.txt
# GNU grep -E locale'in büyük/küçük harf kurallarını kullanır
LC_ALL=tr_TR.UTF-8 grep -iE 'ince' data/catalog-tr.txt
LC_ALL=en_US.UTF-8 grep -iE 'ince' data/catalog-tr.txt
$ bash shell/turkish-casing.sh
INCE
İNCE MEMED
ROBINSON CRUSOE: ISSIZ BİR ADADA 28 YIL
İNCE MEMED
INCE

Son satıra karşılaştırma için İngilizce kurallarla yazılmış INCE ekledim. grep -iP 'ince' Unicode kurallarıyla yalnızca onu buldu. (*TURKISH_CASING) ile ince İNCE MEMED'i, ıssız ISSIZ'i buldu. grep -iE ise sonucu locale'den alıyor: tr_TR.UTF-8'de Türkçe, en_US.UTF-8'de İngilizce kurallar.

Üretimde Dikkat

Kültüre duyarlı bir aramayı ortamın kültürüne bırakma. Geliştirici makinesi, CI agent'ı ve production container'ı farklı kültürlerle çalışır; aynı test birinde geçip diğerinde kalır. Türkçe metin arıyorsan kültürü kodda açıkça ver: CompareInfo ile ya da iki tarafı ToLower(CultureInfo.GetCultureInfo("tr-TR")) ile küçültüp IgnoreCase olmadan arayarak. Container image'ının invariant modda olup olmadığını da bil; kültür verisine ihtiyaç duyan uygulamada ICU'lu bir image seçmek ya da ICU'yu image'a eklemek gerekir.

Binlik ayırıcı örneği bir regex alıştırması; sayı biçimlendirmenin doğru yolu ToString("N0", culture) ya da JavaScript'te Intl.NumberFormat. Aynı pattern'i bütün bir metne uygularsan ISBN (kitapların uluslararası standart numarası) gibi uzun rakam dizilerine de nokta ekler.

Angular formunda Unicode'a dayanan bir validator yazıyorsan u ya da v flag'ini unutma ve aynı kuralı .NET tarafında \p{...} ile karşılaştır: \w'ya güvenmek, Türkçe isimleri tarayıcıda reddedip sunucuda kabul etmek demek.

Lookaround'lar ücretsiz değil. ^(?!.* DBG ) her satırda satırın sonuna kadar bakıyor. Uzun satırlarda ve çok sayıda lookahead'li pattern'de bu fark eder; maliyetini ikinci yazıdaki gibi ölç. .NET'in NonBacktracking seçeneğinin hangi yapıları desteklemediğini yedinci yazıda göreceğiz.

Sık Düşülen Tuzaklar

Değişken uzunluklu lookbehind'ı her yerde var sanmak

.NET ve JavaScript'te çalışan (?<=price:\s*) PCRE2'de ve Python'da hata, ripgrep'te lookaround hiç yok. \K ya da bir capture group kullan.

JavaScript'e (?i) taşımak

JavaScript yalnızca (?i:...) biçimini tanıyor ve yalnızca i, m, s için. (?i) ile başlayan pattern Invalid group hatası veriyor.

\w'nun Türkçe karakterleri kapsadığını varsaymak

.NET, PCRE2 ve ripgrep'te kapsıyor, JavaScript'te u flag'iyle bile kapsamıyor. Harf istiyorsan \p{L} yaz.

u flag'i olmadan \p{L}

JavaScript'te hata vermiyor, düz metin arıyor. Unicode property kullanan her pattern'de u ya da v olduğundan emin ol.

IgnoreCase'i kültürden bağımsız sanmak

.NET'te sonuç Regex oluşturulduğu andaki kültüre bağlı. Türkçe metinde kültürü açıkça ver, sistem tanımlayıcılarında CultureInvariant kullan.

Küçük Bir Deneme

★☆☆ data/app.log'da Bookstore.Payments içeren ve BKS-2026-000126 siparişine ait olan satırı, iki lookahead'i olan tek bir pattern'le bul. Lookahead'lerin sırası sonucu değiştiriyor mu?

★★☆ Binlik ayırıcı pattern'ini, Order 9780441172719 total 1234567 metninde yalnızca total kelimesinden sonraki sayıya uygulanacak şekilde değiştir. .NET'te lookbehind ile, grep -P'de \K ile dene.

★★★ Bookstore'un arama endpoint'i için, tr-TR kültürünü açıkça kullanan ve hem .NET'te hem invariant modda aynı sonucu veren bir arama fonksiyonu yaz. Invariant modda CultureInfo.GetCultureInfo("tr-TR") çalışmadığına göre hangi yolu seçerdin? İpucu: bir container'ın neden invariant modda olduğunu ve ICU'yu eklemenin bedelini düşün.

Böylece syntax'ı okumayı tamamladık: engine'in pozisyonları nasıl denediği, quantifier'lar ve backtracking, gruplar ve replace, lookaround ve Unicode. Bir sonraki yazıda araçlara geçiyoruz: .NET, JavaScript, PCRE2, POSIX'in iki regex syntax'ı ve RE2 arasındaki farkları bir tabloda toplayacak, ripgrep'in neden lookaround ve backreference desteklemediğini, glob ile regex'in nerede karıştığını göreceğiz.