use std::ffi::OsString;
use std::io::{IsTerminal, Write};
use std::path::{Path, PathBuf};
use cli::{Format, GlobalArgs};
use commands::shep_toml::ShepToml;
use exit::ExitCode;
use output::Streams;
use shep_core::paths::{HOME_DIR_VAR, ShepPaths, user_home};
use crate::client::emit_error_locked;
use crate::{cli, commands, exit, output, status, welcome};
#[cfg_attr(windows, allow(dead_code))]
pub(crate) async fn print_shepherd_status(argv: &[OsString]) {
if !std::io::stderr().is_terminal() || names_another_shepherd(argv) {
return;
}
let global = GlobalArgs {
host: None,
home: None,
format: Format::Table,
quiet: false,
style: None,
};
let Ok(paths) = resolve_paths(&global) else {
return;
};
let status = status::ShepherdStatus::probe(&paths).await;
let mut err = std::io::stderr();
let _ = writeln!(err, "{}", status::one_line(&status));
}
fn names_another_shepherd(argv: &[OsString]) -> bool {
argv.iter().any(|arg| {
let arg = arg.to_string_lossy();
["--home", "--host"].iter().any(|flag| {
arg.strip_prefix(flag)
.is_some_and(|rest| rest.is_empty() || rest.starts_with('='))
})
})
}
pub(crate) fn resolve_paths(global: &GlobalArgs) -> Result<ShepPaths, HomeRefusal> {
resolve_paths_in(global, &|key| std::env::var_os(key))
}
fn resolve_paths_in(
global: &GlobalArgs,
var: &dyn Fn(&str) -> Option<OsString>,
) -> Result<ShepPaths, HomeRefusal> {
if let Some(named) = global.home.as_ref() {
require_absolute(HOME_KNOB, named)?;
require_utf8(HOME_KNOB, named)?;
}
let env = |key: &str| match key {
"SHEP_HOME" => global
.home
.as_ref()
.map(|p| p.to_string_lossy().into_owned()),
other => var(other).map(|value| value.to_string_lossy().into_owned()),
};
let home_dir = match (user_home(var), env("SHEP_HOME")) {
(Some(dir), _) => dir,
(None, Some(_)) => PathBuf::new(),
(None, None) => return Err(HomeRefusal::Unresolved),
};
if global.home.is_none() {
require_absolute(HOME_DIR_VAR, &home_dir)?;
require_utf8(HOME_DIR_VAR, &home_dir)?;
}
Ok(ShepPaths::resolve(&env, &home_dir))
}
fn require_utf8(knob: &'static str, candidate: &Path) -> Result<(), HomeRefusal> {
if candidate.to_str().is_some() {
return Ok(());
}
Err(HomeRefusal::NotUtf8 {
knob,
given: candidate.to_path_buf(),
})
}
fn require_absolute(knob: &'static str, candidate: &Path) -> Result<(), HomeRefusal> {
if candidate.is_absolute() {
return Ok(());
}
Err(HomeRefusal::Relative {
knob,
absolute: absolute_form(candidate),
given: candidate.to_path_buf(),
})
}
pub(crate) fn report_home_refusal(fmt: Format, refusal: &HomeRefusal) -> ExitCode {
let code = refusal.code();
emit_error_locked(fmt, code, &refusal.to_string());
code
}
pub(crate) fn absolute_form(candidate: &Path) -> Option<PathBuf> {
std::env::current_dir().ok().map(|cwd| cwd.join(candidate))
}
#[derive(Debug)]
pub(crate) enum HomeRefusal {
Unresolved,
Relative {
knob: &'static str,
given: PathBuf,
absolute: Option<PathBuf>,
},
NotUtf8 {
knob: &'static str,
given: PathBuf,
},
Missing(PathBuf),
Io {
path: PathBuf,
source: std::io::Error,
},
}
impl core::fmt::Display for HomeRefusal {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Unresolved => f.write_str(UNRESOLVED_HOME),
Self::Relative {
knob,
given,
absolute,
} => write_relative_refusal(f, knob, given, absolute.as_deref()),
Self::NotUtf8 { knob, given } => write!(
f,
"{knob} must be valid UTF-8, and {given} is not\n \
shep carries the home path into the `{{{{SHEP_HOME}}}}` template, the log \
paths it reports, and the control socket's own name, all of which are text, \
so a byte that is not UTF-8 would be replaced and shep would use a \
directory you did not name",
given = given.display(),
),
Self::Missing(path) => write!(
f,
"no flock at {path}\n\
did you mean to drop {HOME_KNOB}? the default is {DEFAULT_HOME_SPELLING}\n\
to set up a flock there deliberately: {MKDIR_COMMAND} {quoted}",
path = one_line(path),
quoted = shell_quoted(path),
),
Self::Io { path, source } => write!(
f,
"could not create {path}: {source}",
path = one_line(path),
source = crate::terminal_safe::sanitise(&source.to_string()).0,
),
}
}
}
#[cfg(not(windows))]
fn shell_quoted(path: &Path) -> String {
format!("'{}'", one_line(path).replace('\'', r"'\''"))
}
#[cfg(windows)]
fn shell_quoted(path: &Path) -> String {
format!("\"{}\"", one_line(path))
}
fn one_line(path: &Path) -> String {
crate::terminal_safe::sanitise(&path.display().to_string()).0
}
impl core::error::Error for HomeRefusal {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
Self::Unresolved | Self::Relative { .. } | Self::NotUtf8 { .. } | Self::Missing(_) => {
None
}
Self::Io { source, .. } => Some(source),
}
}
}
impl HomeRefusal {
pub(crate) fn code(&self) -> ExitCode {
match self {
Self::Unresolved | Self::Relative { .. } | Self::NotUtf8 { .. } | Self::Missing(_) => {
ExitCode::Usage
}
Self::Io { .. } => ExitCode::Internal,
}
}
}
pub(crate) fn ensure_home(global: &GlobalArgs) -> Result<(ShepPaths, bool), HomeRefusal> {
let paths = resolve_paths(global)?;
ensure_home_at(paths, global.home.is_some())
}
fn ensure_home_at(paths: ShepPaths, explicit: bool) -> Result<(ShepPaths, bool), HomeRefusal> {
if paths.home.is_dir() {
return Ok((paths, false));
}
if explicit {
return Err(HomeRefusal::Missing(paths.home));
}
let built = {
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new()
.recursive(true)
.mode(shep_daemon::boot::DIR_MODE)
.create(&paths.home)
}
#[cfg(not(unix))]
{
std::fs::DirBuilder::new()
.recursive(true)
.create(&paths.home)
}
};
match built {
Ok(()) => Ok((paths, true)),
Err(source) => Err(HomeRefusal::Io {
path: paths.home,
source,
}),
}
}
pub(crate) fn scaffold_first_run_interpreters(paths: &ShepPaths) {
if let Err(err) = ShepToml::edit(&paths.daemon_config, ShepToml::write_starter_interpreters) {
let mut err_stream = std::io::stderr();
let _ = writeln!(
err_stream,
"could not write a starter interpreter mapping to {}: {err}",
paths.daemon_config.display()
);
}
}
#[cfg(unix)]
pub(crate) fn create_default_home(
streams: &mut Streams<'_>,
paths: ShepPaths,
) -> Result<(), HomeRefusal> {
let (paths, home_is_new) = ensure_home_at(paths, false)?;
if home_is_new {
scaffold_first_run_interpreters(&paths);
welcome::on_first_run(streams, &paths.home, std::io::stderr().is_terminal());
}
Ok(())
}
#[cfg(not(windows))]
const UNRESOLVED_HOME: &str = "none of --home, $SHEP_HOME, or $HOME resolves a root directory";
#[cfg(windows)]
const UNRESOLVED_HOME: &str =
"none of --home, %SHEP_HOME%, or %USERPROFILE% resolves a root directory";
#[cfg(not(windows))]
pub(crate) const HOME_KNOB: &str = "--home/$SHEP_HOME";
#[cfg(windows)]
pub(crate) const HOME_KNOB: &str = "--home/%SHEP_HOME%";
#[cfg(not(windows))]
const DEFAULT_HOME_SPELLING: &str = "~/.shep";
#[cfg(windows)]
const DEFAULT_HOME_SPELLING: &str = r"%USERPROFILE%\.shep";
#[cfg(not(windows))]
const MKDIR_COMMAND: &str = "mkdir -p";
#[cfg(windows)]
const MKDIR_COMMAND: &str = "mkdir";
pub(crate) fn write_relative_refusal(
f: &mut core::fmt::Formatter<'_>,
knob: &str,
given: &Path,
absolute: Option<&Path>,
) -> core::fmt::Result {
write!(
f,
"{knob} must be an absolute path, not {given}\n\
a relative home is read against whatever directory shep runs in, so the flock it \
names is reachable from that one directory and nowhere else",
given = one_line(given),
)?;
match absolute {
Some(absolute) => write!(f, "\ndid you mean: {}", one_line(absolute)),
None => Ok(()),
}
}
#[cfg(test)]
mod tests;