How to append to a file without overwriting it
Short answer: OpenOptions::new().append(true).open(path) opens a file so every write lands at the end and the bytes already in it stay where they are. Add .create(true) and a missing file is created instead of being an error. fs::write is the opposite trade: one call, but it empties the file first.
From a fresh project — cargo new append-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on this page depends on it. Each block sets up the file it appends to, so it runs on its own and in any order. Paths are relative to the directory you run from, which under cargo run is the project root.
Append to a file that already exists
Section titled “Append to a file that already exists”use std::fs::{self, OpenOptions};use std::io::{self, Write};
fn main() -> io::Result<()> { fs::write("log.txt", "first line\n")?; let mut file = OpenOptions::new().append(true).open("log.txt")?; file.write_all(b"second line\n")?; file.write_all(b"third line\n")?; let text = fs::read_to_string("log.txt")?; print!("{text}"); println!("{} bytes", fs::metadata("log.txt")?.len()); Ok(())}first linesecond linethird line34 bytesOpenOptions is a builder: start from new(), set the options you want, then open. append(true) is the one that matters here — each write goes to the current end of the file, not to the start. Setting .write(true).append(true) is the same as .append(true) on its own. The writing itself is unchanged: write_all, which the write page also uses, loops until every byte is in.
Create the file if it isn’t there
Section titled “Create the file if it isn’t there”use std::fs::OpenOptions;use std::io::{self, Write};
fn main() -> io::Result<()> { // Start from nothing so the block runs twice. let _ = std::fs::remove_file("notes.txt"); let mut file = OpenOptions::new().append(true).create(true).open("notes.txt")?; file.write_all(b"one\n")?; drop(file);
// The second open finds the file there and appends to it. let mut file = OpenOptions::new().append(true).open("notes.txt")?; file.write_all(b"two\n")?; let text = std::fs::read_to_string("notes.txt")?; print!("{text}"); Ok(())}onetwocreate(true) opens the file if it exists and makes it if it doesn’t. Without it, a path that isn’t there yet is an error:
use std::fs::OpenOptions;
fn main() { let _ = std::fs::remove_file("missing.txt"); match OpenOptions::new().append(true).open("missing.txt") { Ok(_) => println!("opened"), Err(err) => { println!("{err}"); println!("{err:?}"); } }}No such file or directory (os error 2)Os { code: 2, kind: NotFound, message: "No such file or directory" }Which of the two you want depends on the job. A log file should be created on the first write, so create(true) earns its place. A config file that has gone missing is a bug worth failing on, so leave create off and let the error surface.
Every write lands at the end
Section titled “Every write lands at the end”use std::fs::{self, OpenOptions};use std::io::{Seek, SeekFrom, Write};
fn main() -> std::io::Result<()> { fs::write("log.txt", "AAAA\n")?; let mut file = OpenOptions::new().append(true).open("log.txt")?; file.seek(SeekFrom::Start(0))?; file.write_all(b"BBBB\n")?; let text = fs::read_to_string("log.txt")?; print!("{text}"); println!("{} bytes", fs::metadata("log.txt")?.len()); Ok(())}AAAABBBB10 bytesIn append mode the file is positioned at its end before every write, so the seek above changed nothing: BBBB landed after AAAA instead of overwriting it. That positioning is what makes appending safe for shared files. Two handles, or two processes, take turns at the end instead of overwriting each other; a plain write after seek(SeekFrom::End(0)) leaves a race instead, where another writer slips in between the seek and the write and one of the two writes is lost:
use std::fs::OpenOptions;use std::io::{self, Write};
fn main() -> io::Result<()> { let _ = std::fs::remove_file("shared.txt"); let mut a = OpenOptions::new().append(true).create(true).open("shared.txt")?; let mut b = OpenOptions::new().append(true).open("shared.txt")?; a.write_all(b"from a\n")?; b.write_all(b"from b\n")?; a.write_all(b"from a again\n")?; let text = std::fs::read_to_string("shared.txt")?; print!("{text}"); Ok(())}from afrom bfrom a againData from two writers can still interleave inside one large write, so a message that belongs together goes out in a single write_all.
One thing appending does not fix: a crash in the middle of a write leaves the file with half of it. Making a write appear all at once is a different problem, and it gets its own page later in this chapter.
Related
Section titled “Related”- How to write to a file — the truncating counterpart, and when it is the right one
- How to read a file line by line — reading back a log as it grows
- Reading and writing files — the rest of the chapter