shitbox/corebox/commands/ln/mod.rs
Nathan Fisher d248f7d17b Remove Default constraint on Cmd trait and begin removing
derive(Default) from applets
2023-04-17 23:27:57 -04:00

130 lines
4.0 KiB
Rust

use super::Cmd;
use clap::{Arg, ArgAction, Command};
use std::{error::Error, fs, io, os, path::PathBuf};
#[derive(Debug)]
pub struct Ln;
impl Cmd for Ln {
fn cli(&self) -> clap::Command {
Command::new("ln")
.about("make links between files")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.args([
Arg::new("force")
.help("remove existing destination files")
.short('f')
.long("force")
.action(ArgAction::SetTrue),
Arg::new("symbolic")
.help("make symbolic links instead of hard links")
.short('s')
.long("symbolic")
.action(ArgAction::SetTrue),
Arg::new("verbose")
.help("print name of each linked file")
.short('v')
.long("verbose")
.action(ArgAction::SetTrue),
Arg::new("to-target")
.help(
"For each source_file operand that names a file of type \
symbolic link, create a (hard) link to the file referenced \
by the symbolic link.",
)
.short('L')
.overrides_with("to-link")
.action(ArgAction::SetTrue),
Arg::new("to-link")
.help(
"For each source_file operand that names a file of type \
symbolic link, create a (hard) link to the symbolic link itself.",
)
.short('P')
.conflicts_with("to-target")
.action(ArgAction::SetTrue),
Arg::new("file")
.value_name("source")
.num_args(1..)
.required(true),
Arg::new("dest").num_args(1).required(true),
])
}
fn run(&self, matches: &clap::ArgMatches) -> Result<(), Box<dyn std::error::Error>> {
let files = matches
.get_many::<String>("file")
.unwrap()
.collect::<Vec<_>>();
let dest = matches.get_one::<String>("dest").unwrap();
let dest = PathBuf::from(dest);
let sym_target = if matches.get_flag("to-target") {
SymTarget::ToTarget
} else {
SymTarget::ToLink
};
if files.len() > 1 && !dest.is_dir() {
return Err(
io::Error::new(io::ErrorKind::Other, "destination must be a directory").into(),
);
}
for f in files {
let source = PathBuf::from(f);
do_link(
source,
dest.clone(),
sym_target,
matches.get_flag("force"),
matches.get_flag("symbolic"),
matches.get_flag("verbose"),
)?;
}
Ok(())
}
fn path(&self) -> Option<shitbox::Path> {
Some(shitbox::Path::Bin)
}
}
#[derive(Clone, Copy, PartialEq)]
enum SymTarget {
ToTarget,
ToLink,
}
fn do_link(
source: PathBuf,
mut dest: PathBuf,
sym_target: SymTarget,
force: bool,
symbolic: bool,
verbose: bool,
) -> Result<(), Box<dyn Error>> {
let source = if source.is_symlink() && sym_target == SymTarget::ToTarget {
fs::canonicalize(&source)?
} else {
source
};
if dest.is_dir() {
let name = source.file_name().unwrap();
dest.push(name);
}
if dest.exists() && force {
unistd::unlink(
dest.to_str()
.ok_or(io::Error::new(io::ErrorKind::Other, "invalid utf8"))?,
)?;
}
if symbolic {
os::unix::fs::symlink(&source, &dest)?;
} else {
fs::hard_link(&source, &dest)?;
}
if verbose {
println!("{} -> {}", dest.display(), source.display());
}
Ok(())
}