21 G/Ç Kütüphanesi

21 G/Ç Kütüphanesi

G/Ç kütüphanesi dosya manipülasyonu için iki farklı model sunar. Basit model, geçerli bir girdi dosyası ve geçerli bir çıktı dosyası, ve Giriş/Çıkış işlemlerinin bu dosyalar üzerinden yürütüleceğini varsayar. Tam model, malum dosya tanıtıcılarını(handle) kullanır; tüm işlemlerin dosya tanıtıcıları üzerinden metotlar olarak tanımlandığı nesne yönelimli bir stil benimser.

Basit model basit şeyler için uygundur; şimdiye kadar bu kitap boyunca kullanıyoruz. Ancak aynı anda birkaç dosyadan okuma yapmak gibi daha gelişmiş dosya manipülasyonu için yeterli değil. Bu manipülasyonlar için tam modele ihtiyacımız var.

21.1 Basit G/Ç Modeli

Basit model, tüm işlemlerini iki geçerli dosya üzerinden yapar. Kütüphane, standart girdi (stdin) ve standart çıktıyı (stdout) geçerli girdi ve çıktı dosyası olarak ilkler. Bu nedenle io.read gibi bir şey yürüttüğümüzde standart girdiden satır okuyoruz.

io.input ve io.output fonksiyonları ile bu mevcut dosyaları değiştirebiliriz. io.input(dosyaadı) gibi bir çağrı  verilen dosyayı okuma modunda açar ve geçerli girdi dosyası olarak ayarlar. Bu noktadan itibaren tüm girdiler bu dosyadan gelecektir ta ki diğer io.input'u çağırana kadar; io.output çıktı için benzer işi yapar. Hata durumunda her iki fonksiyon da hata yükseltir. Hataları doğrudan işlemek istiyorsanız io.open'ı kullanmanız gerekir, tam modelden.

Yazma okumadan daha basit olduğu için önce ona bakacağız. io.write fonksiyonu keyfi sayıda  string argümanlar alır ve geçerli çıktı dosyasına onları basitçe yazar.  Sayıların stringlere dönüşümü her zaman ki kuralları takip eder; bu dönüşüm üzerinde tam kontrol için string.format fonksiyonunu kullanmalısınız :

> io.write("sin (3) = ", math.sin(3), "\n")
--> sin (3) = 0.14112000805987
> io.write(string.format("sin (3) = %.4f\n", math.sin(3)))
--> sin (3) = 0.1411

io.write(a..b..c) gibi kodlardan kaçının; io.write(a,b,c) çağrısı birleştirmelerden kaçınıldığı için aynı etkiyi daha az kaynakla gerçekleştirir.

Bir kural olarak, hızlı-ve-kirli programlar için ya da hata ayıklama için print, ve çıktınız üzerinde tam kontrole ihtiyacınız olduğunda write kullanın:

> print("hello", "Lua"); print("Hi")
--> hello Lua
--> Hi

> io.write("hello", "Lua"); io.write("Hi", "\n")
--> helloLuaHi

print'in aksine write sekmeler veya yenisatırlar gibi çıktıya hiçbir ekstra karakter eklemez. Ayrıca, write geçerli çıktı dosyasını kullanır oysa print her zaman standart çıktıyı kullanır. Son olarak, print otomatik olarak argümanlarına tostring'i uygular böylece tabloları, fonksiyonları ve nil 'i de aynı zamanda gösterebilir.

io.read fonksiyonu geçerli girdi dosyasından stringleri okur. Argümanları, okunan şeyi kontrol eder:

"*all"         tüm dosyayı oku
"*line"       sıradaki satırı oku
"*number" sayı oku
num           num karakterlerine kadar string oku

io_read("*all") çağrısı, mevcut konumdan başlayarak tüm geçerli girdi dosyasını okur. Dosyanın sonunda ya da dosya boşsa çağrı boş string döndürür.

Lua uzun stringleri verimli bir şekilde işlediğinden, Lua'da filtre yazımın basit tekniği ile tüm dosyayı string içine okunur, string işlenir (tipik olarak gsub ile) ve ardından string çıktıya yazılır:

t = io.read("*all")     -- tüm dosyayı oku
t = string.gsub(t, ...) -- işi yap
io.write(t)             -- dosyaya yaz

Örnek olarak aşağıdaki kod, MIME quoted-printable kodlamayı(genelde mail kodlaması için) kullanarak bir dosyanın içeriğini kodlamak için komple bir programdır. Bu kodlamada, ASCII olmayan karakterler =xx olarak kodlanır, burada xx hex cinsinden karakterin sayısal kodudur. Kodlamanın kıvamını korumak için '=' karakteri de kodlanmalıdır:

t = io.read("*all")
t =string.gsub(t, "([\128-\255=])", function(c)
        return string.format("=%02X", string.byte(c))
    end)
io.write(t)

gsub'da kullanılan şablon, 128'den 255'e kodlu tüm karakterleri artı eşittir işaretini yakalar. 

io.read("*line") çağrısı, yenisatır karakteri olmadan geçerli girdi dosyasından sıradaki satırı döndürür. Dosyanın sonuna ulaştığımızda çağrı nil döndürür (döndürülecek bir sonraki satır yok olarak). Bu şablon read için varsayılandır. Genellikle, bu şablonu yalnızca algoritma satır satır dosyayı doğal olarak işlediğinde kullanıyorum; aksi halde, tüm dosyayı bir kerede *all ile veya daha sonra göreceğimiz gibi bloklarla okumayı tercih ederim.

Bu şablonun kullanımının basit bir örneği olarak, aşağıdaki program mevcut girdisini, her satırı numaralandırarak mevcut çıktıya kopyalar:

for count = 1, math.huge do
    local line = io.read()
    if line == nil then
        break
    end
    io.write(string.format("%6d ", count), line, "\n")
end

Tabi, tüm dosya üzerinde satır satır iterasyon için  io.lines iteratörünü kullanmak daha iyi. Örneğin, bir dosyanın satırlarını sıralamak için aşağıdaki gibi eksiksiz bir program yazabiliriz:

local lines = {}
-- 'lines’ tablosundaki satırları oku
for line in io.lines() do lines[#lines + 1] = line end
-- sırala
table.sort(lines)
-- tüm satırları yaz
for _, l in ipairs(lines) do io.write(l, "\n") end

io_read("*number") çağrısı geçerli girdi dosyasından sayı okur. read'in string yerine sayı döndürdüğü tek durum budur. Bir programın bir dosyadan çok sayı okuması gerektiğinde ara stringlerin olmaması performansını artırır. *number seçeneği numaradan önceki tüm boşlukları atlar ve -3, +5.2, 1000 ve -3.4 e-23 gibi sayı biçimlerini kabul eder. Geçerli dosya konumunda (kötü biçim veya dosya sonu nedeniyle) bir sayı bulamazsa nil döndürür.

Birden çok seçenekle read'i çağırabilirsiniz; her argüman için, fonksiyon karşılık gelen sonucu döndürecek. her satırda üç sayı içeren bir dosyanız olduğunu varsayalım:

6.0 -3.23 15e12
4.3 234   1000001
...

Şimdi her satırdaki en büyük değeri yazdırmak istiyorsunuz. read 'e tek bir çağrı ile üç numaranın hepsini okuyabilirsiniz:

while true do
    local n1, n2, n3 = io.read("*number", "*number", "*number")
    if not n1 then break end
    print(math.max(n1, n2, n3))
end

Herhangi durumda, "*all" seçeneği ile tüm dosyayı okumanın alternatifini düşünmelisiniz ve daha sonra onu parçalara ayırmak için gmatch kullanırsınız:

local pat = "(%S+)%s+(%S+)%s+(%S+)%s+"
for n1, n2, n3 in string.gmatch(io.read("*all"), pat) do
    print(math.max(tonumber(n1), tonumber(n2), tonumber(n3)))
end

Temel okuma şablonlarının yanı sıra, argüman olarak bir n sayısı ile read'i çağırabilirsiniz: bu durumda, read girdi dosyasından n karakter kadar okuma yapmaya çalışır. Herhangi bir karakter (dosyanın sonu) okuyamıyorsa, read nil döndürür; aksi takdirde, en fazla n sayıda karakterli stringi döndürür. Bu okuma şablonun bir örneği olarak aşağıdaki program, stdin'den stdout'a bir dosyayı kopyalamak için etkili bir yöntemidir (Lua'da elbette).:

while true do
    local block = io.read(2 ^ 13) -- tampon büyüklüğü 8K
    if not block then break end
    io.write(block)
end

Özel bir durum olarak io.read(0), dosya sonu için bir test olarak çalışır: okunacak daha fazlası varsa boş string döndürür aksi takdirde nil.

21.2 Tam G/Ç Modeli

I/O üzerinde daha fazla kontrol için tam modeli kullanabilirsiniz. Bu modeldeki merkezi bir kavram, C'deki akışlara(FILE*) eşdeğer olan dosya handle'idir: mevcut konumla açık bir dosyayı temsil eder.

Bir dosyayı açmak için C'deki fopen fonksiyonunu taklit eden io.open fonksiyonunu kullanırsınız.  argüman olarak açılacak dosya adı artı bir açma modu string'i alır. Bu mod stringi, okuma için 'r', yazma için 'w' (dosyanın önceki içeriğini de siler) veya ekleme için ‘a’ artı isteğe bağlı binary dosyaları açmak için  'b' içerebilir. open fonksiyonu dosya için yeni bir tanıtıcı(handle) döndürür. Hata durumunda open nil döndürür artı bir hata iletisi ve bir hata numarası:

print(io.open("olmayan-bir-dosya", "r"))
--> nil olmayan-bir-dosya: boyle dosya veya dizin yok 2

print(io.open("/etc/passwd", "w"))
--> nil /etc/passwd: izin rededildi 13

Hata numaralarının yorumlanması sisteme bağlı. Hataları kontrol etmek için geleneksel bir deyim:

local f = assert(io.open(filename, mode))

open başarısız olursa hata iletisi, sonra gösterilecek mesaj olan assert için ikinci argüman olarak gider.

Bir dosyayı açtıktan sonra read/write metotları ile onu okuyabilir veya yazabilirsiniz. read/write fonksiyonlarına benzerler ancak dosya handleyi üzerinden : operatörünü kullanarak metotlar olarak onları çağırsınız. Örneğin bir dosyayı açmak ve tümünü okumak için şöyle bir öbek kullanabilirsiniz:

local f = assert(io.open(filename, "r"))
local t = f:read("*all")
f:close()

I/O kütüphanesi, üç öntanımlı C akışı için handleler sunar: io.stdin, io.stdout ve io.stderr.
Velhasıl devamdaki bir kodla hata akışına(error stream- stderr) doğrudan bir mesaj gönderebilirsiniz:

io.stderr:write(mesaj)

Tam model ile basit modeli mix edebiliriz. Argümansız io.input() çağrısıyla geçerli girdi dosyası handleyini elde ederiz. Bu handleyi io.input(handle) çağrısı ile set ederiz. (Benzer çağrılar io.output içinde geçerli.) Örneğin geçerli girdi dosyasını geçici olarak değiştirmek isterseniz şöyle bir şey yazabilirsiniz:

local temp = io.input() -- geçerli dosyayı kaydet
io.input("newinput")    -- yeni geçerli dosya aç
<yeni girdiyle biseyler yap>
io.input():close()      -- geçerli dosyayı kapat
io.input(temp)          -- önceki geçerli dosyaya dön

Küçük bir performans ipucu

Genelde, Lua'da bir dosyayı bir bütün olarak okumak  satır satır okumaktan daha hızlıdır. Bununla birlikte bazen hepsini bir kerede okumanın makul olmayacak büyük dosyalarla (örneğin onlarca veya yüzlerce megabaytlık) yüzleşmeliyiz. Böyle büyük dosyaları maksimum performansla işlemek istiyorsanız en hızlı yol onları makul  büyüklüklü parçalarda (örneğin her biri 8 Kilobayt) okumaktır. Ortada satırları kırma sorununu önlemek için basitçe okunacak öbeği artı satırı sorarsınız:

local lines, rest = f:read(BUFSIZE, "*line")

rest değişkeni, öbek tarafından kırılan satırın geri kalanını alacaktır. Daha sonra öbek ve satırın kalanını birleştiriyoruz. Bu yöntemle ortaya çıkan öbek her zaman satır sınırlarında kırılacak.

Liste 21.1'deki wc örneği bu tekniği kullanır, program bir dosyadaki satıların, kelimelerin ve karakterlerin sayısını sayar.

Liste 21.1. wc programı:

local BUFSIZE = 2 ^ 13      -- 8K
local f = io.input(arg[1])  -- girdi dosyayı aç
local cc, lc, wc = 0, 0, 0 -- karakter, satır, ve kelime sayaçları
while true do
    local lines, rest = f:read(BUFSIZE, "*line")
    if not lines then break end
    if rest then lines = lines .. rest .. "\n" end
    cc = cc + #lines
    -- öbekteki kelimeleri say
    local _, t = string.gsub(lines, "%S+", "")
    wc = wc + t
    -- öbekteki yenisatırları(\n) say
    _, t = string.gsub(lines, "\n", "\n")
    lc = lc + t
end
print(lc, wc, cc)

Binary Dosyalar

Basit model fonksiyonları io.input ve io.output bir dosyayı daima metin modunda açar (varsayılan). Unix'te binary dosyalar ve metin dosyaları arasında fark yoktur. Ancak bazı sistemlerde, özellikle Windows'da, binary dosyalar özel bir flagla açılmış olmalıdır. Bu tür binary dosyaları işlemek için mod stringinde 'b' harfiyle io.open kullanmanız gerekir.

Lua'da binary veri metine benzer şekilde işlenir. Lua'da bir string herhangi bir bayt içerebilir ve kütüphanelerdeki hemen hemen tüm fonksiyonlar keyfi baytları işleyebilir. şablon sıfır baytını içermediği sürece binary veriler üzerinde şablon eşleştirme bile yapabilirsiniz. Eğer denekde bu baytı eşleştirmek istiyorsanız %z yerine sınıf kullanın.

Tipik olarak, binary verileri *all şablonuyla tümüyle okursunuz,  veya n şablonuyla n bayt kadar okursunuz. Basit bir örnek olarak aşağıdaki program bir metin dosyasını DOS formatından Unıx formatına dönüştürür (yani bu dönüşüm satırbaşı–yenisatır'ı yenisatıra çevirir). bu dosyalar metin modunda açık olduğundan standart G/Ç dosyalarını (stdin–stdout) kullanmaz. Bunun yerine, programa argümanlar olarak girdi dosyasının ve çıktı dosyasının adlarının verildiğini varsayar:

local inp = assert(io.open(arg[1], "rb"))
local out = assert(io.open(arg[2], "wb"))

local data = inp:read("*all")
data = string.gsub(data, "\r\n", "\n")
out:write(data)

assert(out:close())

Bu programı aşağıdaki komut satırıyla çağırabilirsiniz:

> lua prog.lua file.dos file.unix

Başka bir örnek olarak aşağıdaki program bir binary dosyada bulunan tüm stringleri yazdırır:

local f = assert(io.open(arg[1], "rb"))
local data = f:read("*all")
local validchars = "[%w%p%s]"
local pattern = string.rep(validchars, 6) .. "+%z"
for w in string.gmatch(data, pattern) do
    print(w)
end

Program, string'in sıfırla bittiğini , validchars şablonuyla kabul edilen 6 veya daha fazla karakter serisi olduğunu varsayar. Örneğimizde bu şablon alfanümerik, noktalama işaretleri ve boşluk karakterleri içerir. string.rep ve birleştirmeyi şablon oluşturmak için kullanıyoruz ki altı veya daha fazla tüm seriler validchars yakalanacak. şablonun sonundaki %z, stringin sonundaki sıfır baytla eşleşir.

Son bir örnek olarak, aşağıdaki program binary dosya dökümü(dump) yapar:

local f = assert(io.open(arg[1], "rb"))
local block = 16
while true do
    local bytes = f:read(block)
    if not bytes then break end
    for _, b in pairs {string.byte(bytes, 1, -1)} do
        io.write(string.format("%02X ", b))
    end
    io.write(string.rep(" ", block - string.len(bytes)))
    io.write(" ", string.gsub(bytes, "%c", "."), "\n")
end

Yine ilk program argümanı, girdi dosya adıdır; çıktı standart çıktıya gider. Program dosyayı 16 baytlık öbeklerle okur. Her bir öbek için her baytın hex karşılığını yazar ve daha sonra öbeği metin olarak yazar ve kontrol karakterlerini noktalara çevirir. ( {string.byte(bytes,1,-1)} deyiminin kullanımına dikkat, bytes stringinin tüm baytlarıyla bir tablo oluşturmakta)

Liste 21.2 programın kendi kendinin üzerinde uygulanmasının sonucunu gösterir(bir unix makinede).

Liste 21.2. dump programı dökümü:

6C 6F 63 61 6C 20 66 20 3D 20 61 73 73 65 72 74 local f = assert
28 69 6F 2E 6F 70 65 6E 28 61 72 67 5B 31 5D 2C (io.open(arg[1],
20 22 72 62 22 29 29 0A 6C 6F 63 61 6C 20 62 6C "rb")).local bl
6F 63 6B 20 3D 20 31 36 0A 77 68 69 6C 65 20 74 ock = 16.while t
72 75 65 20 64 6F 0A 20 20 6C 6F 63 61 6C 20 62 rue do. local b
...
6E 67 2E 67 73 75 62 28 62 79 74 65 73 2C 20 22 ng.gsub(bytes, "
25 63 22 2C 20 22 2E 22 29 2C 20 22 5C 6E 22 29 %c", "."), "\n")
0A 65 6E 64 0A .end.


21.3 Dosyalarda Diğer İşlemler

tmpfile fonksiyonu,  geçici dosya için bir handle döndürür, okuma/yazma modunda açar.
Programınız sona erdiğinde bu dosya otomatik olarak kaldırılır (silinir). flush fonksiyonu bir dosyaya tüm bekleyen yazımları işletir. write fonksiyonu gibi bir fonksiyon olarak onu çağırabilirsiniz, io.flush(), geçerli çıktı dosyasına boşaltma yapar; veya bir metot olarak f:flush(), belli f dosyasına boşaltma için.

seek fonksiyonu bir dosyanın hem geçerli konumunu elde edebilir hem de ayarlayabilir. Genel formu f:seek(whence,offset) ' dir. whence parametresi, offsetin nasıl yorumlanacağını belirten bir stringdir.
Geçerli değerleri, "set", dosyanın başından ;"cur", dosyanın mevcut konumundan; ve "end" dosyanın sonundan bazlı offsetin yorumlanmasını sağlar. whence'nin değerinden bağımsız olarak çağrı, dosyanın başından bazlı bayt cinsinden dosyanın son geçerli konumunu döndürür.

whence için varsayılan değer "cur" ve offset için sıfırdır. Bu nedenle,  file:seek() çağrısı değiştirmeden geçerli dosya konumunu döndürür; file:seek("set") çağrısı dosyanın başına konumu getirir(ve sıfır döndürür); ve file:seek("end") çağrısı konumu dosyanın sonuna ayarlar ve dosyanın boyunu döndürür. Aşağıdaki fonksiyon, geçerli konumu değiştirmeden dosya boyunu elde eder:

function fsize(file)
    local current = file:seek()     -- mevcut konumu al
    local size = file:seek("end")   -- dosya boyunu al
    file:seek("set", current)       -- konumu eski yere getir
    return size
end

Tüm bu fonksiyonlar hata durumunda nil artı bir hata iletisi döndürür.

Hiç yorum yok:

Yorum Gönder