Working with Files and the OS

Rust and the OS filesystem

Most of the standard functions used to deal with the filesystem are collected in the standard std::fs.

This module deals with an abstraction of the filesystem that works for any operating system (OS) supported by Rust. Some OS-specific code can be found in the std::os module.

Finally, some streams or path related functions can be found in std::io and std::path. In this lesson, we’ll see the most common types and functions.

The basics of working with files

The most basic operation we’ll do is to create and then read a file.

  • We create a file with Path:new().

  • We write some contents on the specified path with fs::write(path, contents).

use std::fs;
use std::path::Path;
fn main() -> Result<(), std::io::Error> {
let path = Path::new("output/hello.txt");
let contents = "Hello, world!".to_string();
fs::write(path, contents)?;
Ok(())
}

We create our file in the /output directory. The Educative platform lets us download and examine the file once we run the above code.

Notice the return of main, a Result wrapping std::io::Error; this way, we can use the ? notation for file operations that could get an error coming from the underlying OS or the filesystem.

Another way to do the same thing is to use std::io::Result<()> directly.

If we wanted to read out the content of a file, we could write the following code:

use std::fs::File;
use std::io::{Write, BufReader, BufRead, Error};
fn main() -> Result<(), Error> {
let path = "output/lines.txt";
let mut output = File::create(path)?;
write!(output, "Rust\nis\nfun")?;
let input = File::open(path)?;
let buffered = BufReader::new(input);
for line in buffered.lines() {
println!("{}", line?);
}
Ok(())
}

If we run the code above, we get a file and a console output. Let’s use the arrows in the output to check everything out!

This time, we’ll use a different method to write our file, the std::fs::File struct.

We create a new file with File::create and write to it through the write! macro. Pay attention to the \n on the string, which represents a new line.

Next, we open the file with File::open(), and assign its content to be read with std::io::BufReader.

This buffered reader implements an iterator over the lines() of text, which we use through for and then display with println!.

Path::new() vs. File::create()

In the two examples above, we’ve used two very distinct sets of instructions in order to achieve the same result of writing to a file.

Therefore, we might ask, “What are Path::new() and File::create() doing differently?”

  • The File::create() function creates a file on the filesystem. The std::write! macro then writes the content on a buffer, which in this case is the file just created.

  • The Path::new() function does not create anything on the filesystem. Instead, Path is used to handle the path to a resource on the filesystem independently from the underlying OS, whether it’s Windows, Linux, macOS, or any other. A new Path is actually a new variable that holds a correct path, but it does not imply the path exists on the filesystem. The fs::write function creates a file for content if it does not yet exist.

Check out the following code:

#[allow(unused_imports)]
#[allow(unused_variables)]
use std::fs;
use std::path::Path;
#[allow(unused_variables)]
fn main() -> Result<(), std::io::Error> {
let path = Path::new("output/hello.txt");
let contents = "Hello, world!".to_string();
//fs::write(path, contents)?;
println!("Does the file exist on the filesystem? {}", path.exists());
Ok(())
}

Here, Path is more like a str, but it guarantees that the filesystem path it holds has the correct form. We can use exists() to check whether the path actually corresponds to something on the filesystem.

Try to remove comments from the line above and see the difference.

Working with files and directories

We can use std::fs::create_dir to create a new directory:

use std::fs;
fn main() -> std::io::Result<()> {
fs::create_dir("output/dir")?;
Ok(())
}

Note that we’re using the short form std::io::Result<()> here.

If one of the parents of the directory we want to create is missing, than create_dir will give us an error.

We can, however, use create_dir_all:

use std::fs;
fn main() -> std::io::Result<()> {
fs::create_dir("output/missing/dir")?;
//fs::create_dir_all("output/missing/dir")?;
Ok(())
}

With create_dir() we get the Error: Os { code: 2, kind: NotFound, message: "No such file or directory" } error.

Let’s try commenting create_dir() and removing comments from create_dir_all() to see the difference.

Similar to create_dir() and create_dir_all(), we have remove_dir() and remove_dir_all(), which—you guessed it—remove a directory.

use std::fs;
use std::path::Path;
fn main() -> std::io::Result<()> {
let dir = Path::new("output/dir");
fs::create_dir(dir)?;
println!("Does the dir exist on the filesystem? \t\t{}", dir.exists());
fs::remove_dir(dir)?;
println!("Does the dir exist on the filesystem, now? \t{}", dir.exists());
Ok(())
}

If we want to rename a file or a directory, rename() is our go-to:

use std::fs;
fn main() -> std::io::Result<()> {
fs::File::create("output/a.txt");
fs::rename("output/a.txt", "output/b.txt")?;
Ok(())
}

If we run the code above, Educative will prompt us to download b.txt rather than the original a.txt.

The last thing we might want to do is to replicate the functionalities of ls (or dir in the Windows OS).

To do that, we use read_dir to iterate over the resources of the given directory, allowing us to make all sorts of operations with these resources.

main.rs
test/c.txt
test/b.txt
test/a.txt
use std::{fs, io};
fn main() -> io::Result<()> {
let mut entries = fs::read_dir("./test")?
.map(|resource| resource.map(|res| res.path()))
.collect::<Result<Vec<_>, io::Error>>()?;
entries.sort(); // if you want to have them sorted
for f in entries {
println!("{:?}", f.file_name().unwrap())
}
Ok(())
}

Spawning processes

In order to execute any command on a shell, we can spawn a new process with std::process::Command.

Check out the code below, which only applies to Unix:

main.rs
test/c.txt
test/b.txt
test/a.txt
use std::process::Command;
fn main() -> std::io::Result<()> {
let process = Command::new("ls")
.current_dir("./test")
.spawn()
.expect("failed to execute process");
let _ = process.wait_with_output()?;
Ok(())
}

Spawning a process needs a whole chapter to itself in an advanced course. For now, all we need to know is that it’s possible. To learn more, check out Rust’s official documents.