Rust Compile Error with use of possibly-uninitialized when the logic is sound

Viewed 118

So I have a rust code that acts as a CLI, it has a -o filename.txt kind of optional syntax which if given makes the program write some contents to a file, if not given it skips the file writing part.

A simple code without any of the complicated variable comes down to this:

    fn main() {
        let x:i64; // define the file
        let y = true; // if -o is present, false otherwise (it'll be mut)
        if y {
            x = 1; // open file in argument of -o
        }

        // some other logics and actions, it won't change y.

        if y {
            println!("{}", x); // write to file
        }
    }

So basically the value of x will be initialized and accessed only when y is true, so it'll never be accessed uninitialized. But rust throws this error:

error[E0381]: borrow of possibly-uninitialized variable: `x`
  --> src/main.rs:11:21
   |
11 |         println!("{}", x); // write to file
   |                        ^ use of possibly-uninitialized `x`

So there is similar problem solved in this: question

But unlike that question i need y be a variable so the if statement needs to be checked in runtime instead of compile time. Is there a way to compile it by telling the compiler it's ok?

Actual code is here (to avoid asking the wrong question as the solution could be to use a different method instead of forcing the compiler):

[dependencies]
clap={ version = "3.0", features = ["derive"] }
use std::{io, thread, time};
use std::io::Write;     // for flush
use std::fs;
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// Do not clear output file before writing to it
    #[clap(short, long, action)]
    append: bool,
    /// Output File to write the captured contents.
    #[clap(parse(from_os_str), short, long, default_value = "")]
    output: std::path::PathBuf,
}


fn main() {
    let args = Cli::parse();

    let mut out_to_file = false;
    if !args.output.as_os_str().is_empty() {
      out_to_file = true;
    }
    
    let mut file;
    if out_to_file {
      file = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .append(args.append)
        .truncate(!args.append)
        .open(args.output)
        .unwrap();
    }

    let clip_new = "Test String";
    let separator = "\n";
    loop {
      print!("{}{}", clip_new, separator);
      io::stdout().flush().unwrap();

      if out_to_file{
        file.write_all(clip_new.as_bytes()).expect("Unable to write to file.");
        file.write_all(separator.as_bytes()).expect("Unable to write to file.");
      }

      thread::sleep(time::Duration::from_millis(1000));
    }
}

So the current workaround I have is to have file always initialized.

My current workaround opens a temp file, and uses it to write the output if user does want a output file. But I'd prefer to not open a file if user doesn't want it.

    let mut out_file = env::temp_dir().join("testfile.txt");
    if !args.output.as_os_str().is_empty() {
        out_file = args.output;
    }

Using this and opening out_file instead of args.output without any conditional checks (removing all if out_to_file).

1 Answers

Figured out the solution while still drafting this question so I'll leave it here.

The solution was to use Option<fs::File> which can be None. So, let mut file:Option<fs::File> = None; will keep file initialized, but without having to open a dummy file. And real file can be put with Some.

use std::{io, thread, time};
use std::io::Write;     // for flush
use std::fs;
use clap::Parser;

#[derive(Parser)]
struct Cli {
    /// Do not clear output file before writing to it
    #[clap(short, long, action)]
    append: bool,
    /// Output File to write the captured contents.
    #[clap(parse(from_os_str), short, long, default_value = "")]
    output: std::path::PathBuf,
}


fn main() {
    let args = Cli::parse();

    let out_to_file = if !args.output.as_os_str().is_empty() {
        true
    } else {
        false
    };
    
    let mut file:Option<fs::File> = None;
    if out_to_file {
    file = Some(fs::OpenOptions::new()
        .write(true)
        .create(true)
        .append(args.append)
        .truncate(!args.append)
        .open(args.output)
        .unwrap());
    }

    let clip_new = "Test String";
    let separator = "\n";
    loop {
    print!("{}{}", clip_new, separator);
    io::stdout().flush().unwrap();

    if out_to_file{
        file.as_ref().unwrap().write_all(clip_new.as_bytes()).expect("Unable to write to file.");
        file.as_ref().unwrap().write_all(separator.as_bytes()).expect("Unable to write to file.");
    }

    thread::sleep(time::Duration::from_millis(100));
    }
}

Equivalent solution to the dummy problem

fn main() {
    let mut x:Option<i64> = None; // definite the file as Option
    let y = true; // if -o is present, false otherwise (it'll not be mut)
    if y {
        x = Some(1); // open file in argument of -o as Some
    }

    // some other logics and actions, it won't change y.

    if y {
        println!("{}", x.unwrap()); // write to file after .unwrap()
    }
}
Related