Welcome to my YOLO series, where I'll be showcasing simple tools and projects that I've built—sometimes for fun, sometimes to solve specific problems, and other times just out of pure curiosity. The goal here isn't just to present a tool; I'll also dive into something interesting related to the process, whether it's a technical insight or a lesson learned while crafting these little experiments.
Introducing rrm: The Command-Line Tool Nobody Asked For
Nobody asked for it, and nobody want it—but here it is anyway. Meet rrm, a tool that solves a problem only I seem to have (but hey, it might be a Layer 8 issue—or, more likely, a skill issue!).
rrm adds a layer of safety to your command-line experience by moving files to a trash bin instead of permanently deleting them. With a customizable grace period, you get the chance to realize, "Oops, I actually needed that!" before it’s too late.
What’s more, rrm doesn’t rely on external configuration files or tracking systems to manage deleted files. Instead, it leverages your filesystem’s extended attributes to store essential metadata—like the original file path and deletion time—directly within the trashed item.
You might be wondering, "Why am I building this tool when there are similar, possibly better tools out there?" Well, the answer is simple:
- I wanted to play with Rust. Building small, purposeful tools is a great way to explore a language and sharpen skills.
- like developing my own CLI tools as a way to create a mental framework. It helps me consistently approach how I structure command-line utilities for specific technologies. By building these tools, I refine my understanding of what dependencies to use, how to organize the code, and how to adapt each tool to the language’s ecosystem. It’s a way of building a mental playbook for creating CLI tools that suit my needs.
- Because YOLO. I enjoy making simple tools or proof-of-concepts around problems I want to solve or things I’m curious about. Sometimes, it’s about experimenting for the sake of learning.
Fun note: While working with std::Path, I found an example in the Rust standard library that uses a folder named laputa
. I know it's a reference to Castle in the Sky, but for Spanish speakers, it’s also a curse word, which made it a funny moment for me!<script> // Detect dark theme var iframe = document.getElementById('tweet-1844834987184410735-190'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1844834987184410735&theme=dark" } </script>Extended Attributes: Storing Metadata Without Changing the File
When I started building rrm, I needed a way to track the original path of deleted files and the time when they should be permanently removed. I didn’t want to use a JSON file or implement a weird naming format that includes this information—especially if I wanted to store more data later. A database felt like overkill for such a small task.
That’s when I discovered extended attributes.
What Are Extended Attributes?
Now, I don’t know about you, but I didn’t realize there was a built-in mechanism that lets you add custom metadata to files, which is supported by most Linux filesystems and Unix-like systems such as macOS. This feature is called Extended File Attributes. Different systems have their own limitations—like how much data can be added or the specific namespaces used—but they do allow you to store user-defined metadata.
Extended attributes are essentially name:value pairs that are permanently associated with files and directories. As I mentioned earlier, systems differ in how they handle this. For example, in Linux, the name starts with a namespace identifier. There are four such namespaces: security, system, trusted, and user. In Linux, the name starts with one of these, followed by a dot (".") and then a null-terminated string. On macOS, things are a bit different. macOS doesn't require namespaces at all, thanks to its Unified Metadata Approach, which treats extended attributes as additional metadata directly tied to files without needing to be categorized.
In this tiny CLI, I’m using the crate xattr, which supports both Linux and macOS. Regarding the namespaces I mentioned earlier for Linux, we'll be focusing on the user namespace since these attributes are meant to be used by the user. So, in the code, you’ll see something like this:
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
The #[cfg(target_os = "macos")] attribute in Rust is used to conditionally compile code based on the target operating system. In this case, it ensures that the code block is only included when compiling for macOS. This is relevant because, as mentioned earlier, macOS doesn’t require a namespace for extended attributes, so the XATTR_NAMESPACE is set to an empty string. For other operating systems, the namespace is set to "user.". This conditional compilation allows the code to adapt seamlessly across different platforms, making the CLI cross-compatible with both Linux and macOS.
One thing I found pretty cool about extended attributes is that they don’t modify the file itself. The metadata lives in a separate disk space, referenced by the inode. This means the file's actual contents remain unchanged. For example, if we create a simple file and use shasum to get its checksum:
The inode (index node) is a data structure in a Unix-style file system that describes a file-system object such as a file or a directory. Link
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
After using rrm to delete the file, we can list the deleted files and see that the file has been moved to the trash bin with its metadata intact:
$ cat a.txt https://www.kungfudev.com/ $ shasum a.txt e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 a.txt
As you can see, the file name is changed to a UUID. This is done to avoid name collisions when deleting files with the same name. By assigning a unique identifier to each file, rrm ensures that every deleted file, even if they have identical names, can be tracked and recovered without any issues.
We can navigate to the trash folder and inspect the file to confirm that its contents remain unchanged:
$ rrm rm a.txt $ rrm list ╭──────────────────────────────────────────────────────┬──────────────────────────────────────┬──────┬─────────────────────╮ │ Original Path ┆ ID ┆ Kind ┆ Deletion Date │ ╞══════════════════════════════════════════════════════╪══════════════════════════════════════╪══════╪═════════════════════╡ │ /Users/douglasmakey/workdir/personal/kungfudev/a.txt ┆ 3f566788-75dc-4674-b069-0faeaa86aa55 ┆ File ┆ 2024-10-27 04:10:19 │ ╰──────────────────────────────────────────────────────┴──────────────────────────────────────┴──────┴─────────────────────╯
Additionally, by using xattr on macOS, we can verify that the file has its metadata, such as the deletion date and original path:
$ shasum 3f566788-75dc-4674-b069-0faeaa86aa55 e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 3f566788-75dc-4674-b069-0faeaa86aa55
You can imagine the range of potential use cases for simple validations or actions using this metadata. Since extended attributes work without modifying the file itself, they allow you to check file integrity or perform other operations without affecting the original content.
This is just a small introduction to extended attributes and how they’re used in this project. It’s not meant to be an in-depth explanation, but if you’re interested in learning more, there are plenty of detailed resources out there. Here are a couple of links to the most useful and well-described resources on the topic:
- https://wiki.archlinux.org/title/Extended_attributes
- https://man7.org/linux/man-pages/man7/xattr.7.html
- https://en.wikipedia.org/wiki/Extended_file_attributes
Mocking in Rust: Exploring mockall for Testing
I’ve spent a few years working with Go, and I’ve become fond of certain patterns—mocking being one of them. In Go, I typically implement things myself if it avoids unnecessary imports or gives me more flexibility. I’m so used to this approach that when I started writing tests in Rust, I found myself preferring to manually mock certain things, like creating mock implementations of traits.
For example, in this tiny CLI, I created a trait to decouple the trash manager from the way it interacts with the extended attributes. The trait, named ExtendedAttributes, was initially intended for testing purposes, but also because I wasn’t sure whether I would use xattr or another implementation. So, I defined the following trait:
$ xattr -l 3f566788-75dc-4674-b069-0faeaa86aa55 deletion_date: 2024-10-27T04:10:19.875614+00:00 original_path: /Users/douglasmakey/workdir/personal/kungfudev/a.txt
In Go, I would create something like the following, which provides a simple implementation of the previously mentioned interface. The code below is straightforward and generated without much consideration, just for the sake of example:
/// Namespace for extended attributes (xattrs) on macOS and other operating systems. /// On macOS, this is an empty string, while on other operating systems, it is "user.". #[cfg(target_os = "macos")] const XATTR_NAMESPACE: &str = ""; #[cfg(not(target_os = "macos"))] const XATTR_NAMESPACE: &str = "user."; ... fn set_attr(&self, path: &Path, attr: &str, value: &str) -> Result { let attr_name = format!("{}{}", XATTR_NAMESPACE, attr); ... }
Then, I would use my mock and inject the specific behavior needed for each test. Again, this is simple code just for the sake of the example:
$ cat a.txt https://www.kungfudev.com/ $ shasum a.txt e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 a.txt
I've gotten used to this pattern in Go, and I plan to keep using it. But I’ve also been doing something similar in Rust. For this project, I decided to try the mockall crate, and I found it really useful.
First, I used the mock! macro to manually mock my structure. I know mockall has an automock feature, but I prefer to define the mock struct directly in my tests where it will be used. Let me know if this is something common or if the community has a different standard for this.
$ rrm rm a.txt $ rrm list ╭──────────────────────────────────────────────────────┬──────────────────────────────────────┬──────┬─────────────────────╮ │ Original Path ┆ ID ┆ Kind ┆ Deletion Date │ ╞══════════════════════════════════════════════════════╪══════════════════════════════════════╪══════╪═════════════════════╡ │ /Users/douglasmakey/workdir/personal/kungfudev/a.txt ┆ 3f566788-75dc-4674-b069-0faeaa86aa55 ┆ File ┆ 2024-10-27 04:10:19 │ ╰──────────────────────────────────────────────────────┴──────────────────────────────────────┴──────┴─────────────────────╯
I found mockall really useful, allowing me to inject specific behaviors into my tests without the verbosity of my old pattern.
$ shasum 3f566788-75dc-4674-b069-0faeaa86aa55 e4c51607d5e7494143ffa5a20b73aedd4bc5ceb5 3f566788-75dc-4674-b069-0faeaa86aa55
As we can see, mockall gives us the capability to define specific behaviors for our tests using its mock methods:
- MockXattrManager::new() This creates a new instance of the mock object MockXattrManager, which is used to mock the behavior of XattrManager for testing.
- xattr_manager.expect_set_attr() This sets up an expectation that the set_attr method will be called during the test. You define the expected behavior of this method next.
- with(...) The with method specifies the expected arguments when set_attr is called. In this case, it’s expecting three arguments and uses in_iter to indicate that each argument should match one of the values in the provided vector. This allows flexibility in the test, as it checks if the arguments are one of the values from the passed vector rather than a single exact match.
- times(4) This specifies that the set_attr method is expected to be called exactly four times during the test.
- returning(|_, _, _| Ok(())) This tells the mock what to return when set_attr is called. In this case, it returns Ok(()) regardless of the arguments (|_, _, _| means the arguments are ignored). This simulates the successful execution of set_attr.
Some of you might find this super basic or not that interesting, but as I mentioned, in this YOLO series, I’m sharing things that I find interesting or just want to talk about. I wasn’t a big fan of using this kind of library in Go, partly due to Go’s constraints, but in Rust, I found mockall really useful. It even reminded me of my old days with Python.
Again, this section wasn’t meant to explain mocking in Rust or mockall. I’m sure there are plenty of great resources that cover it in detail. I just wanted to mention it briefly.
To conclude
In this post, I’ve shared some of the reasoning behind building rrm and the tools I used along the way. From using extended attributes to simplify metadata handling to experimenting with the mockall crate for testing in Rust, these were just things that piqued my interest.
The goal of this YOLO series is to highlight the fun and learning that comes with building even simple tools. I hope you found something useful here, and I look forward to sharing more projects and insights in future posts. As always, feedback is welcome!
Happy coding!
The above is the detailed content of Playing with Rust: Building a Safer rm and Having Fun Along the Way. For more information, please follow other related articles on the PHP Chinese website!

Go uses the "encoding/binary" package for binary encoding and decoding. 1) This package provides binary.Write and binary.Read functions for writing and reading data. 2) Pay attention to choosing the correct endian (such as BigEndian or LittleEndian). 3) Data alignment and error handling are also key to ensure the correctness and performance of the data.

The"bytes"packageinGooffersefficientfunctionsformanipulatingbyteslices.1)Usebytes.Joinforconcatenatingslices,2)bytes.Bufferforincrementalwriting,3)bytes.Indexorbytes.IndexByteforsearching,4)bytes.Readerforreadinginchunks,and5)bytes.SplitNor

Theencoding/binarypackageinGoiseffectiveforoptimizingbinaryoperationsduetoitssupportforendiannessandefficientdatahandling.Toenhanceperformance:1)Usebinary.NativeEndianfornativeendiannesstoavoidbyteswapping.2)BatchReadandWriteoperationstoreduceI/Oover

Go's bytes package is mainly used to efficiently process byte slices. 1) Using bytes.Buffer can efficiently perform string splicing to avoid unnecessary memory allocation. 2) The bytes.Equal function is used to quickly compare byte slices. 3) The bytes.Index, bytes.Split and bytes.ReplaceAll functions can be used to search and manipulate byte slices, but performance issues need to be paid attention to.

The byte package provides a variety of functions to efficiently process byte slices. 1) Use bytes.Contains to check the byte sequence. 2) Use bytes.Split to split byte slices. 3) Replace the byte sequence bytes.Replace. 4) Use bytes.Join to connect multiple byte slices. 5) Use bytes.Buffer to build data. 6) Combined bytes.Map for error processing and data verification.

Go's encoding/binary package is a tool for processing binary data. 1) It supports small-endian and large-endian endian byte order and can be used in network protocols and file formats. 2) The encoding and decoding of complex structures can be handled through Read and Write functions. 3) Pay attention to the consistency of byte order and data type when using it, especially when data is transmitted between different systems. This package is suitable for efficient processing of binary data, but requires careful management of byte slices and lengths.

The"bytes"packageinGoisessentialbecauseitoffersefficientoperationsonbyteslices,crucialforbinarydatahandling,textprocessing,andnetworkcommunications.Byteslicesaremutable,allowingforperformance-enhancingin-placemodifications,makingthispackage

Go'sstringspackageincludesessentialfunctionslikeContains,TrimSpace,Split,andReplaceAll.1)Containsefficientlychecksforsubstrings.2)TrimSpaceremoveswhitespacetoensuredataintegrity.3)SplitparsesstructuredtextlikeCSV.4)ReplaceAlltransformstextaccordingto


Hot AI Tools

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Undress AI Tool
Undress images for free

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

MinGW - Minimalist GNU for Windows
This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

MantisBT
Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.

VSCode Windows 64-bit Download
A free and powerful IDE editor launched by Microsoft

EditPlus Chinese cracked version
Small size, syntax highlighting, does not support code prompt function

WebStorm Mac version
Useful JavaScript development tools
