Komut Satırında ve IDE'de Regex: grep -E/-P, ripgrep, sed, git grep, PowerShell ve Repo Genelinde Find/Replace
Repo genelinde regex ile değişikliği güvenli yapmanın sırası şu: temiz çalışma ağacı, dosya listesini git grep ile almak, rg -r ile önizlemek, sed -E ile uygulamak ve git diff ile kontrol etmek. grep, ripgrep, PowerShell ve IDE'lerin regex farklarını, Bookstore'da interpolation'lı log çağrılarını message template'e çeviren gerçek bir refactor'u ve CRLF'li dosyada $'ın neden eşleşmediğini .NET 11'in AnyNewLine option'ıyla birlikte gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.404), bir örnekte .NET 11 (SDK 11.0.100), Node.js 26.13 (LTS), grep 3.12 (GNU), sed 4.10 (GNU), ripgrep 15.2.0, git 2.43, PowerShell 7.7 ve PCRE2 (Perl uyumlu regex library'si) 10.49 ile denenmiştir. grep, sed, rg ve git komutları Linux'ta, WSL'de (Windows içindeki Linux ortamı) ya da Git Bash'te çalışır. Visual Studio, Rider ve Visual Studio Code (VS Code) bu yazı için çalıştırılmadı; IDE bölümündeki bilgiler resmî dokümanlarından.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) bir müşteri BKS-2026-000126 numaralı siparişinin ödemesinin neden reddedildiğini soruyor. Ayşe, Windows sunucudaki uygulamanın log dosyasını indirip Linux makinesinde arıyor. Satırın bu numarayla bittiğini dosyayı açınca gözüyle görüyor:
grep -c 'BKS-2026-000126$' app.log
Komut kaç döndürür? Aynı dosyayı .NET'te File.ReadAllText ile okuyup RegexOptions.Multiline ile BKS-\d{4}-\d{6}$ arasa kaç sipariş numarası bulur? JavaScript'te m flag'iyle?
grep ve ripgrep ile Repo'da Arama
Bu yazının örnek repo'su Bookstore'un üç service'inden küçük bir kesit: sample/ altında OrderService.cs, PaymentService.cs ve BookService.cs. Hepsinde logger.LogInformation($"Order {orderNumber} shipped") gibi string interpolation'lı log çağrıları var; yazının sonuna doğru bunları message template'e çevireceğiz. Önce bulmak:
$ cat shell/search-logs.sh
#!/usr/bin/env bash
# -o yalnız eşleşen kısım, -i büyük/küçük harf duyarsız, -w yalnız bütün kelime
grep -oiwE 'payment [0-9]+' data/app.log
echo '---'
# -r alt klasörler, -n satır numarası, --include dosya filtresi
grep -rnE --include='*.cs' 'Log[A-Za-z]+\(\$"' sample
echo '---'
# -l yalnız dosya adları, -c dosya başına sayı
grep -rcE 'Log[A-Za-z]+\(\$"' sample
echo '---'
# rg: varsayılan olarak alt klasörlere iner, -t cs dosya türü, --sort path sabit sıra
rg -n --sort path -t cs 'Log\w+\(\$"' sample
echo '---'
# -g glob filtresi: Bookstore.Ordering dışındaki her şey
rg -l --sort path -g '!**/Bookstore.Ordering/**' 'Log\w+\(\$"' sample
echo '---'
# -U: pattern satır sonlarını aşabilir (\s, \n'i de yakalar)
rg -n -U 'Log\w+\(\s*\$"' sample/Bookstore.Payments
$ bash shell/search-logs.sh
Payment 4711
Payment 4712
Payment 4713
---
sample/Bookstore.Payments/PaymentService.cs:10: logger.LogInformation($"Payment {paymentId} authorized");
sample/Bookstore.Catalog/BookService.cs:11: logger.LogDebug($"Cache miss for {isbn}");
sample/Bookstore.Catalog/BookService.cs:14: logger.LogWarning($"Book {isbn} is out of stock");
sample/Bookstore.Ordering/OrderService.cs:9: logger.LogInformation($"Order {order.Number} created");
sample/Bookstore.Ordering/OrderService.cs:16: logger.LogInformation($"Order {orderNumber} shipped");
sample/Bookstore.Ordering/OrderService.cs:22: logger.LogWarning($"Order {orderNumber} cancelled: {reason}");
---
sample/Bookstore.Payments/PaymentService.cs:1
sample/Bookstore.Catalog/BookService.cs:2
sample/Bookstore.Ordering/OrderService.cs:3
---
sample/Bookstore.Catalog/BookService.cs:11: logger.LogDebug($"Cache miss for {isbn}");
sample/Bookstore.Catalog/BookService.cs:14: logger.LogWarning($"Book {isbn} is out of stock");
sample/Bookstore.Ordering/OrderService.cs:9: logger.LogInformation($"Order {order.Number} created");
sample/Bookstore.Ordering/OrderService.cs:16: logger.LogInformation($"Order {orderNumber} shipped");
sample/Bookstore.Ordering/OrderService.cs:22: logger.LogWarning($"Order {orderNumber} cancelled: {reason}");
sample/Bookstore.Payments/PaymentService.cs:10: logger.LogInformation($"Payment {paymentId} authorized");
---
sample/Bookstore.Catalog/BookService.cs
sample/Bookstore.Payments/PaymentService.cs
---
sample/Bookstore.Payments/PaymentService.cs:10: logger.LogInformation($"Payment {paymentId} authorized");
sample/Bookstore.Payments/PaymentService.cs:14: logger.LogError(
sample/Bookstore.Payments/PaymentService.cs:15: $"Payment {paymentId} declined");
İlk komuttaki -w önemli: Bookstore.Payments içindeki Payment bir kelimenin parçası olduğu için atlandı, yalnızca Payment 4711 gibi bağımsız kelimeler geldi. -i ile küçük harfli pattern büyük harfli metni buldu. grep -r dosyaları klasörün listelediği sırayla geziyor ve sıralamıyor; bu yüzden çıktıda önce Payments geldi. Her çalıştırmada aynı sırayı istiyorsan ripgrep'te --sort path kullan; bu seçenek ripgrep'in paralel aramasını kapatıyor, büyük repo'larda biraz yavaşlatıyor.
ripgrep'in grep'ten günlük işte ayrıldığı üç yer var. Klasörlere kendiliğinden iniyor. Git repo'su içinde .gitignore'u okuyup bin/, obj/, node_modules/ gibi klasörleri atlıyor. -t cs ile bilinen dosya türlerini, -g ile beşinci yazıda gördüğümüz glob'ları kullanarak dosya seçiyor; başına ! koyunca o glob dışarıda kalıyor.
Son komut regex'in satır sınırını gösteriyor. grep ve ripgrep varsayılan olarak her satırı ayrı ayrı arar; PaymentService.cs'teki ikiye bölünmüş LogError( çağrısı bu yüzden önceki aramalarda hiç çıkmadı. rg -U dosyayı bütün olarak arıyor ve \s* satır sonunu da yakalayabiliyor. Toplu değişiklik yaparken bu satırı unutmamak gerekecek.
Toplu Değişiklik: Önizle, Uygula, git diff ile Kontrol Et
.NET'te $"Order {orderNumber} shipped" biçimindeki log mesajı her çağrıda yeni bir string üretiyor ve log'a tek bir düz metin olarak gidiyor. Message template, yani "Order {OrderNumber} shipped", orderNumber ise değeri ayrı bir property olarak taşıyor; Seq gibi bir log aracında OrderNumber'a göre filtreleyebiliyorsun. Gerekçesi .NET'te Logging serisinin "Neyi Loglamalı, Neyi Loglamamalı" yazısında. Biz dönüşümün regex tarafına bakalım.
Kural şu: repo genelinde bir değişiklik yapmadan önce çalışma ağacı temiz olmalı, yani her şey commit'lenmiş olmalı. Böylece değişiklik git diff'te tek başına görünür ve beğenmezsen git restore . ile geri alırsın. Script bunu sample/'ın geçici bir git kopyasında yapıyor:
$ cat shell/refactor-logs.sh
#!/usr/bin/env bash
set -euo pipefail
# sample/'ın geçici bir git kopyasında çalış: her değişiklik incelenebilir ve geri alınabilir
repo=$(mktemp -d)
cp -r sample/. "$repo"
cd "$repo"
printf 'obj/\n' > .gitignore
git init -q && git add . && git -c user.name=Ada -c user.email=ada@example.com commit -qm "before refactor"
# git'in takip etmediği build çıktısı
mkdir -p Bookstore.Catalog/obj
echo 'logger.LogDebug($"Generated {id}");' > Bookstore.Catalog/obj/Generated.cs
# tek placeholder, sade bir identifier: Log...($"metin {ad} metin")
find='(Log[A-Za-z]+)\(\$"([^"{]*)\{([A-Za-z_][A-Za-z0-9_]*)\}([^"{]*)"\)'
echo "== which files?"
echo "grep -r: $(grep -rlE "$find" . | wc -l) files, git grep: $(git grep -lE "$find" | wc -l) files"
echo "== preview (rg -r changes nothing on disk)"
rg --sort path -n -o "$find" -r '$1("$2{$3}$4", $3)' .
echo "== apply (GNU sed, \u upper-cases the next character)"
git grep -lE "$find" | xargs sed -E -i "s/$find/\1(\"\2{\u\3}\4\", \3)/"
git diff --stat
git diff -U0 | grep '^[-+] '
echo "== still interpolated"
git grep -nE 'Log[A-Za-z]+\(\s*(\$"|$)'
rm -rf "$repo"
$ bash shell/refactor-logs.sh
== which files?
grep -r: 4 files, git grep: 3 files
== preview (rg -r changes nothing on disk)
./Bookstore.Catalog/BookService.cs:11:LogDebug("Cache miss for {isbn}", isbn)
./Bookstore.Catalog/BookService.cs:14:LogWarning("Book {isbn} is out of stock", isbn)
./Bookstore.Ordering/OrderService.cs:16:LogInformation("Order {orderNumber} shipped", orderNumber)
./Bookstore.Payments/PaymentService.cs:10:LogInformation("Payment {paymentId} authorized", paymentId)
== apply (GNU sed, \u upper-cases the next character)
Bookstore.Catalog/BookService.cs | 4 ++--
Bookstore.Ordering/OrderService.cs | 2 +-
Bookstore.Payments/PaymentService.cs | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
- logger.LogDebug($"Cache miss for {isbn}");
+ logger.LogDebug("Cache miss for {Isbn}", isbn);
- logger.LogWarning($"Book {isbn} is out of stock");
+ logger.LogWarning("Book {Isbn} is out of stock", isbn);
- logger.LogInformation($"Order {orderNumber} shipped");
+ logger.LogInformation("Order {OrderNumber} shipped", orderNumber);
- logger.LogInformation($"Payment {paymentId} authorized");
+ logger.LogInformation("Payment {PaymentId} authorized", paymentId);
== still interpolated
Bookstore.Ordering/OrderService.cs:9: logger.LogInformation($"Order {order.Number} created");
Bookstore.Ordering/OrderService.cs:22: logger.LogWarning($"Order {orderNumber} cancelled: {reason}");
Bookstore.Payments/PaymentService.cs:14: logger.LogError(
Adım adım gidelim. find değişkenindeki pattern bilerek dar: tek bir placeholder ve placeholder'ın içinde sade bir değişken adı. [^"{]* grupları metnin geri kalanında başka bir { ya da tırnak olmadığından emin oluyor; ikinci yazıdaki negated class kuralının aynısı. Pattern'i genişletip {order.Number} gibi ifadeleri de kapsamaya çalışmak mümkün, ama o zaman placeholder adını nasıl üreteceğine de karar vermen gerekiyor. Regex'in güvenle yapabildiği kısmı ona bırakıp gerisini elle yapmak daha az riskli.
"Which files" satırı git grep'in farkını gösteriyor. grep -r obj/ altındaki üretilmiş dosyayı da buldu; git grep yalnızca git'in takip ettiği dosyalarda arıyor. Build çıktısını, node_modules'u ya da bir başkasının commit'lenmemiş dosyasını değiştirmemek için toplu değişikliğin dosya listesini git grep -l ile almak iyi bir alışkanlık.
Önizleme rg -r ile: ripgrep'in replace seçeneği yalnızca çıktıyı değiştiriyor, diske hiçbir şey yazmıyor. Replacement syntax'ı $1, $2; -o ile yalnızca değişecek kısım görünüyor. Placeholder adları burada küçük harfle başlıyor, çünkü ripgrep'in replacement'ında harf dönüşümü yok.
Uygulama GNU sed ile. sed'in replacement'ında \u bir sonraki karakteri büyük harfe çeviriyor: orderNumber template'te OrderNumber oldu, argüman olarak giden değişken adı ise değişmedi. git diff --stat hangi dosyalarda kaç satırın değiştiğini, git diff -U0 değişen satırları bağlamsız gösteriyor. Son komut geriye kalanları listeliyor: {order.Number}, iki placeholder'lı satır ve iki satıra bölünmüş LogError( çağrısı. Bu üç satır elle düzeltilecek; .NET'in CA2254 analyzer'ı da sabit olmayan message template'leri işaretleyebiliyor, ayrıntısı Logging serisinde.
sed'in -i seçeneği platformlar arasında taşınabilir değil. GNU sed'de -i tek başına dosyayı yerinde değiştiriyor, yedek uzantısı ise bitişik yazılıyor: -i.bak. macOS'taki BSD (Berkeley Unix geleneğinden gelen) sed -i'den sonra her zaman bir uzantı argümanı bekliyor; sed -i 's/a/b/' dosya yazınca s/a/b/'yi uzantı, dosya adını da sed komutu sanıp hata veriyor. BSD'de doğru yazım sed -i '' ..., ama GNU sed'de aynı '' boş bir sed script'i oluyor ve asıl komut dosya adı sanılıyor. İki tarafta da çalışan tek biçim sed -i.bak ... ve sonra yedekleri silmek. Script'leri ekipte farklı sistemlerde çalıştırıyorsan bu farkı bilmek gerekir.
Windows'ta PowerShell: Select-String, -match ve -replace
Windows'ta grep yoksa (Git Bash ya da WSL kullanmıyorsan) PowerShell'in kendi araçları var ve hepsi .NET regex'i kullanıyor:
# Select-String, PowerShell'in grep karşılığıdır; varsayılan olarak büyük/küçük harf duyarsızdır
Get-ChildItem sample -Recurse -Filter *.cs | Sort-Object Name |
Select-String -Pattern 'Log\w+\(\$"' |
ForEach-Object { '{0}:{1}: {2}' -f $_.Filename, $_.LineNumber, $_.Line.Trim() }
'---'
# -match $Matches'ı doldurur; -cmatch büyük/küçük harf duyarlı biçimidir
'BKS-2026-000123' -match '^bks-(?<year>\d{4})'
$Matches.year
'BKS-2026-000123' -cmatch '^bks-'
'---'
# -replace .NET syntax'ı kullanır: çift tırnakta PowerShell $2 ve $1'i önce değişken olarak açar
'Herbert, Frank' -replace '(\w+), (\w+)', '$2 $1'
'Herbert, Frank' -replace '(\w+), (\w+)', "$2 $1"
'Herbert, Frank' -replace '(\w+), (\w+)', "`$2 `$1"
$ pwsh -NoProfile -File powershell/search-replace.ps1
BookService.cs:11: logger.LogDebug($"Cache miss for {isbn}");
BookService.cs:14: logger.LogWarning($"Book {isbn} is out of stock");
OrderService.cs:9: logger.LogInformation($"Order {order.Number} created");
OrderService.cs:16: logger.LogInformation($"Order {orderNumber} shipped");
OrderService.cs:22: logger.LogWarning($"Order {orderNumber} cancelled: {reason}");
PaymentService.cs:10: logger.LogInformation($"Payment {paymentId} authorized");
---
True
2026
False
---
Frank Herbert
Frank Herbert
Üç ayrıntı var. Birincisi, PowerShell'in regex operatörleri varsayılan olarak büyük/küçük harf duyarsız: -match '^bks-' BKS'yi buldu, duyarlı karşılaştırma için -cmatch, -creplace ve Select-String -CaseSensitive var. Bu, grep'ten ve .NET'in Regex class'ından ters bir varsayılan. İkincisi, -match başarılı olunca otomatik $Matches değişkenini dolduruyor ve isimli gruplara $Matches.year diye erişebiliyorsun.
Üçüncüsü en sık yapılan hata. -replace .NET'in $1 syntax'ını kullanıyor, ama çift tırnaklı bir string'de PowerShell $2 ve $1'i önce kendi değişkenleri olarak açıyor. Bu değişkenler tanımlı olmadığı için replacement tek bir boşluğa dönüştü ve çıktıda boş görünen satır oluştu; hata da yok. Replacement'ı tek tırnakla yaz, çift tırnak gerekiyorsa $'ı backtick ile kaçışla. PowerShell'in bu konudaki diğer ayrıntıları ilerideki PowerShell Scripting serisinde.
IDE'de Regex Find/Replace: VS Code, Visual Studio ve Rider
Aynı refactor'u IDE'de de yapabilirsin; avantajı sonucu her dosyada görüp tek tek onaylayabilmek. Üç IDE'yi bu makinede çalıştırmadım; aşağıdakiler her birinin resmî dokümanından:
| VS Code | Visual Studio | Rider | |
|---|---|---|---|
| Dosyalarda arama | Ctrl+Shift+F | Ctrl+Shift+F, replace için Ctrl+Shift+H | Ctrl+Shift+F, replace için Ctrl+Shift+R |
| Regex'i açmak | arama kutusundaki regex düğmesi | Alt+E | Alt+X |
| Regex syntax'ı | dosyalarda arama ripgrep ile | .NET | dokümanı Java'nın Pattern syntax'ını gösteriyor |
| Grup replacement | $1 | $1, ${name} | $1 |
| Harf dönüşümü | \u, \l, \U, \L | dokümanda yok | \u, \l, \U...\E, \L...\E |
| Çok satırlı arama | kutuda Ctrl+Enter ile yeni satır | \r?\n ile | Ctrl+Shift+Enter |
| Dosya filtresi | include/exclude glob'ları | File types ve klasör kümeleri | File mask |
Tablodaki satırların pratik karşılıkları şöyle. VS Code'da refactor'u birebir yapabilirsin: arama kutusuna find değişkenindeki pattern'i, replace kutusuna $1("$2{\u$3}$4", $3) yazıyorsun; files to include alanına **/*.cs. VS Code replace kutusuna yazarken bekleyen değişiklikleri diff olarak gösteriyor ve her dosyayı ayrı ayrı onaylayabiliyorsun. Dokümanına göre dosyalar arası arama ripgrep ile yapılıyor; beşinci yazıdaki ripgrep davranışı burada da geçerli.
Visual Studio .NET regex'ini kullanıyor; yani dördüncü yazıdaki inline flag'ler ve lookaround'lar olduğu gibi çalışıyor, replacement'ta $1 ve ${name} var. Dokümanında replacement için harf dönüşümü geçmiyor; placeholder adlarını büyük harfle başlatmak istiyorsan ikinci bir adım gerekiyor. Dokümanın kendi notu da önemli: değişken ya da method adı değiştirmek için find/replace yerine rename refactoring'i kullan, çünkü o kodun anlamını biliyor, regex bilmiyor. Rider'ın dokümanı syntax referansı olarak Java'nın regex dokümanına yönlendiriyor, replace kutusunda $1 ve \u gibi harf dönüşümlerini destekliyor.
Üç IDE'de de dosya filtresi glob ile veriliyor, arama ise regex ile. Beşinci yazıdaki ayrım burada da işe yarıyor.
CRLF ve $: Windows'tan Gelen Dosyalar
Tahmine dönelim. Windows'ta satırlar \r\n (CRLF: satır başına dönüş ve yeni satır karakterleri) ile, Linux'ta yalnızca \n ile biter. Regex'in $'ı çoğu araçta yalnızca \n'i tanıyor; \r ise satırın sıradan bir karakteri olarak kalıyor:
$ cat shell/crlf.sh
#!/usr/bin/env bash
# log'un Windows satır sonlu bir kopyası
crlf=$(mktemp)
sed 's/$/\r/' data/app.log > "$crlf"
tail -1 "$crlf" | od -c | tail -2
# $ \n'den önce; \r hâlâ satırın parçası
grep -c 'BKS-2026-000126$' data/app.log
grep -c 'BKS-2026-000126$' "$crlf"
grep -c $'BKS-2026-000126\r$' "$crlf"
# rg --crlf: $ \r\n'den önce de eşleşir
rg -c 'BKS-2026-000126$' "$crlf" || echo "rg: 0"
rg -c --crlf 'BKS-2026-000126$' "$crlf"
rm -f "$crlf"
$ bash shell/crlf.sh
0000120 0 0 1 2 6 \r \n
0000127
1
0
1
rg: 0
1
Tahminin grep cevabı 0. od -c satırın sonunu karakter karakter gösteriyor: 6'dan sonra \r ve \n var, $ ise \n'den önceki pozisyonda; arada \r duruyor. Bash'in $'...' string'iyle \r'yi pattern'e koymak ya da ripgrep'in --crlf seçeneği çözüm. .NET ve JavaScript'te aynı metin:
using System.Text.RegularExpressions;
// Windows'tan geldiği hâliyle log: her satır \r\n ile bitiyor
string text = File.ReadAllText("data/app.log").ReplaceLineEndings("\r\n");
// Multiline'da $ yalnızca \n'den önce eşleşir, aradaki \r eşleşmeyi engeller
Console.WriteLine(Regex.Count(text, @"BKS-\d{4}-\d{6}$", RegexOptions.Multiline));
Console.WriteLine(Regex.Count(text, @"BKS-\d{4}-\d{6}\r?$", RegexOptions.Multiline));
// . \r'yi de yakalar, bu yüzden .+$ değere \r'yi de taşır
string last = Regex.Match(text, @"declined.+$", RegexOptions.Multiline).Value;
Console.WriteLine(last.Replace("\r", "\\r"));
import { readFileSync } from "node:fs";
// aynı metin JavaScript'te: \r, ^, $ ve . için satır sonudur
const text = readFileSync("data/app.log", "utf8").replace(/\n/g, "\r\n");
console.log(text.match(/BKS-\d{4}-\d{6}$/gm).length);
console.log(text.match(/declined.+$/m)[0].replace("\r", "\\r"));
$ dotnet run dotnet/crlf.cs
0
3
declined for BKS-2026-000126\r
$ node js/crlf.mjs
3
declined for BKS-2026-000126
.NET hiçbir sipariş numarası bulmadı, \r?$ ile üçünü de buldu. Daha sinsi olanı üçüncü satır: .+$ ile alınan değer sonunda \r taşıyor. Bu değeri bir dictionary'de key olarak kullanırsan ya da başka bir string'le karşılaştırırsan eşit çıkmaz ve sebebini ekranda göremezsin. JavaScript ise \r'yi de satır sonu sayıyor; aynı pattern üç sonuç verdi ve değer temiz.
.NET 11 bunun için yeni bir option getirdi. Microsoft Learn'deki RegexOptions sayfasına göre RegexOptions.AnyNewLine, ^, $ ve .'nın \r\n, \r, \n ve Unicode'un diğer satır sonu karakterlerini tanımasını sağlıyor ve \r\n'yi tek bir satır sonu olarak görüyor. Bu dosya ayrı bir klasörde, çünkü .NET 11 SDK'sı gerekiyor:
using System.Text.RegularExpressions;
// .NET 11: AnyNewLine ile ^, $ ve . \r\n, \r ve \n'i satır sonu sayar
string text = File.ReadAllText("data/app.log").ReplaceLineEndings("\r\n");
RegexOptions options = RegexOptions.Multiline | RegexOptions.AnyNewLine;
Console.WriteLine(Regex.Count(text, @"BKS-\d{4}-\d{6}$", options));
Console.WriteLine(Regex.Match(text, @"declined.+$", options).Value.Replace("\r", "\\r"));
// NonBacktracking ya da ECMAScript ile birlikte kullanılamaz
try
{
_ = new Regex("x$", options | RegexOptions.NonBacktracking);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine(ex.Message);
}
$ dotnet run dotnet11/any-newline.cs
3
declined for BKS-2026-000126
Specified argument was out of the range of valid values. (Parameter 'options')
.NET 10'da kalıyorsan iki yol var: dosyayı File.ReadAllLines ile satır satır okumak (üçüncü yazıda yaptığımız gibi, satır sonları zaten atılıyor) ya da pattern'e \r? eklemek. Repo içindeki dosyalar için kalıcı çözüm ise satır sonlarını standart hâle getirmek: bu serinin snapshot'larında olduğu gibi .gitattributes'a * text=auto eol=lf yazınca Git, Windows'ta checkout edilen dosyaları da LF (yalnız \n) satır sonuyla tutuyor.
Üretimde Dikkat
Toplu değişikliği her zaman temiz bir çalışma ağacında yap ve önce önizle. Sıra şu: git status boş mu, rg -r ya da IDE'nin önizlemesiyle neyin değişeceğini gör, uygula, git diff --stat ile dosya ve satır sayısının beklediğin gibi olduğunu kontrol et, build ve testleri çalıştır, sonra commit'le. Bir şey ters giderse git restore . tek komut.
Dosya listesini git grep -l ya da rg -l ile al; grep -r ve find, bin/, obj/, node_modules/ ve .git/ klasörlerine de girer. .git/ içindeki bir dosyayı sed ile değiştirmek repo'yu bozabilir.
Üretilmiş kodu (Entity Framework Core migration'ları, *.g.cs dosyaları, OpenAPI'den üretilen client'lar) toplu değişikliğin dışında tut ya da kaynağından yeniden üret. Elle yapılan değişiklik bir sonraki üretimde kaybolur.
Log dosyasında arama yaparken dosyanın nereden geldiğini bil. Windows sunucudan, bir Windows container'ından ya da bir müşterinin gönderdiği CSV (virgülle ayrılmış değerler) dosyasından gelen metinde CRLF olması normal. file komutu ya da od -c ile bir kez bakmak, yarım saatlik "neden bulmuyor" aramasından kısa sürer.
Sık Düşülen Tuzaklar
Değişikliği commit'siz çalıştırmak
sed -i ya da IDE'nin Replace All'u geri alma adımı sunmaz. Önce commit'le, sonra değiştir; git diff incelemeni ve git restore geri almanı sağlar.
CRLF'li dosyada $
grep, .NET ve ripgrep'in varsayılan $'ı \r'den önce eşleşmez. \r?$ yaz, rg --crlf kullan, .NET 11'de RegexOptions.AnyNewLine'ı aç ya da satır sonlarını normalize et.
PowerShell'de replacement'ı çift tırnakla yazmak
"$2 $1" PowerShell değişkenlerine açılır ve sonuç boş olur. Tek tırnak kullan.
BSD sed ile GNU sed'i aynı sanmak
sed -i ikisinde farklı argüman bekler. Taşınabilir biçim sed -i.bak.
Regex'i rename refactoring yerine kullanmak
Bir method'un ya da property'nin adını değiştirmek için IDE'nin rename'i kodun anlamını bilir; regex ise aynı adı taşıyan string'leri, yorumları ve başka class'ların üyelerini de değiştirir.
Küçük Bir Deneme
★☆☆ data/app.log'da ERR ve WRN satırlarının sayısını tek bir grep -cE komutuyla, sonra aynı şeyi PowerShell'de Select-String ile bul.
★★☆ refactor-logs.sh'deki pattern'i iki placeholder'lı çağrıları da ($"Order {orderNumber} cancelled: {reason}") dönüştürecek biçimde genişlet. Önizlemede hangi yeni sorunlar çıkıyor?
★★★ Aynı refactor'u .NET'te Regex.Replace ve bir MatchEvaluator ile yaz: her placeholder'ı bul, adını PascalCase'e çevir ve argüman listesini sırayla oluştur. İki satıra bölünmüş LogError( çağrısını da yakalamak için hangi option gerekir?
Komut satırı ve IDE tarafını bitirdik. Bir sonraki yazıda yalnızca .NET'e bakacağız: interpreter, RegexOptions.Compiled ve [GeneratedRegex] arasındaki fark ve BenchmarkDotNet ile ölçümü, allocation yapmayan Span API'leri, timeout ve NonBacktracking, Bookstore için ISBN (kitapların uluslararası standart numarası) doğrulama ve Türkçe karakterli slug üretimi.