awk ile Alan Ayırıcı Belirleme ve Çok Karakterli Delimiter Kullanımı

Metin işleme dünyasında awk ile zaman geçirdikçe şunu fark ediyorsunuz: aracın gücünün yarısı alan ayırıcıyı doğru anlamaktan geliyor. Bir CSV dosyasını parse etmeye çalışıyorsunuz, veri gelmiyor. Log dosyasındaki alanları çekmeye uğraşıyorsunuz, çıktı bir türlü beklediğiniz gibi olmuyor. Çoğu zaman sorun awk‘ın kendisinde değil, delimiter’ı yanlış ya da eksik tanımlamakta yatıyor. Bu yazıda alan ayırıcı meselesini her boyutuyla ele alacağız; basit karakterlerden çok karakterli delimiter’lara, regex tabanlı ayırıcılardan dinamik kullanıma kadar.

Temel Mantık: FS Nedir, Ne İşe Yarar?

awk her satırı işlerken o satırı alanlara böler. Bu bölme işleminin nasıl yapılacağını Field Separator (FS) değişkeni belirler. Varsayılan olarak FS, bir ya da birden fazla boşluk veya tab karakteridir. Yani normalde hiçbir şey belirtmesek de awk boşluklarla ayrılmış verileri sorunsuz okur.

echo "ali veli ayse" | awk '{print $2}'
# Çıktı: veli

Bu gayet güzel çalışır. Ama gerçek dünya verileri nadiren bu kadar temiz olur. Üretim ortamlarında karşılaştığım dosyaların büyük çoğunluğu ya iki nokta üst üste ile ayrılmış, ya pipe (|) karakterli, ya da bazen iki-üç karakterlik özel bir delimiter içeriyor. İşte o zaman FS’i açıkça tanımlamak şart oluyor.

FS’i tanımlamanın iki yolu var:

  • -F seçeneği: Komut satırında doğrudan belirtmek için
  • BEGIN bloğunda atama: Daha karmaşık senaryolar için
# -F ile kullanım
awk -F':' '{print $1}' /etc/passwd

# BEGIN bloğu ile kullanım
awk 'BEGIN{FS=":"} {print $1}' /etc/passwd

Her iki kullanım da aynı sonucu verir. Ama BEGIN bloğu, özellikle birden fazla ayırıcı veya koşullu ayırıcı kullanmak istediğinizde çok daha esnek bir yapı sunuyor.

Tek Karakterli Ayırıcılar

En sık kullanılan ve en az sorun çıkaran durum budur. CSV benzeri dosyalar, /etc/passwd, Apache access log’ları, özel uygulama logları çoğunlukla tek bir karakterle ayrılmış veriler içerir.

# /etc/passwd dosyasından kullanıcı adı ve home dizini
awk -F':' '{print $1, $6}' /etc/passwd

# Çıktı örneği:
# root /root
# daemon /usr/sbin
# www-data /var/www

Tab ayırıcı için t kullanabilirsiniz:

# Tab karakteriyle ayrılmış dosya
awk -F't' '{print $1, $3}' dosya.tsv

Pipe karakteri kullanırken dikkat etmek gerekiyor. Pipe, shell için özel anlam taşıdığından tırnak içine almak şart:

# Pipe ile ayrılmış log dosyası
awk -F'|' '{print $2, $4}' uygulama.log

Nokta (.) karakteri de sık kullanılıyor, ama burada bir tuzak var. FS regex olarak yorumlandığında nokta “herhangi bir karakter” anlamına gelir. Bunu aşmak için kaçış karakteri kullanılmalı:

# Yanlış - nokta her karakteri eşleştirir
awk -F'.' '{print $1}' versiyon.txt

# Doğru - literal nokta
awk -F'[.]' '{print $1}' versiyon.txt
# ya da
awk -F'\.' '{print $1}' versiyon.txt

Çok Karakterli Delimiter Kullanımı

İşte asıl güzellik burası başlıyor. awk‘ın bir özelliği var ki birçok kişi bunun farkında değil: FS birden fazla karakter içerdiğinde regex olarak yorumlanır. Bu son derece güçlü bir özellik.

Diyelim ki bir uygulama log dosyanız var ve alanlar :: (boşluk çift nokta boşluk) ile ayrılıyor:

# Örnek log satırı:
# 2024-01-15 :: ERROR :: database connection failed :: retry_count=3

awk -F' :: ' '{print $2, $4}' uygulama.log
# Çıktı: ERROR retry_count=3

Ya da iki karakterli bir ayırıcı, mesela ||:

# Veri: kullanici||sifre_hash||email||rol
echo "admin||abc123hash||[email protected]||superuser" | awk -F'||' '{print $1, $3}'

Dur bir dakika. Burada ciddi bir tuzak var. || regex olarak yorumlandığında “boş string veya boş string” anlamına gelir, yani pratikte her karakter arasında eşleşir. Doğru kullanım şu şekilde olmalı:

# Doğru - kaçış karakterleriyle
echo "admin||abc123hash||[email protected]||superuser" | awk -F'\|\|' '{print $1, $3}'
# Çıktı: admin [email protected]

# Ya da karakter sınıfı kullanarak
echo "admin||abc123hash||[email protected]||superuser" | awk -F'[|][|]' '{print $1, $3}'

Bir başka yaygın senaryo: çift boşlukla ayrılmış veriler. Bunu da regex ile kolayca halledebilirsiniz:

# İki ya da daha fazla boşlukla ayrılmış
echo "kolon1  kolon2   kolon3" | awk -F' +' '{print $2}'
# Çıktı: kolon2

Regex Tabanlı Ayırıcılar

FS’in regex desteklemesi, son derece esnek parsing imkanı sunuyor. Üretim ortamında bununla ilgili çok güzel bir örnek yaşadım: bir müşterinin legacy sisteminden gelen log dosyasında ayırıcı bazen virgül, bazen noktalı virgül, bazen de sadece tab oluyordu. Tek bir awk komutuyla bunu çözmek mümkün:

# Virgül, noktalı virgül veya tab ile ayrılmış karma veri
awk -F'[,;t]' '{print $1, $2, $3}' karma_veri.txt

Rakamları ayırıcı olarak kullanmak da mümkün, her ne kadar garip gelse de:

# Rakamları ayırıcı olarak kullan
echo "veri1bilgi2sonuc" | awk -F'[0-9]' '{print $1, $2, $3}'
# Çıktı: veri bilgi sonuc

Daha gerçekçi bir senaryo: Apache log dosyalarını parse etmek. Apache combined log formatında köşeli parantez, tırnak işareti gibi birden fazla özel karakter var:

# Apache access log parsing - IP ve HTTP status code
awk -F'"' '{print $1, $3}' /var/log/apache2/access.log | awk '{print $1, $NF}'

# Daha temiz bir yaklaşım
awk '{
    match($0, /^([0-9.]+)/, ip)
    match($0, /" ([0-9]+) /, status)
    print ip[1], status[1]
}' /var/log/apache2/access.log

OFS: Çıktı Alan Ayırıcısı

FS’ten bahsedip OFS’i atlamak olmaz. Output Field Separator (OFS) çıktıda alanlar arasına ne konulacağını belirler. Varsayılan olarak boşluktur, ama bunu değiştirmek çok işe yarıyor.

# /etc/passwd'den kullanıcı ve shell bilgisini CSV olarak al
awk -F':' 'BEGIN{OFS=","} {print $1, $7}' /etc/passwd
# Çıktı:
# root,/bin/bash
# daemon,/usr/sbin/nologin

FS ve OFS’i birlikte kullanarak format dönüşümü yapmak son derece pratik:

# Tab'lı dosyayı CSV'ye çevir
awk -F't' 'BEGIN{OFS=","} {print $1,$2,$3,$4}' dosya.tsv > dosya.csv

# Virgülle ayrılmış dosyayı pipe ile ayrılmış formata çevir
awk -F',' 'BEGIN{OFS="|"} {$1=$1; print}' input.csv > output.psv

Buradaki $1=$1 numarasına dikkat edin. Bu işlem gereksiz gibi görünse de awk‘ı tüm satırı yeniden oluşturmaya zorlar. Bu olmadan OFS değişikliği print $1,$2 gibi açık çıktılarda çalışır ama print $0 ile satırı olduğu gibi basmak istediğinizde OFS uygulanmaz.

BEGIN Bloğunda Dinamik FS Kullanımı

Bazen dosyanın ilk satırına göre ya da bir dış parametreye göre FS’i dinamik olarak belirlemek gerekebilir. Bu tür durumlar için BEGIN bloğu veya ARGV kullanımı devreye giriyor:

# Komut satırından ayırıcı geçirme
awk -v sep='::' 'BEGIN{FS=sep} {print $1, $2}' dosya.txt

# Daha pratik: shell değişkeninden ayırıcı alma
DELIMITER=","
awk -v ayirici="$DELIMITER" 'BEGIN{FS=ayirici} {print $2}' veri.csv

Bu yaklaşım özellikle script yazarken çok işe yarıyor. Ayırıcıyı hardcode etmek yerine parametre olarak almak, scriptinizi çok daha yeniden kullanılabilir hale getiriyor.

Gerçek Dünya Senaryosu: Nginx Log Analizi

Bir prodüksiyon senaryosu üzerinden gidelim. Nginx log dosyasını parse edip endpoint bazında response time ortalaması hesaplayalım:

# Nginx log formatı (custom):
# 2024-01-15T10:23:45 GET /api/users 200 0.045 192.168.1.100

awk '
BEGIN {
    FS = " "
    OFS = "t"
}
{
    endpoint = $3
    response_time = $5
    count[endpoint]++
    total[endpoint] += response_time
}
END {
    print "Endpoint", "İstek Sayısı", "Ort. Yanıt Süresi (s)"
    for (ep in count) {
        printf "%st%dt%.4fn", ep, count[ep], total[ep]/count[ep]
    }
}
' /var/log/nginx/access.log | sort -t't' -k3 -rn | head -20

Bu script, en yavaş endpoint’leri bulmanıza yardımcı oluyor. Prodüksiyonda performans sorununu tespit etmek için bu tür hızlı analizler inanılmaz değerli.

Gerçek Dünya Senaryosu: Çok Karakterli Delimiter ile Konfigürasyon Dosyası Parse Etme

Bazı uygulamalar konfigürasyon dosyalarında alışılmadık ayırıcılar kullanıyor. Bir örnek görelim:

# config.dat içeriği:
# sunucu_adi => db-master-01
# port => 5432
# max_conn => 100
# timeout => 30

# Sadece değerleri çek
awk -F' => ' '{print $2}' config.dat

# Anahtar-değer çiftlerini bash değişkenlerine yükle
while IFS= read -r line; do
    key=$(echo "$line" | awk -F' => ' '{print $1}' | tr -d ' ')
    value=$(echo "$line" | awk -F' => ' '{print $2}')
    echo "export ${key}=${value}"
done < config.dat

Ya da doğrudan awk içinde işlemek:

awk -F' => ' '
/^[^#]/ {
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", $1)
    gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2)
    config[$1] = $2
}
END {
    print "Sunucu:", config["sunucu_adi"]
    print "Port:", config["port"]
}
' config.dat

Birden Fazla Dosyada Farklı Ayırıcı Kullanmak

awk birden fazla dosyayı ardışık işleyebilir. Peki her dosyanın farklı bir ayırıcısı varsa ne yapacaksınız? FILENAME değişkeni ve koşullu FS ataması burada imdada yetişiyor:

# dosya1.csv virgülle, dosya2.tsv tab ile ayrılmış
awk '
FNR == 1 {
    if (FILENAME ~ /.csv$/) FS = ","
    else if (FILENAME ~ /.tsv$/) FS = "t"
    else FS = ":"
}
{
    print FILENAME": "$1, $2
}
' dosya1.csv dosya2.tsv /etc/passwd

Bu teknik özellikle farklı kaynaklardan gelen verileri birleştirmek zorunda kaldığınızda hayat kurtarıyor.

Sık Yapılan Hatalar ve Çözümleri

Birkaç yaygın hatadan bahsetmeden geçemeyeceğim:

Boş alan sorunu: Çift delimiter yan yana geldiğinde awk bu ikisi arasında boş bir alan görür. Bu bazen istenen davranış, bazen de sorun kaynağı olur.

# "a,,b,c" verisinde $2 boş olacak
echo "a,,b,c" | awk -F',' '{print NF, $2, $3}'
# Çıktı: 4  b

# Boş alanları atlamak istiyorsanız
echo "a,,b,c" | awk -F',+' '{print NF, $2}'
# Çıktı: 3 b

Satır sonu karakteri sorunu: Windows’tan gelen dosyalarda rn satır sonu var. Bu son alana yapışıp kalır:

# r karakterini temizle
awk -F',' '{gsub(/r/, ""); print $NF}' windows_dosyasi.csv

# Ya da doğrudan RS ile
awk 'BEGIN{RS="rn"; FS=","} {print $NF}' windows_dosyasi.csv

Büyük-küçük harf karışıklığı: -F büyük harf kullanırsanız, yani -f yazarsanız, awk bunu bir script dosyası olarak yorumlamaya çalışır. Küçük harfle -F delimiter, büyük harfle -F yok, küçük -f script dosyası.

NF ve NR ile Birlikte Kullanım

Alan sayısını ve satır numarasını doğru ayırıcıyla birlikte kullanmak çok güçlü sonuçlar veriyor:

# Her satırda kaç alan var, dinamik olarak son iki alanı yazdır
awk -F'::' '{print NR": Alan sayisi="NF, "Son iki alan:", $(NF-1), $NF}' log.txt

# Belirli sayıda alan içeren satırları filtrele
awk -F',' 'NF == 5 {print}' veri.csv

# Eksik alan içeren satırları bul ve raporla
awk -F'|' 'NF < 8 {print NR": Eksik alan - sadece "NF" alan var: "$0}' veri.psv

Bunların hepsini bir monitoring scriptinde kullanabilirsiniz. Mesela günlük gelen veri dosyasını doğrulamak için:

#!/bin/bash
DOSYA=$1
BEKLENEN_ALAN=7
HATALI=0

awk -F',' -v beklenen="$BEKLENEN_ALAN" '
NR > 1 {
    if (NF != beklenen) {
        print "HATA - Satir "NR": Beklenen "beklenen" alan, bulunan "NF" alan"
        hatali++
    }
}
END {
    if (hatali > 0)
        print "nToplam "hatali" satirda format hatasi bulundu."
    else
        print "Tum satirlar gecerli formatta."
}
' "$DOSYA"

Sonuç

awk‘da alan ayırıcı konusu ilk bakışta basit görünüyor: -F',' yazarsın olur biter. Ama derine indikçe regex tabanlı ayırıcılar, çok karakterli delimiter’lar, OFS ile format dönüşümleri, dinamik FS atamaları gibi katmanlar ortaya çıkıyor. Bu katmanları kavradığınızda awk gerçek anlamda elinizin altındaki en güçlü metin işleme aracına dönüşüyor.

Özellikle şu iki noktayı aklınızda tutun: Birincisi, FS birden fazla karakter içerdiğinde regex olarak yorumlanır ve bu hem güç hem de tuzak demektir. Özel regex karakterlerini (|, ., * vb.) literal olarak kullanmak istiyorsanız mutlaka kaçış karakteri kullanın ya da karakter sınıfına ([]) alın. İkincisi, $1=$1 ile OFS’i aktif hale getirme numarasını unutmayın; özellikle format dönüşümü işlemlerinde bunu çok kullanacaksınız.

Prodüksiyonda log analizi, veri temizleme, format dönüşümü, konfigürasyon dosyası okuma gibi onlarca farklı görevde bu bilgilerin doğrudan işinize yarayacağını göreceksiniz. awk öğrenmeye yatırım yapılan her dakika, terminalde geçirilen zamanı ciddi ölçüde daha verimli hale getiriyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir