use std::{
ffi::{OsStr, OsString},
mem::take,
path::{Path, PathBuf},
};
use clap::{
builder::BoolishValueParser,
builder::TypedValueParser,
error::{Error, ErrorKind},
Parser, ValueEnum, ValueHint,
};
use miette::{IntoDiagnostic, Result};
use tracing::{info, warn};
use watchexec_signals::Signal;
use crate::socket::{SocketSpec, SocketSpecValueParser};
use super::{TimeSpan, OPTSET_COMMAND};
const fn get_platform_default_quoting() -> bool {
#[cfg(windows)]
{
false
}
#[cfg(unix)]
{
true
}
}
#[derive(Debug, Clone, Parser)]
pub struct CommandArgs {
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SHELL",
display_order = 190,
)]
pub shell: Option<String>,
#[arg(
short = 'n',
help_heading = OPTSET_COMMAND,
display_order = 140,
)]
pub no_shell: bool,
#[arg(
long,
help_heading = OPTSET_COMMAND,
hide = true, // deprecated
)]
pub no_environment: bool,
#[arg(
long,
short = 'E',
help_heading = OPTSET_COMMAND,
value_name = "KEY=VALUE",
value_parser = EnvVarValueParser,
display_order = 50,
)]
pub env: Vec<EnvVar>,
#[arg(
long,
help_heading = OPTSET_COMMAND,
display_order = 141,
)]
pub no_process_group: bool,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "MODE",
default_value = WRAP_DEFAULT,
display_order = 231,
)]
pub wrap_process: WrapMode,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "SIGNAL",
display_order = 191,
)]
pub stop_signal: Option<Signal>,
#[arg(
long,
help_heading = OPTSET_COMMAND,
default_value = "10s",
hide_default_value = true,
value_name = "TIMEOUT",
display_order = 192,
)]
pub stop_timeout: TimeSpan,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "TIMEOUT",
display_order = 193,
)]
pub timeout: Option<TimeSpan>,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "DURATION",
display_order = 40,
)]
pub delay_run: Option<TimeSpan>,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_hint = ValueHint::DirPath,
value_name = "DIRECTORY",
display_order = 230,
)]
pub workdir: Option<PathBuf>,
#[arg(
long,
help_heading = OPTSET_COMMAND,
value_name = "PORT",
value_parser = SocketSpecValueParser,
display_order = 60,
)]
pub socket: Vec<SocketSpec>,
#[arg(
long,
short = 'Q',
help_heading = OPTSET_COMMAND,
default_value_t = get_platform_default_quoting(),
value_parser = BoolishValueParser::new(),
display_order = 170,
)]
pub quote: bool,
}
impl CommandArgs {
pub(crate) async fn normalise(&mut self) -> Result<()> {
if self.no_process_group {
warn!("--no-process-group is deprecated");
self.wrap_process = WrapMode::None;
}
let workdir = if let Some(w) = take(&mut self.workdir) {
w
} else {
let curdir = std::env::current_dir().into_diagnostic()?;
let canonical = dunce::canonicalize(&curdir).into_diagnostic()?;
prefer_non_verbatim(canonical, curdir)
};
info!(path=?workdir, "effective working directory");
self.workdir = Some(workdir);
debug_assert!(self.workdir.is_some());
Ok(())
}
}
fn prefer_non_verbatim(canonical: PathBuf, original: PathBuf) -> PathBuf {
if is_verbatim(&canonical) && !is_verbatim(&original) {
original
} else {
canonical
}
}
fn is_verbatim(path: &Path) -> bool {
path.as_os_str().as_encoded_bytes().starts_with(br"\\?\")
}
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum WrapMode {
#[default]
Group,
Session,
None,
}
pub const WRAP_DEFAULT: &str = if cfg!(target_os = "macos") {
"session"
} else {
"group"
};
#[derive(Clone, Debug)]
pub struct EnvVar {
pub key: String,
pub value: OsString,
}
#[derive(Clone)]
pub(crate) struct EnvVarValueParser;
impl TypedValueParser for EnvVarValueParser {
type Value = EnvVar;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &OsStr,
) -> Result<Self::Value, Error> {
let value = value
.to_str()
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "invalid UTF-8"))?;
let (key, value) = value
.split_once('=')
.ok_or_else(|| Error::raw(ErrorKind::ValueValidation, "missing = separator"))?;
Ok(EnvVar {
key: key.into(),
value: value.into(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keeps_uncanonicalised_path_when_canonical_is_verbatim_unc() {
assert_eq!(
prefer_non_verbatim(
PathBuf::from(r"\\?\UNC\Mac\my-directory"),
PathBuf::from(r"Z:\my-directory"),
),
PathBuf::from(r"Z:\my-directory"),
);
}
#[test]
fn keeps_canonical_path_when_it_is_not_verbatim() {
assert_eq!(
prefer_non_verbatim(PathBuf::from(r"Z:\real"), PathBuf::from(r"Z:\link")),
PathBuf::from(r"Z:\real"),
);
}
#[test]
fn keeps_canonical_path_when_original_is_verbatim_too() {
assert_eq!(
prefer_non_verbatim(
PathBuf::from(r"\\?\UNC\Mac\my-directory"),
PathBuf::from(r"\\?\UNC\Mac\other"),
),
PathBuf::from(r"\\?\UNC\Mac\my-directory"),
);
}
}