I want to remove the _ from the name of png images in a folder:
use std::error::Error;
use std::fs;
use std::path::Path;
fn main() -> Result<(), Box<dyn Error>> {
let dir = Path::new("/home/alex/Desktop");
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().unwrap_or_default() == "png" {
if let Some(Some(new_path)) = path
.file_name()
.map(|name| name.to_str().map(|s| s.replace("_", "")))
{
fs::rename(path, new_path)?;
}
}
}
Ok(())
}
The code is "deleting" the png files instead of renaming them. I suspect it's because they are being renamed to a location that doesn't exist. But I'm not quite sure how to modify the code to fix that.
Live code: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=323f901e4f0f4c99dbf5affeb0127991
Your current code just uses the file name as new path, what you want to do is use with_path_name to replace the filename of path with your new file name:
use std::error::Error;
use std::fs;
fn main() -> Result<(), Box<dyn Error>> {
for entry in fs::read_dir("/home/alex/Desktop")? {
let path = entry?.path();
if path.is_file() && path.extension().unwrap_or_default() == "png" {
if let Some(new_name) = path
.file_name()
.and_then(|name| name.to_str())
.map(|s| s.replace("_", ""))
{
let new_path = path.with_file_name(new_name);
fs::rename(path, new_path)?;
}
}
}
Ok(())
}
Related
This code deletes PNG files in a folder and prints them. I wanted to simplify it a bit by replacing all the unwrap()'s with ?.
use std::fs;
use std::path::Path;
fn main() -> Result<(), std::io::Error> {
let path = Path::new("/home/alex/Desktop");
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().unwrap() == "png" {
fs::remove_file(&path)?;
println!("{}", path.file_name().unwrap().to_str().unwrap());
}
}
Ok(())
}
I found out I can't replace the unwrap()'s that are handling Option instead of Result. In this case, extension(), file_name(), and to_str().
I changed the code to solve that problem. However, the code just became more complicated:
use std::fs;
use std::path::Path;
fn main() -> Result<(), std::io::Error> {
let path = Path::new("/home/alex/Desktop");
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let ext = path.extension().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Invalid file extension"))?;
if ext == "png" {
fs::remove_file(&path)?;
println!("{}", path.file_name().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Invalid file name"))?.to_str().ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Invalid file name"))?);
}
}
}
Ok(())}
How to replace the unwrap()'s that are handling Option without making the code more complicated (especially, without so much nesting)? Or at least not as complicated as the one I shared?
In many of the cases where you were calling unwrap it wasn't actually an error, you just want to do something different (or not at all).
If you're only interested in the case where there is a png extension, check if that's what you got. It doesn't matter if it's None or .jpg.
Printing a filename can fail because filenames can have non unicode characters and Rust strings can't. In this case given it's presumably meant to be human readable, printing the to_string_lossy() output (replacing non-unicode characters) or printing it using debug mode (escaping non-unicode characters) is probably fine.
use std::fs;
use std::path::Path;
fn main() -> Result<(), std::io::Error> {
let path = Path::new("/home/alex/Desktop");
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "png" {
fs::remove_file(&path)?;
// I would keep expect here as extension would return None if there was no filename
// to_string_lossy returns a printable string - possibly replacing non-unicode characters
println!("{}", path.file_name().expect("File has no name").to_string_lossy());
}
}
}
}
Ok(())
}
I want to read files from a config folder at the directory where the executable is located. I do that using the following functions:
use std::env;
// add part of path to te path gotten from fn get_exe_path();
fn get_file_path(path_to_file: &str) -> PathBuf {
let final_path = match get_exe_path() {
Ok(mut path) => {
path.push(path_to_file);
path
}
Err(err) => panic!("Path does not exists"),
};
final_path
}
// Get path to current executable
fn get_exe_path() -> Result<PathBuf, io::Error> {
//std::env::current_exe()
env::current_exe()
}
In my case, get_exe_path() will return C:\Users\User\Documents\Rust\Hangman\target\debug\Hangman.exe.
With get_file_path("Config\test.txt"), I want to append Config\test.txt To the above path. Then I get the following path to the file: C:\Users\User\Documents\Rust\Hangman\target\debug\Hangman.exe\Config\test.txt
The problem is that std::env::current_exe() will get the file name of the executable also and I do not need that. I only need the directory where it is located.
Question
The following the following function call should return C:\Users\User\Documents\Rust\Hangman\target\debug\Config\test.txt:
let path = get_file_path("Config\\test.txt");
How can I get the path from the current directory without the executable name like above example? Are there any other ways to do this than using std::env::current_exe()
PathBuf::pop is the mirror of PathBuf::push:
Truncates self to self.parent.
Returns false and does nothing if self.file_name is None. Otherwise,
returns true.
In your case:
use std::env;
use std::io;
use std::path::PathBuf;
fn inner_main() -> io::Result<PathBuf> {
let mut dir = env::current_exe()?;
dir.pop();
dir.push("Config");
dir.push("test.txt");
Ok(dir)
}
fn main() {
let path = inner_main().expect("Couldn't");
println!("{}", path.display());
}
There's also the possibility of using Path::parent:
Returns the Path without its final component, if there is one.
Returns None if the path terminates in a root or prefix.
In your case:
fn inner_main() -> io::Result<PathBuf> {
let exe = env::current_exe()?;
let dir = exe.parent().expect("Executable must be in some directory");
let mut dir = dir.join("Config");
dir.push("test.txt");
Ok(dir)
}
See also:
How to get the name of current program without the directory part?
I'm a Java developer and in the process of learning Rust. Here is a tiny example that reads the file names of the current directory into a string list/vector and then outputs it.
Java with nio:
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.stream.*;
public class ListFileNames {
public static void main(String[] args) {
final Path dir = Paths.get(".");
final List<String> fileNames = new ArrayList<>();
try {
final Stream<Path> dirEntries = Files.list(dir);
dirEntries.forEach(path -> {
if (Files.isRegularFile(path)) {
fileNames.add(path.getFileName().toString());
}
});
}
catch (IOException ex) {
System.err.println("Failed to read " + dir + ": " + ex.getMessage());
}
print(fileNames);
}
private static void print(List<String> names) {
for (String name : names) {
System.out.println(name);
}
}
}
Here is what I came up with in Rust:
use std::fs;
use std::path::Path;
fn main() {
let dir = Path::new(".");
let mut entries: Vec<String> = Vec::new();
let dir_name = dir.to_str().unwrap();
let dir_content = fs::read_dir(dir);
match dir_content {
Ok(dir_content) => {
for entry in dir_content {
if let Ok(dir_entry) = entry {
if let Ok(file_type) = dir_entry.file_type() {
if file_type.is_file() {
if let Some(string) = dir_entry.file_name().to_str() {
entries.push(String::from(string));
}
}
}
}
}
}
Err(error) => println!("failed to read {}: {}", dir_name, error)
}
print(entries);
}
fn print(names: Vec<String>) {
for name in &names {
println!("{}", name);
}
}
Are there means to reduce the excessive indentations in the Rust code making it more easier to read similar to the Java code?
It is possible to use ? to short-circuit execution, as demonstrated by #Bazaim, although this has slightly different semantics as it stops on the first error, rather than ignoring it.
To keep true to your semantics, you would move to a Stream-based Iterator-based approach as can be seen here:
use std::error::Error;
use std::fs;
use std::path::Path;
fn main() -> Result<(), Box<dyn Error>> {
let dir = Path::new(".");
let dir_content = fs::read_dir(dir)?;
dir_content.into_iter()
.filter_map(|entry| entry.ok())
.filter(|entry| if let Ok(t) = entry.file_type() { t.is_file() } else { false })
.filter_map(|entry| entry.file_name().into_string().ok())
.for_each(|file_name| println!("{}", file_name));
Ok(())
}
I am not quite happy with that filter step, but I do like the fact that the functional-style API cleanly separates each step.
I'm also a beginner at Rust.
The ? operator is very usefull.
Here is the smallest version I get :
use std::error::Error;
use std::fs;
use std::path::Path;
fn main() -> Result<(), Box<dyn Error>> {
let dir = Path::new(".");
let mut entries: Vec<String> = Vec::new();
let dir_content = fs::read_dir(dir)?;
for entry in dir_content {
let entry = entry?;
if entry.file_type()?.is_file() {
if let Some(string) = entry.file_name().to_str() {
entries.push(String::from(string));
}
}
}
print(entries);
Ok(())
}
fn print(names: Vec<String>) {
for name in &names {
println!("{}", name);
}
}
Slightly modified version of #Bazaim's answer.
So, you can write the fallible code in a separate function and pass it reference to the vector.
If it fails, you can print the error and still print the names from the vector you passed in.
use std::error::Error;
use std::fs;
use std::path::Path;
fn main() {
let mut names = vec![];
if let Err(e) = collect(&mut names) {
println!("Error: {}", e);
}
for name in names {
println!("{}", name);
}
}
fn collect(names: &mut Vec<String>) -> Result<(), Box<dyn Error>> {
let dir = Path::new(".");
let dir_content = fs::read_dir(dir)?;
for entry in dir_content {
let entry = entry?;
if entry.file_type()?.is_file() {
if let Some(string) = entry.file_name().to_str() {
names.push(String::from(string));
}
}
}
Ok(())
}
This code walks the /tmp folder to show files that end in .txt:
const FOLDER_NAME: &str = "/tmp";
const PATTERN: &str = ".txt";
use std::error::Error;
use walkdir::WalkDir; // 2.2.9
fn main() -> Result<(), Box<dyn Error>> {
println!("Walking folder {}", FOLDER_NAME);
for entry in WalkDir::new(FOLDER_NAME).into_iter().filter_map(|e| e.ok()) {
let x = entry.file_name().to_str();
match x {
Some(x) if x.contains(PATTERN) => println!("This file matches: {:?}", entry),
_ => (),
}
}
Ok(())
}
Although this works, is it possible to leverage filter_map to do the suffix filtering that's currently happening in match?
You need to return the entry wrapped in a Some when the condition is true:
use std::error::Error;
use walkdir::WalkDir; // 2.2.9
const FOLDER_NAME: &str = "/tmp";
const PATTERN: &str = ".txt";
fn main() -> Result<(), Box<dyn Error>> {
println!("Walking folder {}", FOLDER_NAME);
let valid_entries = WalkDir::new(FOLDER_NAME)
.into_iter()
.flat_map(|e| e)
.flat_map(|e| {
let name = e.file_name().to_str()?;
if name.contains(PATTERN) {
Some(e)
} else {
None
}
});
for entry in valid_entries {
println!("This file matches: {:?}", entry);
}
Ok(())
}
You'll note that I've secretly switched to Iterator::flat_map. Iterator::filter_map would also work, but I find flat_map more ergonomic, especially for your "ignore the errors" case.
It's debatable whether this is useful compared to a regular Iterator::filter call:
let valid_entries = WalkDir::new(FOLDER_NAME)
.into_iter()
.flat_map(|e| e)
.filter(|e| {
e.file_name()
.to_str()
.map_or(false, |n| n.contains(PATTERN))
});
See also:
Why does `Option` support `IntoIterator`?
How can I filter an iterator when the predicate returns a Result<bool, _>?
The goal is to write a function that gets two paths, input_dir and output_dir, and convertes all markdown files from input_dir to html files in output_dir.
I finally managed to get it to run but it was rather frustrating. The parts that should be hard are super easy: the actual conversion from Markdown to HTML is effectively only one line. The seemingly easy parts are what took me the longest. Using a vector of paths and put all files into it is something I replaced with the glob crate. Not because I couldn't get it to work but it was a mess of if let and unwrap. A simple function that iterates over the list of elements and figures out which of them are actually files and not directories? Either I need four indentation levels if if let or I freak out over matches.
What am I doing wrong?
But lets start with some things I tried to get a list of items in a directory filtered to only contain actual files:
use std::fs;
use std::vec::Vec;
fn list_files (path: &str) -> Result<Vec<&str>, &str> {
if let Ok(dir_list) = fs::read_dir(path) {
Ok(dir_list.filter_map(|e| {
match e {
Ok(entry) => match entry.file_type() {
Ok(_) => entry.file_name().to_str(),
_ => None
},
_ => None
}
}).collect())
} else {
Err("nope")
}
}
fn main() {
let files = list_files("testdir");
println!("{:?}", files.unwrap_or(Vec::new()));
}
So, this code doesn't build, because the file name in Line 10 doesn't live long enough. I guess I could somehow create an owned String but that would introduce another nesting level because OsStr.to_string() returns a Result.
Now I looked through the code of the glob crate and they just use a mutable vector:
fn list_files (path: &str) -> Result<Vec<&str>, &str> {
let mut list = Vec::new();
if let Ok(dir_list) = fs::read_dir(path) {
for entry in dir_list {
if let Ok(entry) = entry {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
if let Some(name) = entry.file_name().to_str() {
list.push(name)
}
}
}
}
}
Ok(list)
} else {
Err("nope")
}
}
This not only adds crazy nesting, it also fails with the same problem. If I change from Vec<&str> to Vec<String>, it works:
fn list_files (path: &str) -> Result<Vec<String>, &str> {
let mut list = Vec::new();
if let Ok(dir_list) = fs::read_dir(path) {
for entry in dir_list {
if let Ok(entry) = entry {
if let Ok(file_type) = entry.file_type() {
if file_type.is_file() {
if let Ok(name) = entry.file_name().into_string() {
list.push(name)
}
}
}
}
}
Ok(list)
} else {
Err("nope")
}
}
Looks like I should apply that to my first try, right?
fn list_files (path: &str) -> Result<Vec<String>, &str> {
if let Ok(dir_list) = fs::read_dir(path) {
Ok(dir_list.filter_map(|e| {
match e {
Ok(entry) => match entry.file_type() {
Ok(_) => Some(entry.file_name().into_string().ok()),
_ => None
},
_ => None
}
}).collect())
} else {
Err("nope")
}
}
At least a bit shorter… but it fails to compile because a collection of type std::vec::Vec<std::string::String> cannot be built from an iterator over elements of type std::option::Option<std::string::String>.
It is hard to stay patient here. Why does .filter_map return Options instead of just using them to filter? Now I have to change line 15 from }).collect()) to }).map(|e| e.unwrap()).collect()) which iterates once more over the result set.
That can't be right!
You can massively rely on ? operator:
use std::fs;
use std::io::{Error, ErrorKind};
fn list_files(path: &str) -> Result<Vec<String>, Error> {
let mut list = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_file() {
list.push(entry.file_name().into_string().map_err(|_| {
Error::new(ErrorKind::InvalidData, "Cannot convert file name")
})?)
}
}
Ok(list)
}
Do not forget that you can split your code into functions or implement your own traits to simplify the final code:
use std::fs;
use std::io::{Error, ErrorKind};
trait CustomGetFileName {
fn get_file_name(self) -> Result<String, Error>;
}
impl CustomGetFileName for std::fs::DirEntry {
fn get_file_name(self) -> Result<String, Error> {
Ok(self.file_name().into_string().map_err(|_|
Error::new(ErrorKind::InvalidData, "Cannot convert file name")
)?)
}
}
fn list_files(path: &str) -> Result<Vec<String>, Error> {
let mut list = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
if entry.file_type()?.is_file() {
list.push(entry.get_file_name()?)
}
}
Ok(list)
}
An alternative answer with iterators, playground
use std::fs;
use std::error::Error;
use std::path::PathBuf;
fn list_files(path: &str) -> Result<Vec<PathBuf>, Box<Error>> {
let x = fs::read_dir(path)?
.filter_map(|e| e.ok())
.filter(|e| e.metadata().is_ok())
.filter(|e| e.metadata().unwrap().is_file())
.map(|e| e.path())
.collect();
Ok(x)
}
fn main() {
let path = ".";
for res in list_files(path).unwrap() {
println!("{:#?}", res);
}
}