Awk ve Sed Komutları İle Pratik İşlemler

Güncelleme Yayın

datamash yazımda awk ve sed komutlarına referansta bulunmuştum. Genel olarak metin işlemlerinde dair örnekler barındıran sed ve awk yazılarını AWK ve SED Komutları İle Pratik İşlemler başlığı altında datamash benzerlikleri ve farklılıkları ile yeniden ele almak, basit veri işlemleri ile komut örneklerini zenginleştirmek istiyorum.

Awk ve Sed Komutları

Örneklere geçmeden önce komutlarla ilgili kısa bir hatırlatma yapayım. Awk, örüntü temelli tarama ve işleme dilidir. Evet, komut kullanımının dışında kapsamlı işlemler yapmanızı mümkün kılan bir dil olma özelliğine sahiptir. NAWK ve GAWK adında daha gelişmiş yeni versiyonları / alternatifleri bulunur. Sed,lListeleme, işaretleme, sorgulama, örüntülerle metin işlemleri gerçekleştirme gibi özelliklere sahiptir. Şimdi bu kısa açıklamalara dair örnekler görelim.

Awk ve Sed İle Temel İşlemler

cat komutu ve pipe aracılığıyla dosyalarımızı ilgili işlemlere dosya adını açık bir şekilde belirterek dahil etmiştik. Bu yazı altında, önceki örnekleri de zenginleştirmek adına dosya adını bir değişken üzerinden kullanacağım.

Değişken Tanımlama

İşlemimiz oldukça basit; dosyamızın adı test.txt, değişkenimizin adı table olsun ve dosyamız işlemleri gerçekleştirdiğimiz dizinde bulunsun. table sadece değer olarak test.txt ifadesine sahip olacağı için dosyamızın bu aşamada oluşturulmuş olması gerekmiyor.

table="test.txt"

Dosya eğer farklı bir dizinde yer alıyorsa, çağırdığımız zaman hata dönmemesi için aşağıdaki örneklerde olduğu gibi dosyanın net bir şekilde bulunduğu dizini belirtmeliyiz.

table="./klasor/test.txt"
table="../test.txt"
table="/Users/[kullanici-adi]/Desktop/test.txt"

table değişkenini test edelim. Bunun için $ işaretini kullanabiliriz. Bu işaret sonrasında belirttiğimiz ifadenin bir değer taşıdığını gösterir. Aşağıda farkı görebilmeniz için iki farklı şekilde işlem gerçekleştireceğim.

echo table

Bu komutu uyguladığımızda echo ekrana belirttiğimiz ifadeyi yazdırır ve komutun dönüşü yine table olur.

echo $table

$ kullandığımızda ise echo belirtilen ifadenin bir değişken olduğunu görür ve değişkenin sahip olduğu karşılığı yazdırır. Dolayısıyla yukarıda belirttiğimiz dosya yolu dönecektir. Dosyanın içeriği herhangi bir şekilde işleme alınmaz. Dolayısıyla dosya içeriğinde değişiklikler söz konusu ise değişkeni her çağırdığımızda yeni içeriği işleme almış oluruz. Benim aldığım dönüş şu şekilde oldu:

/Users/[kullanici-adi]/Desktop/test.txt

Unutmadan, tanımlanan tüm değişkenleri set komutunu kullanarak listeleyebilirsiniz.

set

Komut uygulandıktan sonra yarattığımız table değişkeni de dahil olmak üzere ilgili oturumda kullanılabilecek tüm değişkenler listelenecektir. Bu listede yer alan bir değişkeni unset komutuyla silebiliriz.

unset table

env tanımı yapmadığımız için değişkenlerimiz ve aldıkları değerler oturumumuz sürecinde geçerli olacaklardır. Bu açıklamaların hemen ardından, veri işlemleri için kullanacağımız test.txt dosyamızın da içeriğine bir göz atalım.

OrderDate;Region;Rep;Item;Units;UnitCost;Total
1.6.18;East;Jones;Pencil;95;1.99;189.05
1.23.18;Central;Kivell;Binder;50;19.99;999.50
2.9.18;Central;Jardine;Pencil;36;4.99;179.64
2.26.18;Central;Gill;Pen;27;19.99;539.73
3.15.18;West;Sorvino;Pencil;56;2.99;167.44
4.1.18;East;Jones;Binder;60;4.99;299.40
4.18.18;Central;Andrews;Pencil;75;1.99;149.25
5.5.18;Central;Jardine;Pencil;90;4.99;449.10
6.6.18;East;Jardine;Pencil;95;5.99;189.05
6.13.18;West;Kivell;Binder;50;1.99;999.50
7.19.18;Central;Kivell;Pencil;96;4.99;179.64
7.6.18;Central;Gill;Pencil;27;9.99;539.73
7.5.18;East;Jones;Binder;56;3.99;167.44
8.13.18;West;Gill;Pencil;60;49.99;299.40
9.28.18;East;Andrews;Pen;75;12.99;149.25
9.25.18;Central;Jardine;Pencil;20;4.99;449.10

17 satır (row) ve 7 sütundan (col) oluşan değerler comma separated (;) olarak ayrılmış durumda ve ilk satırımızda başlıklar (heading) yer alıyor. İlk olarak head ve tail komutlarıyla test.txt dosyasının içeriğine kabaca göz atalım.

Head ve Tail Kullanımı

head $table

Komutu uygulamamızın ardından şu dönüşü alırız:

OrderDate;Region;Rep;Item;Units;UnitCost;Total
1.6.18;East;Jones;Pencil;95;1.99;189.05
1.23.18;Central;Kivell;Binder;50;19.99;999.50
2.9.18;Central;Jardine;Pencil;36;4.99;179.64
2.26.18;Central;Gill;Pen;27;19.99;539.73
3.15.18;West;Sorvino;Pencil;56;2.99;167.44
4.1.18;East;Jones;Binder;60;4.99;299.40
4.18.18;Central;Andrews;Pencil;75;1.99;149.25
5.5.18;Central;Jardine;Pencil;90;4.99;449.10
6.6.18;East;Jardine;Pencil;95;5.99;189.05

head ve tail aksi belirtilmediği sürece 10 değeri üzerinden işlem yürütürler. Yukarıda da görüldüğü üzere test.txt dosyamızın içeriğinde yer alan ilk 10 satır bize iletilmiş durumda. Şimdi de tail ile son 10 satırı alalım.

tail $table

Komut uygulandıktan sonra şu dönüşü alırız:

4.18.18;Central;Andrews;Pencil;75;1.99;149.25
5.5.18;Central;Jardine;Pencil;90;4.99;449.10
6.6.18;East;Jardine;Pencil;95;5.99;189.05
6.13.18;West;Kivell;Binder;50;1.99;999.50
7.19.18;Central;Kivell;Pencil;96;4.99;179.64
7.6.18;Central;Gill;Pencil;27;9.99;539.73
7.5.18;East;Jones;Binder;56;3.99;167.44
8.13.18;West;Gill;Pencil;60;49.99;299.40
9.28.18;East;Andrews;Pen;75;12.99;149.25
9.25.18;Central;Jardine;Pencil;20;4.99;449.10

Görüntülenmesini istediğimiz satır sayısını belirtmek istersek -n kullanırız.

head -n 5 $table
tail -n 5 $table

head kullanımında dikkatinizi çekmiştir, başlık da yine listeleme içerisinde yer almakta. Bir dosyanın başlık ve/ya toplam değerler barındırıp barındırmadığından emin olmak için head ve tail oldukça pratik ve işlevsel araçlardan biri. Başlıkları görüntülemeden değerleri listelemek istersek ne yapmalıyız?

tail -n+2 $table

Yukarıdaki komut ile 2. satırdan itibaren bize içeriği döndürecektir. Evet, başlığı artık görmüyoruz. Bu işlemi bir de sed ve awk ile gerçekleştirelim.

Awk İşlemleri

awk '{if(NR>1)print}' $table
awk '{if(NR!=1)print}' $table
awk '{if(NR==1){next}print}' $table

Yukarıdaki örneklerde 1,1d, '1!p' ve NR ifadeleri hep ilk satır sonrasının işleme alınması gerektiğini belirtmekteler. Tablodan kesit alma işlemini biraz daha farklılaştıralım ve random olarak ilgili dosya içerisinden bir dilim getirelim.

head -$((${RANDOM} % \`wc -l < $table\` + 10)) $table | tail -10

wc -l < $table bize word count verisi sunmakta ve -l ile satır değerini alabilmekteyiz. Dolayısıyla, yukarıdaki kodumuz bize ilgili dosya içerisinden rasgele bir alan belirlemekte ve +10 ile dilimin kapsamını tanımlamış olmaktayız. Bu sayede head ve tail ile edindiğimiz alanları tekrar görmemiş oluruz. Ek bir not, satır sayısını ayrıca şu şekilde de edinebiliriz.

awk 'END{print NR}' $table

İlgili dosya içeriğine hızlıca göz attık ve temel düzeyde bir bilgi edindik. Artık işlemlerimizi çeşitlendirebiliriz. Bu işlem için awk komutunu kullanacağım. Öncesinde temel bir bilgi eklemek istiyorum. awk komutu ile sütun olarak ifade edebileceğimiz ayrılmış alanları (space, tab space, noktalı virgül, virgül, pipe vb.) değişkenler olarak sıralı bir şekilde edinebilmekteyiz. test.txt dosyamız içerisinde 7 sütun olduğunu biliyoruz.

awk -F';' '{print NF; exit}' $table

Komut bize ; ile ayrıştırılmış alan sayısını verecektir. 7 değerini aldık, değil mi? $n ile de tüm sütunları yazdırabiliriz. Unutmadan, -F';' sütunların ; ile ayrılmış olduğunu ve işlemin buna göre gerçekleştirilmesi gerektiğini belirtmekte. Farklı bir ayrıştırıcı (örneğin pipe) kullanılmışsa ilgili alanın buna uygun şekilde düzenlenmesi gerekmektedir.

awk -F';' '{print $n}' $table

O halde doğru yoldayız ve $1, $2, $3..., $7 şeklinde her sütunu ayrı ayrı edinebildiğimize göre ilk sütun ile başlayalım.

awk -F';' '{print $1}' $table

Yukarıdaki komut ile 1. sütunu listelemiş oluruz. Örneğimizi head komutuna dair edindiğimiz bilgi ile pekiştirelim.

awk -F';' 'NR!=1 { print $3 }' $table | head -n 5

Yukarıdaki komut ; ile ayrıştırılmış sütunlarımızdan 3. olanı ilk satırı (başlık) es geçecek şekilde çekmekte ve bize sadece ilk 5 değeri döndürmekte. Yeni örneğimizde sütunları ayrı ayrı çekip | (pipe) ile birleştirelim.

awk 'BEGIN{FS=";"; OFS="|"} {print $1,$4,$6}' $table

Komuttaki FS kullanımdaki işareti OFS ise yeni işareti ifade eder. Ön tanımlı olarak OFS boşluk değerini tanımlar. Komutu uygulamamızın ardından alacağımız dönüş şu olacaktır. Bu komutu noktaları . virgül , olarak değiştirmek için de kullanabilirsiniz.

OrderDate|Item|UnitCost
1.6.18|Pencil|1.99
1.23.18|Binder|19.99
2.9.18|Pencil|4.99
2.26.18|Pen|19.99
3.15.18|Pencil|2.99
4.1.18|Binder|4.99
4.18.18|Pencil|1.99
5.5.18|Pencil|4.99
6.6.18|Pencil|5.99
6.13.18|Binder|1.99
7.19.18|Pencil|4.99
7.6.18|Pencil|9.99
7.5.18|Binder|3.99
8.13.18|Pencil|49.99
9.28.18|Pen|12.99
9.25.18|Pencil|4.99

Başlığı hariç tutmak isteyebiliriz.

awk 'BEGIN{FS=";"; OFS="|"} 'NR==1'{next} {print $1,$4,$6}' $table

Şimdi de edindiğimiz sütunlar üzerinde ek işlemler yapalım.

awk  -F';' 'BEGIN{FIELDWIDTHS="3 4 3"} NR==1 {next} {print $1,$2,$3}' $table

Komutu uyguladığımızda şu dönüşü alırız.

1.6 .18; Eas
1.2 3.18 ;Ce
2.9 .18; Cen
2.2 6.18 ;Ce
3.1 5.18 ;We
4.1 .18; Eas
4.1 8.18 ;Ce
5.5 .18; Cen
6.6 .18; Eas
6.1 3.18 ;We
7.1 9.18 ;Ce
7.6 .18; Cen
7.5 .18; Eas
8.1 3.18 ;We
9.2 8.18 ;Ea
9.2 5.18 ;Ce

Komut ; ile ayrıştırılmış alanları alıp ($1, $2, $3) bu alanlar içerisinden $1'in 3, $2'nin 4 ve $3'ün 3 karakterini bize verir. Bu işlemi gerçekleştiren FIELDWIDTHS tanımıdır. İşlem yaptığımız test.txt dosyasının bir nedenden dolayı bloklar halinde işlenmiş olduğunu varsayalım.

OrderDate;Region;Rep;Item;Units;UnitCost;Total
1.6.18;East;Jones;Pencil;95;1.99;189.05

OrderDate;Region;Rep;Item;Units;UnitCost;Total
1.23.18;Central;Kivell;Binder;50;19.99;999.50

OrderDate;Region;Rep;Item;Units;UnitCost;Total
2.9.18;Central;Jardine;Pencil;36;4.99;179.64

Bu durumdaki içerikten başlıklar dışındaki alanları seçmek ve başka bir dosyaya kayıt etmek istiyorum.

awk 'BEGIN{FS="\n"; RS=""} {print $2,$3}' $table > yeni.txt

Evet, komutu uyguladığımızda yeni satırlar değerlendirilmeye alınır FS="\n", ardından $2 alan (değerler) ve $3 ile boşluğun kendisi getirilir. Boşluğu değer olarak almak istemezseniz (başka bir tanım da olabilir) sadece değer satırını belirtmeniz yeterli olacaktır.

awk 'BEGIN{FS="\\n"; RS=""} {print $2}' $table > yeni.txt

Çok basit bir değişiklik yapalım ve sonucu ne olacak görelim.

awk 'BEGIN{FS="\\n"; RS=";"} {print $1}' $table

Komutu uygulamamızın ardından tüm alanları (satır ve sütunları) yeni satır olarak olarak alırız.

OrderDate
Region
Rep
Item
Units
UnitCost
Total
East
Jones
Pencil
95
1.99
189.05
Region
Rep
Item
Units
UnitCost
Total
Central
Kivell
Binder
50
19.99
999.50
Region
Rep
Item
Units
UnitCost
Total
Central
Jardine
Pencil
36
4.99
179.64

awk komutunu şartlı ifadelerle zenginleştirebiliriz. Tekrar ilk veri tablomuza dönelim ve aşağıdaki komutu uygulayalım.

awk -F';' 'NR==1{next}{if ($5 > 50) print $3}' $table

Edindiğimiz bilgiler çerçevesinde komutu incelediğimizde şu sonuca varıyoruz; ilk satırdan sonraki satırları değerlendirmeye al, 5. sütuna bak, 50'den büyükse ilgili satırın 3. sütun değerini dön.

Jones
Sorvino
Jones
Andrews
Jardine
Jardine
Kivell
Jones
Gill
Andrews

$3 yerine $n ifadesi kullansak sonuç ne olurdu?

1.6.18;East;Jones;Pencil;95;1.99;189.05
3.15.18;West;Sorvino;Pencil;56;2.99;167.44
4.1.18;East;Jones;Binder;60;4.99;299.40
4.18.18;Central;Andrews;Pencil;75;1.99;149.25
5.5.18;Central;Jardine;Pencil;90;4.99;449.10
6.6.18;East;Jardine;Pencil;95;5.99;189.05
7.19.18;Central;Kivell;Pencil;96;4.99;179.64
7.5.18;East;Jones;Binder;56;3.99;167.44
8.13.18;West;Gill;Pencil;60;49.99;299.40
9.28.18;East;Andrews;Pen;75;12.99;149.25

if...else... örneğimizi biraz daha genişletelim.

awk -F';' ' NR > 1 {if ($5>30){ x=$1; print x} else { x=$5/2; print x }}' $table

Komutu uyguladığımızda $5 değerinin 30 üzerinde olması durumunda $1 sütun değeri, aksi durumda $5 sütun değerinin yarısı yazdırılır. Şimdi de sütun sayısını ve belirli bir sütun değerlerinin toplamını alalım.

awk -F';' ' NR > 1 { sum += $5 }END{ print NR-1, sum }' $table

Komutun dönüşü şu şekilde olacaktır;

16 968

Sadece Binder değeri içeren satırları alalım.

awk -F';' '$4=="Binder"' $table

Şimdi son örneklerimize istinaden şöyle bir işlem gerçekleştirelim.

awk -F';' ' NR > 1 {sum=0;i=1;while(i<5){sum += $i;i++} average=sum/3; print "Average of",sum,":",average;}' $table

Komut satır sayısı kadar (başlık hariç) $i değerini döngü (while) içerisinde artırır ve sum değerini 3 ile bölerek average değişkenine atar. Ardından Average of ... şeklinde döker.

Average of 1.6 : 0.533333
Average of 1.23 : 0.41
Average of 2.9 : 0.966667
Average of 2.26 : 0.753333
Average of 3.15 : 1.05
Average of 4.1 : 1.36667
Average of 4.18 : 1.39333
Average of 5.5 : 1.83333
Average of 6.6 : 2.2
Average of 6.13 : 2.04333
Average of 7.19 : 2.39667
Average of 7.6 : 2.53333
Average of 7.5 : 2.5
Average of 8.13 : 2.71
Average of 9.28 : 3.09333
Average of 9.25 : 3.08333

while döngüsü yerine for da kullanabiliriz.

awk -F';' ' NR > 1 {sum=0;for(i=1;i<5;i++){sum += $i;i++} average=sum/3; print "Average of",sum,":",average;}' $table

Sonuç yine aynı oldu, değil mi? Şartlı ifadeler ve döngülerin ardından son olarak pivot işlemlerine değinmek istiyorum. İlk pivot örneğimiz yine başlığı hariç tuttuğumuz bir şekilde sütun $4 gruplandırılması üzerine olsun ve bunun karşılığı olarak da sütun $5'i alalım.

awk -F';' 'NR>1 {pivot[$4]+=$5} END{for (x in pivot) print x";"pivot[x]}' $table

Komutumuzun uygulanmasının ardından şu dönüşü alırız.

102;Pen
650;Pencil
216;Binder

Bu işlemi datamash ile gerçekleştirecek olsak komutumuz şöyle olurdu:

cat $table | datamash -st ';' --header-in groupby 4 sum 5
datamash -st 'st-in groupby 4 sum 5 < $table | datamash -;' --header ';' reverse

Son olarak, örneğin Binder için toplamın doğruluğunu kontrol etmek istediğimizi varsayalım;

awk -F';' ' NR > 1 {if ($4=="Binder"){ sum += $5;}}END {print sum}' $table

If...Else... kullanımında bir kısa yol mevcut; ~ /[deger]/ ile belirlenen şartın sağlanması durumunda süslü parantez {} içerisinde belirtilen işlem uygulanır.

awk -F ';' '$4 ~ /Binder/ {sum += $5} END {print sum}' $table

Sed İşlemleri

Awk örneklerinin başında başlık alanı olarak ifade edebileceğimiz ilk satırı nasıl göz ardı edebileceğimizden bahsetmiştim. Aynı işlemi sed ile tekrarlayalım;

sed 1,1d $table
sed -n '1!p' $table

Evet, komutu uyguladığımızda OrderDate;Region;Rep;Item;Units;UnitCost;Total içeriği es geçilecektir. Bu işlemin yanı sıra ayrıştırıcı olarak kullandığımız noktalı virgülü ; pipe | ile değiştirelim.

sed -i 's/;/|/g' $table

Komutu uyguladığımızda $table ile çağırdığımız test.txt dosyasının içeriğinde yer alan noktalı virgüller (;) pipe (|) olarak değiştirilecektirler. Ancak, bir not olarak eklemekte fayda var. Yukarıdaki komutu uyguladığınızda büyük ihtimalle command c expects \ followed by text şeklinde bir hata alacaksınız. Bunun nedeni dosya içerisinde yer alan bir ifadenin komut kabul edilmesi. Bu hatayı şu şekilde önleyebiliriz. -i işlemin çıktı yerine dosya üzerinde gerçekleştirilmesini sağlayacaktır. -e kullanmamızın nedeni ise sed'in seçenek olmayan ilk parametreyi komut dosyası olarak kullanmaya çalışmasıdır1. Yukarıdaki hatanın da nedeni budur.

sed -ie 's/;/|/g' $table

$table içeriğini listeleyelim, ancak Örüntüler kullanarak 9 ile başlayan satırları bu listemenin dışında tutalım.

sed '/^9.\*/d' $table

Komut uygulandıktan sonra istediğimiz çıktı ekranda dönecektir ve dosya içeriğinde bir değişiklik söz konusu olmayacaktır. Ancak, dosyanın da güncellenmesini istersek -i parametresini de komuta dahil edebiliriz.

Son Olarak

Elbette ilgili komut örneklerini daha da çoğaltmak, çeşitlendirmek mümkün. Aklıma geldikçe eklemeler yapmaya devam edeceğim. Bu süreçte değerlendirmek üzere GNU > Datamash Alternatives sayfasını incelemenizi öneririm2. Ayrıca, liste olarak eklediğim bağlantılar aracılığıyla da pek çok alternatif örneğe ulaşabilirsiniz.