You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
phetch/src/args.rs

385 lines
13 KiB
Rust

//! args::parse() is used to parse command line arguments into a
//! Config structure.
use {
crate::{
config::{self, Config},
encoding::Encoding,
ui::Mode,
},
std::{error::Error, fmt, result::Result},
};
/// The error returned if something goes awry while parsing the
/// command line arguments.
#[derive(Debug)]
pub struct ArgError {
details: String,
}
impl ArgError {
/// An ArgError represents an error in the user-supplied command
/// line arguments.
pub fn new(err: impl fmt::Display) -> ArgError {
ArgError {
details: format!("{}", err),
}
}
}
impl fmt::Display for ArgError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for ArgError {
fn description(&self) -> &str {
&self.details
}
}
/// Parse command line arguments into a Config structure.
pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
let mut set_nocfg = false;
let mut set_cfg = false;
let mut cfg = Config::default();
// check for config to load / not load first
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_ref() {
"-C" | "--no-config" | "-no-config" => {
if set_cfg {
return Err(ArgError::new("can't mix --config and --no-config"));
}
set_nocfg = true
}
"-c" | "--config" | "-config" => {
if set_nocfg {
return Err(ArgError::new("can't mix --config and --no-config"));
}
set_cfg = true;
if let Some(arg) = iter.next() {
cfg = config::load_file(arg.as_ref())
.map_err(|e| ArgError::new(format!("error loading config: {}", e)))?;
} else {
return Err(ArgError::new("need a config file"));
}
}
a if a.starts_with("--config=") || a.starts_with("-config=") => {
if set_nocfg {
return Err(ArgError::new("can't mix --config and --no-config"));
}
set_cfg = true;
let mut parts = arg.as_ref().splitn(2, '=');
if let Some(file) = parts.nth(1) {
cfg = match config::load_file(file) {
Ok(c) => c,
Err(e) => {
return Err(ArgError::new(format!("error loading config: {}", e)));
}
};
} else {
return Err(ArgError::new("need a config file"));
}
}
_ => {}
}
}
// load phetch.conf from disk if they didn't pass -c or -C
if cfg!(not(test)) && !set_cfg && !set_nocfg && config::exists() {
match config::load() {
Err(e) => return Err(ArgError::new(e)),
Ok(c) => cfg = c,
}
}
let mut iter = args.iter();
let mut got_url = false;
let mut set_tls = false;
let mut set_notls = false;
let mut set_tor = false;
let mut set_notor = false;
let mut set_media = false;
let mut set_nomedia = false;
let mut set_autoplay = false;
let mut set_noautoplay = false;
while let Some(arg) = iter.next() {
match arg.as_ref() {
"-v" | "--version" | "-version" => {
cfg.mode = Mode::Version;
return Ok(cfg);
}
"-h" | "--help" | "-help" => {
cfg.mode = Mode::Help;
return Ok(cfg);
}
"-r" | "--raw" | "-raw" => {
if args.len() > 1 {
cfg.mode = Mode::Raw;
} else {
return Err(ArgError::new("--raw needs gopher-url"));
}
}
"-p" | "--print" | "-print" => cfg.mode = Mode::Print,
"-l" | "--local" | "-local" => cfg.start = "gopher://127.0.0.1:7070".into(),
"-C" | "--no-config" | "-no-config" => {}
"-c" | "--config" | "-config" => {
iter.next(); // skip arg
}
arg if arg.starts_with("--config=") || arg.starts_with("-config=") => {}
"-t" | "--theme" | "-theme" => {
if let Some(arg) = iter.next() {
cfg.theme = config::load_file(arg.as_ref())
.map_err(|e| ArgError::new(format!("error loading theme: {}", e)))?
.theme;
} else {
return Err(ArgError::new("need a theme file"));
}
}
"--print-theme" => {
cfg.mode = Mode::PrintTheme;
}
"-s" | "--tls" | "-tls" => {
if set_notls {
return Err(ArgError::new("can't set both --tls and --no-tls"));
}
set_tls = true;
cfg.tls = true;
if cfg!(not(feature = "tls")) {
return Err(ArgError::new("phetch was compiled without TLS support"));
}
}
"-S" | "--no-tls" | "-no-tls" => {
if set_tls {
return Err(ArgError::new("can't set both --tls and --no-tls"));
}
set_notls = true;
cfg.tls = false;
}
"-o" | "--tor" | "-tor" => {
if set_notor {
return Err(ArgError::new("can't set both --tor and --no-tor"));
}
if cfg!(not(feature = "tor")) {
return Err(ArgError::new("phetch was compiled without Tor support"));
}
set_tor = true;
cfg.tor = true;
}
"-O" | "--no-tor" | "-no-tor" => {
if set_tor {
return Err(ArgError::new("can't set both --tor and --no-tor"));
}
set_notor = true;
cfg.tor = false;
}
"-w" | "--wrap" | "-wrap" => {
if let Some(column) = iter.next() {
if let Ok(col) = column.as_ref().parse() {
cfg.wrap = col;
} else {
return Err(ArgError::new("--wrap expects a COLUMN arg"));
}
} else {
return Err(ArgError::new("--wrap expects a COLUMN arg"));
}
}
"-m" | "--media" | "-media" => {
if set_nomedia {
return Err(ArgError::new("can't set both --media and --no-media"));
}
set_media = true;
if let Some(player) = iter.next() {
cfg.media = Some(player.as_ref().to_string());
} else {
return Err(ArgError::new("--media expects a PROGRAM arg"));
}
}
"-M" | "--no-media" | "-no-media" => {
if set_media {
return Err(ArgError::new("can't set both --media and --no-media"));
}
set_nomedia = true;
cfg.media = None;
}
"-a" | "--autoplay" | "-autoplay" => {
if set_nomedia {
return Err(ArgError::new("can't set both --no-media and --autoplay"));
}
if set_noautoplay {
return Err(ArgError::new("can't set both --autoplay and --no-autoplay"));
}
set_autoplay = true;
cfg.autoplay = true;
}
"-A" | "--no-autoplay" | "-no-autoplay" => {
if set_autoplay {
return Err(ArgError::new("can't set both --autoplay and --no-autoplay"));
}
cfg.autoplay = false;
set_noautoplay = true;
}
"-e" | "--encoding" | "-encoding" => {
if let Some(encoding) = iter.next() {
cfg.encoding = Encoding::from_str(encoding.as_ref())
.map_err(|e| ArgError::new(e.to_string()))?;
} else {
return Err(ArgError::new("--encoding expects an ENCODING arg"));
}
}
arg => {
if arg.starts_with('-') {
return Err(ArgError::new(format!("unknown flag: {}", arg)));
} else if got_url {
return Err(ArgError::new(format!("unknown argument: {}", arg)));
} else {
got_url = true;
cfg.start = arg.trim().into();
}
}
}
}
if cfg.tor && cfg.tls {
return Err(ArgError::new("can't set both --tor and --tls"));
}
#[cfg(not(test))]
{
if !atty::is(atty::Stream::Stdout)
&& !matches!(cfg.mode, Mode::Raw | Mode::Print | Mode::PrintTheme)
{
cfg.mode = Mode::NoTTY;
}
}
Ok(cfg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple() {
let cfg = parse(&["-l"]).expect("failed to parse");
assert_eq!(cfg.start, "gopher://127.0.0.1:7070");
assert_eq!(cfg.wide, false);
}
#[test]
fn test_ignore_trailing_whitespace() {
let cfg = parse(&["some-url.io "]).expect("should work");
assert_eq!(cfg.start, "some-url.io");
}
#[test]
fn test_unknown() {
let err = parse(&["-z"]).expect_err("-z shouldn't exist");
assert_eq!(err.to_string(), "unknown flag: -z");
let err = parse(&["-l", "-x"]).expect_err("-x shouldn't exist");
assert_eq!(err.to_string(), "unknown flag: -x");
let err = parse(&["sdf.org", "sdf2.org"]).expect_err("two urls should fail");
assert_eq!(err.to_string(), "unknown argument: sdf2.org");
}
#[test]
fn test_local() {
let cfg = parse(&["--local"]).expect("should work");
assert_eq!(cfg.start, "gopher://127.0.0.1:7070");
let cfg = parse(&["-s", "-l"]).expect("should work");
assert_eq!(cfg.start, "gopher://127.0.0.1:7070");
assert_eq!(cfg.tls, true);
}
#[test]
fn test_raw() {
let cfg = parse(&["--raw", "sdf.org"]).expect("should work");
assert_eq!(cfg.mode, Mode::Raw);
assert_eq!(cfg.start, "sdf.org");
let err = parse(&["--raw"]).expect_err("should fail");
assert_eq!(err.to_string(), "--raw needs gopher-url");
}
#[test]
fn test_print() {
let cfg = parse(&["--print", "sdf.org"]).expect("should work");
assert_eq!(cfg.mode, Mode::Print);
assert_eq!(cfg.start, "sdf.org");
let _ = parse(&["--print"]).expect("should work");
assert_eq!(cfg.mode, Mode::Print);
}
#[test]
fn test_help() {
let cfg = parse(&["--help"]).expect("should work");
assert_eq!(cfg.mode, Mode::Help);
}
#[test]
fn test_version() {
let cfg = parse(&["--version"]).expect("should work");
assert_eq!(cfg.mode, Mode::Version);
}
#[test]
fn test_tls_tor() {
let err = parse(&["--tls", "--tor"]).expect_err("should fail");
assert_eq!(err.to_string(), "can\'t set both --tor and --tls");
let err = parse(&["--tls", "--no-tls"]).expect_err("should fail");
assert_eq!(err.to_string(), "can\'t set both --tls and --no-tls");
let err = parse(&["-s", "-S"]).expect_err("should fail");
assert_eq!(err.to_string(), "can\'t set both --tls and --no-tls");
let cfg = parse(&["--tor", "--no-tls"]).expect("should work");
assert_eq!(cfg.tor, true);
assert_eq!(cfg.tls, false);
}
#[test]
fn test_mix_and_match() {
let cfg = parse(&["-r", "-s", "-C"]).expect("should work");
assert_eq!(cfg.mode, Mode::Raw);
assert_eq!(cfg.tls, true);
}
#[test]
fn test_config() {
let err = parse(&["-c"]).expect_err("should fail");
assert_eq!(err.to_string(), "need a config file");
let err = parse(&["-C", "-c", "file.conf"]).expect_err("should fail");
assert_eq!(err.to_string(), "can't mix --config and --no-config");
let err = parse(&["-c", "file.conf"]).expect_err("should fail");
assert_eq!(
err.to_string(),
"error loading config: No such file or directory (os error 2)"
);
let err = parse(&["--config=file.conf"]).expect_err("should fail");
assert_eq!(
err.to_string(),
"error loading config: No such file or directory (os error 2)"
);
let err = parse(&["--config", "file.conf"]).expect_err("should fail");
assert_eq!(
err.to_string(),
"error loading config: No such file or directory (os error 2)"
);
let cfg = parse(&["-C"]).expect("should work");
assert_eq!(cfg.tls, false);
}
}