Rust'ta bir dosyayı okumanın en kısa yolu std::fs::read_to_string("yol"), yazmanın en kısa yolu ise std::fs::write("yol", içerik) çağrısıdır. İkisi de io::Result döndürür; yani dosya yoksa, izin yoksa ya da içerik geçerli UTF-8 değilse program çökmez, size bir hata değeri verilir ve bunu ? ile yukarı taşıyabilir ya da match ile ele alabilirsiniz. Satır satır okumak gerektiğinde BufReader, çok sayıda küçük yazma yapılacaksa BufWriter, dosya yollarını kurmak için de Path ve PathBuf kullanılır.
Bu yazıda standart kütüphanenin üç modülünü birlikte ele alıyoruz: std::fs dosya sistemi işlemlerini, std::io okuma ve yazma trait'lerini, std::path ise platformdan bağımsız yol işlemeyi sağlar. Örneklerin hepsi yalnızca standart kütüphaneyi kullanır ve Rust 1.99.1 ile derlenip çalıştırılmıştır. Hata türlerini kendi tipinizde toplamak isterseniz özel hata tipi yazısına, Result ve ? temelleri için Result ve hata yönetimi yazısına bakabilirsiniz.
std::fs, std::io ve std::path ne işe yarar?

std::fs yerel dosya sistemini değiştiren işlemleri içerir: dosya açma, oluşturma, silme, yeniden adlandırma, klasör oluşturma, klasör içeriğini listeleme ve meta veri okuma. Belgelere göre bu modüldeki işlemler platformlar arası çalışacak şekilde tasarlanmıştır; Unix veya Windows'a özgü ek yetenekler std::os altındaki uzantı trait'lerinde durur.
std::io modülünün kalbi Read ve Write trait'leridir. File, ağ soketleri, standart girdi ve hatta bellekteki bir Vec<u8> bu trait'leri uygular. Belgeler bu tipleri kısaca okuyucu (reader) ve yazıcı (writer) diye adlandırır. Aynı modülde konumu değiştirmeye yarayan Seek, tamponla satır okumayı sağlayan BufRead ve birçok fonksiyonun dönüş tipi olan io::Result<T> da bulunur.
std::path ise Path ve PathBuf tiplerini verir. Belgelerin benzetmesiyle bunlar str ve String ikilisine benzer: Path ödünç alınan bir yol dilimidir, PathBuf ise sahip olunan ve büyütülebilen yoldur. İkisi de işletim sisteminin yol sözdizimine göre çalışan OsStr ve OsString üzerine ince birer sarmalayıcıdır.
Tek seferde okuma ve yazma
Küçük ve orta boy dosyalar için en pratik yol, içeriği tek çağrıda belleğe almaktır. fs::read_to_string dosyayı açar, tamamını okur ve bir String döndürür; fs::read aynı işi bayt vektörü (Vec<u8>) olarak yapar. fs::write ise dosya yoksa oluşturur, varsa içeriğini tamamen değiştirir. Belgeler bu fonksiyonları, File::open ile read_to_string ya da File::create ile write_all ikililerinin daha az import ve ara değişkenle yazılmış kısayolları olarak tanımlar.
use std::fs;
use std::io;
fn main() -> io::Result<()> {
fs::write("not.txt", "merhaba\nçalışma dosyası\n")?;
let metin = fs::read_to_string("not.txt")?;
print!("{metin}");
let baytlar = fs::read("not.txt")?;
println!("{} karakter, {} bayt", metin.chars().count(), baytlar.len());
Ok(())
}
// çıktı:
// merhaba
// çalışma dosyası
// 24 karakter, 28 baytÇıktıdaki fark Türkçe karakterlerden gelir: ç, ı ve ş UTF-8'de ikişer bayt tuttuğu için karakter sayısı ile bayt sayısı eşit değildir. Bu ayrımın ayrıntısı String ve &str yazısında anlatılıyor. Fonksiyonların yol parametresi AsRef<Path> olduğu için &str, String, &Path veya PathBuf verebilirsiniz; fs::write içeriği de AsRef<[u8]> olarak alır, bu yüzden hem metin hem bayt dizisi kabul eder.
main fonksiyonunun io::Result<()> döndürmesi, içeride ? kullanmanızı sağlar. Bir hata oluşursa main bu hatayı döndürür, program sıfır olmayan bir çıkış koduyla biter ve hata standart hataya yazılır. Bu davranışın komut satırı araçlarındaki kullanımı komut satırı argümanları yazısında ele alınıyor.
Hataları türüne göre ele almak
Dosya işlemlerinde hata kaçınılmazdır: dosya silinmiş olabilir, kullanıcı yanlış yol yazmış olabilir, disk dolmuş olabilir. io::Error değerinin kind() metodu bir io::ErrorKind döndürür ve bununla hatanın sınıfına göre farklı davranabilirsiniz. Aşağıdaki örnekte olmayan bir dosya açılıyor, ardından geçersiz UTF-8 içeren bir dosya metin olarak okunmaya çalışılıyor.
use std::fs::{self, File};
use std::io::ErrorKind;
fn main() {
match File::open("olmayan.txt") {
Ok(_) => println!("açıldı"),
Err(e) if e.kind() == ErrorKind::NotFound => println!("dosya yok"),
Err(e) => println!("başka hata: {e}"),
}
fs::write("bozuk.txt", [0x66, 0xff, 0x6f]).unwrap();
let hata = fs::read_to_string("bozuk.txt").unwrap_err();
println!("{:?}", hata.kind());
println!("{hata}");
}
// çıktı:
// dosya yok
// InvalidData
// stream did not contain valid UTF-80xff baytı hiçbir geçerli UTF-8 dizisinin parçası olamaz; bu yüzden read_to_string InvalidData türünde hata döndürür. Böyle dosyaları okumanız gerekiyorsa fs::read ile baytları alıp kendiniz yorumlayabilirsiniz. İşletim sisteminden gelen hata mesajlarının metni platforma ve sistem diline göre değişir; bu nedenle programın mantığını mesaj metnine değil ErrorKind değerine dayandırın. OpenOptions belgesi ayrıca hangi durumun hangi ErrorKind ile eşlendiğinin fonksiyonun uyumluluk sözleşmesinin parçası olmadığını, bazı dosya sistemi hatalarının da şu an hiçbir türle eşleşmediğini belirtir. Dolayısıyla match ifadelerinizde her zaman genel bir Err(e) kolu bırakın.
File ve OpenOptions ile ayrıntılı kontrol

Tek seferlik fonksiyonlar yetmediğinde File tipini doğrudan kullanırsınız. File::open dosyayı salt okunur açar. File::create salt yazılır açar, dosya yoksa oluşturur, varsa uzunluğunu sıfıra indirir; yani eski içerik silinir. File::create_new ise dosya zaten varsa hata verir ve bunu tek bir atomik işlemle yapar. Daha ince ayar için OpenOptions kurucusu vardır: OpenOptions::new() ile tüm seçenekler false olarak başlar, zincirleme metotlarla istediklerinizi açar ve en sonda open çağırırsınız. File::options() aynı nesneyi import gerektirmeden verir.
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
fn main() -> io::Result<()> {
fs::write("gunluk.log", "ilk satır\n")?;
let mut f = OpenOptions::new().append(true).open("gunluk.log")?;
writeln!(f, "ikinci satır")?;
writeln!(f, "üçüncü satır")?;
print!("{}", fs::read_to_string("gunluk.log")?);
match File::create_new("gunluk.log") {
Ok(_) => println!("yeni dosya oluşturuldu"),
Err(e) => println!("create_new: {:?}", e.kind()),
}
let sonuc = OpenOptions::new().create(true).open("x.txt");
println!("yalnızca create: {:?}", sonuc.unwrap_err().kind());
Ok(())
}
// çıktı:
// ilk satır
// ikinci satır
// üçüncü satır
// create_new: AlreadyExists
// yalnızca create: InvalidInputSon satır sık yapılan bir hatayı gösteriyor: create(true) tek başına yetmez. Belgeye göre dosyanın oluşturulabilmesi için write veya append erişimi de açılmalıdır; aksi halde open InvalidInput döndürür. Benzer şekilde truncate(true) yazma erişimi olmadan çalışmaz. write(true) var olan dosyayı kesmeden baştan üzerine yazar; eski içeriğin sonu dosyada kalabilir. Ekleme modu ise her yazmanın o anki dosya sonuna yapılmasını garanti eder, ama farklı süreçlerin yazdığı verinin birbirine karışmayacağını garanti etmez.
| Seçenek | Etkisi | Dikkat |
|---|---|---|
| read(true) | Okuma erişimi | File::open bunu kullanır |
| write(true) | Yazma erişimi | Var olan içeriği kesmez, üzerine yazar |
| append(true) | Her yazma dosya sonuna gider | write(true) ile birlikte vermek aynı etkiyi yapar |
| truncate(true) | Var olan dosyayı sıfır uzunluğa indirir | Yazma erişimi gerekir |
| create(true) | Yoksa oluşturur, varsa açar | write veya append olmadan InvalidInput |
| create_new(true) | Yalnızca yeni dosya oluşturur | Atomiktir; create ve truncate yok sayılır |
File kapsam dışına çıktığında otomatik kapanır. Ancak belgeler, kapanış sırasında oluşan hataların Drop uygulamasında yok sayıldığını vurgular. Verinin diske ulaştığından emin olmanız ve bu aşamadaki hataları yakalamanız gerekiyorsa sync_all çağırın; bu çağrının dosyayı yalnızca kapatmaktan genelde daha pahalı olduğunu da unutmayın.
BufReader ve BufWriter ile tamponlu okuma ve yazma
File okuma ve yazmaları kendi başına tamponlamaz. Her küçük read veya write çağrısı işletim sistemine ayrı bir çağrı olarak gidebilir. BufReader alttaki okuyucudan büyük parçalar halinde okuyup bellekte tutar; BufWriter ise yazılanları biriktirip seyrek ve büyük parçalar halinde aktarır. Her ikisinin varsayılan tampon boyutu belgelere göre şu an 8 KiB'tır ve ileride değişebilir. Belgeler bu tiplerin küçük ve tekrarlı çağrılarda fayda sağladığını; tek seferde çok büyük okuma veya yazmada, ya da zaten bellekte olan bir kaynakta (örneğin Vec<u8>) bir kazanç getirmediğini açıkça söyler.
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
fn main() -> io::Result<()> {
let dosya = File::create("sayilar.txt")?;
let mut yazici = BufWriter::new(dosya);
for i in 1..=3 {
writeln!(yazici, "satır {i}")?;
}
yazici.flush()?;
let okuyucu = BufReader::new(File::open("sayilar.txt")?);
for (no, satir) in okuyucu.lines().enumerate() {
let satir = satir?;
println!("{}: {satir}", no + 1);
}
Ok(())
}
// çıktı:
// 1: satır 1
// 2: satır 2
// 3: satır 3lines() metodu BufRead trait'inden gelir; bu yüzden use std::io::BufRead satırını unutursanız metot bulunamaz. Döndürdüğü yineleyicinin her elemanı io::Result<String> tipindedir ve satır sonundaki \n ya da \r\n çıkarılmış olur. Yineleyicilerle çalışmanın genel kuralları için iterator yazısına göz atabilirsiniz.
read_line ise farklı davranır: satır sonu karakterini korur, okunanı verdiğiniz String değişkeninin sonuna ekler ve okunan bayt sayısını döndürür. Ok(0) dosya sonuna gelindiğini gösterir. Döngü içinde aynı tamponu kullanıyorsanız her turda clear() çağırmazsanız satırlar birikir. Belgeler ayrıca read_line çağrısının satır sonu veya dosya sonu gelene kadar bloklandığını, güvenilmeyen bir kaynaktan okurken take ile üst sınır koymanın akıllıca olduğunu belirtir.
Path ve PathBuf ile yol oluşturma
Yolları metin birleştirerek kurmak, ayırıcı karakterin / mı \ mı olacağı gibi sorunlar çıkarır. std::path modülü bu işi platforma bırakır. Path::new bir metni maliyetsiz biçimde yol dilimine çevirir; parent, file_name, file_stem ve extension gibi metotlar yolu parçalarına ayırır. Değiştirmeniz gerekiyorsa PathBuf kullanır, push ile parça ekler, set_extension ile uzantıyı değiştirirsiniz.
use std::path::{Path, PathBuf};
fn main() {
let yol = Path::new("veri/raporlar/ekim.csv");
println!("{:?}", yol.parent());
println!("{:?}", yol.file_name());
println!("{:?}", yol.file_stem());
println!("{:?}", yol.extension());
let mut kok = PathBuf::from("veri");
kok.push("yedek");
kok.push("ekim.csv");
kok.set_extension("bak");
println!("{}", kok.display());
let yeni = yol.with_extension("json");
println!("{}", yeni.display());
println!("{}", Path::new("veri").join("/mutlak").display());
}
// çıktı (Windows):
// Some("veri/raporlar")
// Some("ekim.csv")
// Some("ekim")
// Some("csv")
// veri\yedek\ekim.bak
// veri/raporlar/ekim.json
// /mutlakBu çıktı Windows'ta alındı; push parçaları platformun ayırıcısıyla eklediği için beşinci satırda ters bölü görünüyor, Linux'ta aynı satır veri/yedek/ekim.bak olur. Son satır önemli bir kuralı gösterir: push ve join metotlarına mutlak bir yol verirseniz mevcut yolun yerini alır. Kullanıcıdan gelen bir dosya adını bir kök klasöre eklerken bunu hesaba katın.
Yazdırmak için display() kullanılır, çünkü yollar geçerli Unicode olmayabilir ve bu metot gerekirse kayıplı dönüşüm yapar. to_str() yalnızca yol geçerli UTF-8 ise Some döndürür. Belgelere göre components, karşılaştırma ve has_root gibi metotlar tekrarlanan ayırıcıları ve baştaki olmayan . parçalarını yok sayan basit bir normalleştirme yapar, ama .. parçalarını ve sembolik bağlantıları çözmez. Tam çözüm için dosya sistemine erişen fs::canonicalize gerekir. Ayrıca starts_with gibi dosya sistemine dokunmayan metotlar, Windows sürücü harfleri dışında, platformdan bağımsız olarak büyük-küçük harfe duyarlıdır.
Klasör oluşturma, listeleme ve silme
fs::create_dir_all eksik olan tüm üst klasörleri de oluşturur. Belgeler bu fonksiyonun atomik olmadığını, hata verirse o ana kadar oluşturduğu klasörlerin yerinde kaldığını belirtir. fs::read_dir bir klasördeki girdileri io::Result<DirEntry> üreten bir yineleyici olarak verir; . ve .. atlanır. Sıralama platforma ve dosya sistemine bağlıdır, çağrıdan çağrıya bile değişebilir; tekrarlanabilir bir sıra istiyorsanız kendiniz sıralamalısınız.
use std::fs;
use std::io;
use std::path::Path;
fn main() -> io::Result<()> {
let kok = Path::new("deneme_klasor");
fs::create_dir_all(kok.join("alt/derin"))?;
fs::write(kok.join("b.txt"), "B")?;
fs::write(kok.join("a.txt"), "A")?;
let mut girdiler = fs::read_dir(kok)?
.map(|g| g.map(|g| g.file_name()))
.collect::<Result<Vec<_>, io::Error>>()?;
girdiler.sort();
println!("{girdiler:?}");
let meta = fs::metadata(kok.join("a.txt"))?;
println!("dosya mı: {}, boyut: {}", meta.is_file(), meta.len());
fs::remove_dir_all(kok)?;
println!("var mı: {:?}", fs::exists(kok));
Ok(())
}
// çıktı:
// ["a.txt", "alt", "b.txt"]
// dosya mı: true, boyut: 1
// var mı: Ok(false)collect::<Result<Vec<_>, io::Error>>() kalıbı, yineleyicideki ilk hatada durup o hatayı döndüren, hata yoksa tüm sonuçları bir vektöre toplayan kısa bir yoldur. remove_dir_all klasörü içeriğiyle birlikte siler; belgedeki uyarı dikkat çekicidir: dikkatli kullanın. Klasör yoksa hata verir, yani tekrar çağrılması güvenli değildir, sembolik bağlantıları izlemez ve bağlantının kendisini siler.
Varlık kontrolü için iki seçenek vardır. Path::exists bir bool döndürür ve meta veriye erişilemediği her durumda, örneğin izin hatasında, false der; belgeler bu yüzden onu hataya açık bulur. fs::exists ise io::Result<bool> döndürür ve var olup olmadığını doğrulayamadığında hata verir. Ancak ikisi de aşağıda anlatılan yarış durumunu çözmez.
Kontrol ile kullanım arasındaki yarış (TOCTOU)

std::fs modülü belgesi, bir koşulun kontrol edildiği an ile sonucun kullanıldığı an arasında başka bir sürecin durumu değiştirebileceği yarış durumuna özel bir bölüm ayırır. Örneğin önce dosyanın var olmadığını kontrol edip sonra oluşturmak bu yarışa açıktır: iki adım arasında başka bir süreç aynı dosyayı oluşturabilir. Önerilen çözümler şunlardır:
- Mümkün olduğunda atomik işlem kullanın: varlık kontrolü yerine
File::create_newya dacreate_new(true)seçeneği. - Meta veri sorgularının başka süreçlerin değişikliklerinden etkilenebileceğini unutmayın.
- İşiniz bitene kadar dosyayı açık tutun; aynı yolu tekrar tekrar açmak yerine elinizdeki
Filedeğerini kullanın.
Uygulamalı mini örnek: log dosyasından hata özeti
Şimdi parçaları birleştirelim. Aşağıdaki fonksiyon bir log dosyasını satır satır okur, HATA ile başlayan satırları satır numarasıyla ayrı bir dosyaya yazar ve toplam satır ile hata sayısını döndürür. Girdi dosyası yoksa hata ? ile çağırana taşınır ve main bunu türüne göre raporlar.
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::Path;
fn ozetle(girdi: &Path, cikti: &Path) -> io::Result<(usize, usize)> {
let okuyucu = BufReader::new(File::open(girdi)?);
let mut yazici = BufWriter::new(File::create(cikti)?);
let (mut toplam, mut hata) = (0, 0);
for satir in okuyucu.lines() {
let satir = satir?;
toplam += 1;
if satir.starts_with("HATA") {
hata += 1;
writeln!(yazici, "{toplam}: {satir}")?;
}
}
yazici.flush()?;
Ok((toplam, hata))
}
fn main() {
fs::write("sunucu.log", "BİLGİ başladı\nHATA disk dolu\nBİLGİ istek\nHATA zaman aşımı\n").unwrap();
match ozetle(Path::new("sunucu.log"), Path::new("hatalar.txt")) {
Ok((t, h)) => println!("{t} satır, {h} hata"),
Err(e) => eprintln!("okunamadı: {e}"),
}
print!("{}", fs::read_to_string("hatalar.txt").unwrap());
if let Err(e) = ozetle(Path::new("yok.log"), Path::new("x.txt")) {
println!("hata türü: {:?}", e.kind());
}
}
// çıktı:
// 4 satır, 2 hata
// 2: HATA disk dolu
// 4: HATA zaman aşımı
// hata türü: NotFoundFonksiyon parametre olarak &Path alıyor; böylece çağıran taraf ister Path::new ister PathBuf versin, kopya oluşmaz. Okuma tamponlu olduğu için büyük log dosyalarında bile dosyanın tamamı belleğe alınmaz. Bu fonksiyonu test etmek isterseniz geçici dosyalar oluşturan bir birim testi yazabilirsiniz; ayrıntılar Rust test yazma yazısında.
Sık yapılan hatalar
lines()veyaread_linekullanırkenBufReadtrait'ini kapsama almamak.File::createçağrısının var olan dosyayı sıfırladığını unutmak; ekleme yapmak içinappend(true)gerekir.create(true)seçeneğiniwriteveyaappendolmadan vermek veInvalidInputhatası almak.BufWriterdeğeriniflush()çağırmadan düşürmek ve yazma hatalarını kaçırmak.read_dirsonucunun sıralı geleceğini varsaymak.- Önce
exists()ile kontrol edip sonra dosya oluşturmak; bunun yerinecreate_newkullanın. - Kullanıcı girdisini
joinile eklerken mutlak yolun mevcut yolu tamamen değiştirebileceğini gözden kaçırmak. - Hata mesajı metnine göre karar vermek; mesajlar platforma göre değişir,
ErrorKindkullanın.
Hangi yöntemi ne zaman kullanmalı?
| İhtiyaç | Önerilen araç | Not |
|---|---|---|
| Küçük metin dosyasını okumak | fs::read_to_string | Geçersiz UTF-8 ise InvalidData |
| İkili dosyayı okumak | fs::read | Vec<u8> döner |
| Dosyayı baştan yazmak | fs::write | Yoksa oluşturur, varsa değiştirir |
| Büyük dosyayı satır satır işlemek | BufReader ve lines() | Tamamını belleğe almaz |
| Çok sayıda küçük yazma | BufWriter | Sonda flush() çağırın |
| Dosyaya ekleme yapmak | OpenOptions append(true) | Dosyayı kesmez |
| Yalnızca yeni dosya oluşturmak | File::create_new | Atomik, yarışa karşı güvenli |
| Yol kurmak | PathBuf ve join/push | Ayırıcıyı platform belirler |
Sık Sorulan Sorular
read_to_string ile BufReader arasında nasıl seçim yaparım?
Dosya küçükse ve içeriğin tamamına aynı anda ihtiyacınız varsa fs::read_to_string en sade çözümdür. Dosya büyükse, satır satır işleyebiliyorsanız ya da kaynağın ne kadar büyük olacağını bilmiyorsanız BufReader ile okumak bellek kullanımını sınırlı tutar.
Dosya kapatmak için ayrıca bir şey yapmam gerekir mi?
Hayır. File kapsam dışına çıktığında otomatik kapanır. Ancak kapanırken oluşan hatalar yok sayılır; verinin kalıcı olarak yazıldığından emin olmak istiyorsanız sync_all çağırıp sonucunu kontrol edin. Sahiplik ve düşürme mantığı için ownership yazısına bakabilirsiniz.
Windows ve Linux için ayrı yol kodu yazmam gerekir mi?
Genellikle hayır. Path ve PathBuf platformun yol sözdizimine göre çalışır; parçaları join veya push ile eklediğinizde doğru ayırıcı kullanılır. Yolu metin olarak elle birleştirmekten kaçının.
Dosya yoksa oluştur, varsa ekle davranışını nasıl elde ederim?
OpenOptions::new().append(true).create(true).open(yol) zinciri bu işi yapar. append yazma erişimini sağladığı için create da geçerli hale gelir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Rust öğrenme rehberi · Rust nedir? · Result ve hata yönetimi · Özel hata tipi ve Error trait · Komut satırı argümanları · String ve &str farkı