A file copier is a useful small program. It opens one file for reading, opens another file for writing, then copies bytes from the first to the second.
A file copier is a useful small program. It opens one file for reading, opens another file for writing, then copies bytes from the first to the second.
The command will look like this:
copy source.txt output.txtHere is a complete first version.
const std = @import("std");
pub fn main() !void {
var args = std.process.args();
_ = args.next();
const src_path = args.next() orelse {
std.debug.print("missing source file\n", .{});
return;
};
const dst_path = args.next() orelse {
std.debug.print("missing destination file\n", .{});
return;
};
const cwd = std.fs.cwd();
var src = try cwd.openFile(src_path, .{});
defer src.close();
var dst = try cwd.createFile(dst_path, .{});
defer dst.close();
var buffer: [4096]u8 = undefined;
while (true) {
const n = try src.read(&buffer);
if (n == 0) break;
try dst.writeAll(buffer[0..n]);
}
}Run it:
zig run main.zig -- source.txt output.txtThe program starts by reading arguments. The first argument is the executable path and is ignored.
_ = args.next();The next two arguments are file names.
const src_path = args.next() orelse {
std.debug.print("missing source file\n", .{});
return;
};
const dst_path = args.next() orelse {
std.debug.print("missing destination file\n", .{});
return;
};Each call to args.next() returns an optional byte slice. If the argument is missing, the program prints an error and returns.
The current working directory is obtained with:
const cwd = std.fs.cwd();This gives a directory handle. File operations are done relative to it.
The source file is opened for reading:
var src = try cwd.openFile(src_path, .{});
defer src.close();The destination file is created for writing:
var dst = try cwd.createFile(dst_path, .{});
defer dst.close();defer runs when the current scope exits. It is a good fit for closing files. The file is closed whether the copy succeeds or an error occurs.
The buffer is an array of 4096 bytes.
var buffer: [4096]u8 = undefined;undefined means the bytes are not initialized. That is fine here because the program fills the buffer before reading from it.
The copy loop reads bytes into the buffer.
const n = try src.read(&buffer);read returns the number of bytes actually read. At the end of the file it returns zero.
if (n == 0) break;Only the part of the buffer that contains data is written.
try dst.writeAll(buffer[0..n]);The expression buffer[0..n] is a slice. It refers to the first n bytes of the array.
writeAll keeps writing until the whole slice has been written or an error occurs.
A file copier shows several common Zig ideas at once. Resources are explicit. Errors are returned. Cleanup is local. The buffer is ordinary memory, not a hidden object.
A shorter version can use the standard library helper.
const std = @import("std");
pub fn main() !void {
var args = std.process.args();
_ = args.next();
const src_path = args.next() orelse return error.MissingSource;
const dst_path = args.next() orelse return error.MissingDestination;
const cwd = std.fs.cwd();
var src = try cwd.openFile(src_path, .{});
defer src.close();
var dst = try cwd.createFile(dst_path, .{});
defer dst.close();
try src.copyRangeAll(0, dst, 0, try src.getEndPos());
}This version copies the range from offset zero to the end of the file. It is compact, but the explicit loop is worth understanding first. Most I/O programs are variations of that loop.
Exercise 20-6. Change the buffer size to 16 KiB and verify that the program still works.
Exercise 20-7. Refuse to overwrite the destination file if it already exists.
Exercise 20-8. Print the number of bytes copied.
Exercise 20-9. Copy from standard input when the source path is -.
Exercise 20-10. Copy to standard output when the destination path is -.