[opentitantool] Support parsing an opentitantoolrc

Allow parsing an `opentitantoolrc` file which can hold the user's desired
default configuration options.

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/sw/host/opentitantool/Cargo.toml b/sw/host/opentitantool/Cargo.toml
index 93bedde..043c83c 100644
--- a/sw/host/opentitantool/Cargo.toml
+++ b/sw/host/opentitantool/Cargo.toml
@@ -20,6 +20,8 @@
 nix = "0.17.0"
 indicatif = "0.16.2"
 humantime = "2.1.0"
+directories = "4.0.1"
+shellwords = "1.1.0"
 
 serde = {version="1", features=["serde_derive"]}
 serde_json = "1"
diff --git a/sw/host/opentitantool/src/main.rs b/sw/host/opentitantool/src/main.rs
index cad72bd..1398c1b 100644
--- a/sw/host/opentitantool/src/main.rs
+++ b/sw/host/opentitantool/src/main.rs
@@ -3,7 +3,12 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use anyhow::Result;
+use directories::ProjectDirs;
 use log::LevelFilter;
+use std::env::{args_os, ArgsOs};
+use std::ffi::OsString;
+use std::io::ErrorKind;
+use std::path::PathBuf;
 use structopt::StructOpt;
 
 mod backend;
@@ -29,6 +34,13 @@
 
 #[derive(Debug, StructOpt)]
 struct Opts {
+    #[structopt(
+        long,
+        default_value = "config",
+        help = "Filename of a default flagsfile.  Relative to $XDG_CONFIG_HOME/opentitantool."
+    )]
+    rcfile: PathBuf,
+
     #[structopt(long, default_value = "off")]
     logging: LevelFilter,
 
@@ -39,11 +51,63 @@
     command: RootCommandHierarchy,
 }
 
+// Given some existing option configuration, maybe re-evaluate command
+// line options by reading an `rcfile`.
+fn parse_command_line(opts: Opts, mut args: ArgsOs) -> Result<Opts> {
+    // Initialize the logger if the user requested the non-defualt option.
+    let logging = opts.logging;
+    if logging != LevelFilter::Off {
+        env_logger::Builder::from_default_env()
+            .filter(None, opts.logging)
+            .init();
+    }
+    if opts.rcfile.as_os_str().is_empty() {
+        // No rcfile to parse.
+        return Ok(opts);
+    }
+
+    // Construct the rcfile path based on the user's config directory
+    // (ie: $HOME/.config/opentitantool/<filename>).
+    let rcfile = if let Some(base) = ProjectDirs::from("org", "opentitan", "opentitantool") {
+        base.config_dir().join(&opts.rcfile)
+    } else {
+        opts.rcfile.clone()
+    };
+
+    // argument[0] is the executable name.
+    let mut arguments = vec![args.next().unwrap()];
+
+    // Read in the rcfile and extend the argument list.
+    match std::fs::read_to_string(&rcfile) {
+        Ok(content) => {
+            for line in content.split('\n') {
+                // Strip basic comments as shellwords won't handle comments.
+                let (line, _) = line.split_once('#').unwrap_or((line, ""));
+                arguments.extend(shellwords::split(line)?.iter().map(OsString::from));
+            }
+            Ok(())
+        }
+        Err(e) if e.kind() == ErrorKind::NotFound => {
+            log::warn!("Could not read {:?}. Ignoring.", rcfile);
+            Ok(())
+        }
+        Err(e) => Err(anyhow::Error::new(e).context(format!("Reading file {:?}", rcfile))),
+    }?;
+
+    // Extend the argument list with all remaining command line arguments.
+    arguments.extend(args.into_iter());
+    let opts = Opts::from_iter(&arguments);
+    if opts.logging != logging {
+        // Try re-initializing the logger.  Ignore errors.
+        let _ = env_logger::Builder::from_default_env()
+            .filter(None, opts.logging)
+            .try_init();
+    }
+    Ok(opts)
+}
+
 fn main() -> Result<()> {
-    let opts = Opts::from_args();
-    env_logger::Builder::from_default_env()
-        .filter(None, opts.logging)
-        .init();
+    let opts = parse_command_line(Opts::from_args(), args_os())?;
 
     let transport = backend::create(&opts.backend_opts)?;