Project Requirement

I needed a script to parse through a directory + subdirectories to find all swift files and see if there are any string quotes in the files.

Download and Install Process

# Install
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Source cargo env
source $HOME/.cargo/env

Writing the code

Project Configuration with Cargo.toml

Cargo.toml is a configuration file used by Cargo, the Rust package manager and build system. It's an essential part of any Rust project. This file contains metadata about your project and dependencies on other Rust crates (libraries or packages).

You can find the crates here: https://crates.io/

Code

[package]  
name = "parse_swift_string"  
version = "0.1.0"  
edition = "2021"  
  
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html  
  
[dependencies]  
walkdir = "2.4"  
regex = "1.10"  
csv = "1.3"

Structure of Cargo.toml

A typical Cargo.toml file includes several sections:

  • [package]: Contains metadata about your project like name, version, authors, edition, etc.
  • [dependencies]: Lists the crates your project depends on. You can specify versions here.
  • [dev-dependencies]: Crates used in development, like testing libraries, which aren't needed in the final build.
  • Other sections like [features], [workspace], etc., for more advanced configurations.

Actual Code in main.rs

Code

use walkdir::WalkDir;  
use regex::Regex;  
use csv::Writer;  
use std::fs::File;  
use std::io::{self, BufRead};  
use std::env;  
  
fn main() -> Result<(), Box<dyn std::error::Error>> {  
    // Grab the arguments from the run command
    // In this case we want it to be the directory that we want to parse
    let args: Vec<String> = env::args().collect();  
    if args.len() < 2 {  
        eprintln!("Usage: {} <directory>", args[0]);  
        std::process::exit(1);  
    }
    let project_path = &args[1];
    let output_file = "extracted_strings.csv";  
  
    let re = Regex::new(r#""([^"\\]*(?:\\.[^"\\]*)*)""#).unwrap();  
    let mut wtr = Writer::from_path(output_file)?;  
  
    for entry in WalkDir::new(project_path) {  
        let entry = entry?;  
        let path = entry.path();  
        if path.extension().map_or(false, |e| e == "swift") {  
            let file = File::open(path)?;  
            let reader = io::BufReader::new(file);  
  
            for (line_number, line) in reader.lines().enumerate() {  
                let line = line?;  
                for cap in re.captures_iter(&line) {  
                    // Check if the match is not escaped  
                    if !line[..cap.get(0).unwrap().start()].ends_with('\\') {  
                        wtr.write_record(&[  
                            path.to_string_lossy().to_string(),  
                            line_number.to_string(),  
                            cap[1].to_string()  
                        ])?;  
                    }  
                }  
            }  
        }  
    }  
  
    wtr.flush()?;  
    println!("Data extracted to {}", output_file);  
    Ok(())  
}

Namespaces like C/C++

Similar to C/C++ and probably other languages you can let the compiler that you are using certain name spaces.

In this case wea re using walkdir/regex/csv on top of the builtin std

Variables immutable by default

In Rust all variables are immutable by default , so if you look at the wtr variable that one is mutable because it represents a CSV writer (Writer from the csv crate) that changes its internal state as it writes data.

When you create a Writer instance in Rust for writing CSV data, the act of writing to a CSV file is not just a single operation but potentially many. Each time you write a record (a row in the CSV file), the Writer updates its internal state. This includes keeping track of the current position in the output stream (like a file or in-memory buffer), buffering the data, and possibly handling encodings.

? after a variable

In Rust, the question mark (?) is an operator used for error handling. It's a convenient shorthand for propagating errors up the call stack. When you see ? used after an expression, it means that if the expression evaluates to an Err variant of a Result or Option, it will return from the enclosing function with that Err. If the expression is Ok, the program will continue, and the value inside the Ok will be extracted.

Here, WalkDir::new(project_path) returns an iterator that yields items of type Result<DirEntry, Error>. Each item is a Result because reading a directory can fail for various reasons (like permissions issues, the directory not existing, etc.).

The let entry = entry?; line is checking each item as it's iterated over. If an item is an Err, the ? operator will cause the function to return early with that error. If it's an Ok, it extracts the DirEntry from inside the Ok and assigns it to the entry variable, which is then used in the loop.

This use of ? makes the code much cleaner and more readable, especially compared to manual error handling where you would need to use match statements or unwrapping. It's a very idiomatic way in Rust to handle errors in situations where you want to propagate them upwards instead of handling them at the point where they occur.

Error Propagation: Since the main function itself returns Result, when an error is returned using ?, it propagates up to the caller of main. In the case of the main function, this caller is the Rust runtime. If main returns an Err, the Rust runtime will handle this by printing a diagnostic message to the standard error stream and exiting the program.

Reassigning entry in the for loop

for entry in WalkDir::new(project_path) {
    let entry = entry?;
    // ...
}
  1. Error Handling: It uses the ? operator to handle the Result. If entry is an Err, the ? operator will return that error from the current function (main in your case), effectively stopping the iteration and the function execution. This is a concise way to propagate errors upwards.

  2. Unwrapping the Result: If entry is an Ok, the ? operator unwraps it, extracting the DirEntry value. This unwrapped value is then bound to a new variable, also named entry (shadowing the previous entry variable from the loop). This new entry variable is now a DirEntry object that can be used directly in the subsequent code without further unwrapping.

More Rust specifics

What is map_or()

fn map_or<T, F>(self, default: T, f: F) -> T
where
    F: FnOnce(T) -> T,

if path.extension().map_or(false, |e| e == "swift") {
    // ...
}

  • false: is the default
  • |e| e == "swift": apply the closure / synonymous with supplying a lambda function in python

"applying the closure" refers to executing a closure (an anonymous function) on the value contained within an Option, if that Option is Some.

Compile

# Compile and run immediately against a directory
cargo run -- /path/to/your/ios/project

# Compile for release. Release binary is compiled to `./target/release/{program_name}`
cargo build --release

Results

example/Example1.swift,6,"Hello, World!"
example/Example1.swift,10,Greeting: \(message)
example/Example1.swift,14,Calculating sum of \(a) and \(b)
example/Example1.swift,19,To be or not to be
example/Example1.swift,27,Sum is \(sum)
example/Example2.swift,7,My name is \(name) and I am \(age) years old.
example/Example2.swift,11,John Doe
example/Example2.swift,15,My favorite color is \(color).
example/Example2.swift,18,blue
example/Example2.swift,20,This is a warning message!
example/Example2.swift,23,An error occurred while processing your request.
file line no string
example/Example1.swift 6 "Hello, World!"
example/Example1.swift 10 Greeting: (message)
example/Example1.swift 14 Calculating sum of (a) and (b)
example/Example1.swift 19 To be or not to be
example/Example1.swift 27 Sum is (sum)
example/Example2.swift 7 My name is (name) and I am (age) years old.
example/Example2.swift 11 John Doe
example/Example2.swift 15 My favorite color is (color).
example/Example2.swift 18 blue
example/Example2.swift 20 This is a warning message!
example/Example2.swift 23 An error occurred while processing your request.

Repository

https://github.com/JJK-IO/playing_with_rust