mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 11:37:44 +00:00
feature(ln): implement -r (#1540)
* bump the minimal version of rustc to 1.32 * feature(ln): implement -r * fix two issues * Use cow * rustfmt the change * with cargo.lock 1.31 * try to unbreak windows
This commit is contained in:
parent
87af997c7a
commit
f17a112781
3 changed files with 115 additions and 10 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -1,5 +1,3 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "advapi32-sys"
|
name = "advapi32-sys"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
@ -171,7 +169,7 @@ dependencies = [
|
||||||
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
"time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unindent 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unindent 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"uu_arch 0.0.1",
|
"uu_arch 0.0.1",
|
||||||
|
@ -1078,7 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unindent"
|
name = "unindent"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2284,7 +2282,7 @@ dependencies = [
|
||||||
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
|
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
|
||||||
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
|
||||||
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
|
||||||
"checksum unindent 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993"
|
"checksum unindent 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "af41d708427f8fd0e915dcebb2cae0f0e6acb2a939b2d399c265c39a38a18942"
|
||||||
"checksum unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564"
|
"checksum unix_socket 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6aa2700417c405c38f5e6902d699345241c28c0b7ade4abaad71e35a87eb1564"
|
||||||
"checksum users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
"checksum users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4227e95324a443c9fcb06e03d4d85e91aabe9a5a02aa818688b6918b6af486"
|
||||||
"checksum uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)" = "<none>"
|
"checksum uucore 0.0.4 (git+https://github.com/uutils/uucore.git?branch=canary)" = "<none>"
|
||||||
|
|
|
@ -10,13 +10,17 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate uucore;
|
extern crate uucore;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use std::io::{stdin, Result};
|
use std::io::{stdin, Result};
|
||||||
#[cfg(any(unix, target_os = "redox"))]
|
#[cfg(any(unix, target_os = "redox"))]
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::os::windows::fs::{symlink_dir, symlink_file};
|
use std::os::windows::fs::{symlink_dir, symlink_file};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use uucore::fs::{canonicalize, CanonicalizeMode};
|
||||||
|
|
||||||
static NAME: &str = "ln";
|
static NAME: &str = "ln";
|
||||||
static SUMMARY: &str = "";
|
static SUMMARY: &str = "";
|
||||||
|
@ -36,6 +40,7 @@ pub struct Settings {
|
||||||
backup: BackupMode,
|
backup: BackupMode,
|
||||||
suffix: String,
|
suffix: String,
|
||||||
symbolic: bool,
|
symbolic: bool,
|
||||||
|
relative: bool,
|
||||||
target_dir: Option<String>,
|
target_dir: Option<String>,
|
||||||
no_target_dir: bool,
|
no_target_dir: bool,
|
||||||
verbose: bool,
|
verbose: bool,
|
||||||
|
@ -92,7 +97,6 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
// TODO: opts.optflag("n", "no-dereference", "treat LINK_NAME as a normal file if it is a \
|
// TODO: opts.optflag("n", "no-dereference", "treat LINK_NAME as a normal file if it is a \
|
||||||
// symbolic link to a directory");
|
// symbolic link to a directory");
|
||||||
// TODO: opts.optflag("P", "physical", "make hard links directly to symbolic links");
|
// TODO: opts.optflag("P", "physical", "make hard links directly to symbolic links");
|
||||||
// TODO: opts.optflag("r", "relative", "create symbolic links relative to link location");
|
|
||||||
.optflag("s", "symbolic", "make symbolic links instead of hard links")
|
.optflag("s", "symbolic", "make symbolic links instead of hard links")
|
||||||
.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX")
|
.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX")
|
||||||
.optopt(
|
.optopt(
|
||||||
|
@ -106,6 +110,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
"no-target-directory",
|
"no-target-directory",
|
||||||
"treat LINK_NAME as a normal file always",
|
"treat LINK_NAME as a normal file always",
|
||||||
)
|
)
|
||||||
|
.optflag(
|
||||||
|
"r",
|
||||||
|
"relative",
|
||||||
|
"create symbolic links relative to link location",
|
||||||
|
)
|
||||||
.optflag("v", "verbose", "print name of each linked file")
|
.optflag("v", "verbose", "print name of each linked file")
|
||||||
.parse(args);
|
.parse(args);
|
||||||
|
|
||||||
|
@ -168,6 +177,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
|
||||||
backup: backup_mode,
|
backup: backup_mode,
|
||||||
suffix: backup_suffix,
|
suffix: backup_suffix,
|
||||||
symbolic: matches.opt_present("s"),
|
symbolic: matches.opt_present("s"),
|
||||||
|
relative: matches.opt_present("r"),
|
||||||
target_dir: matches.opt_str("t"),
|
target_dir: matches.opt_str("t"),
|
||||||
no_target_dir: matches.opt_present("T"),
|
no_target_dir: matches.opt_present("T"),
|
||||||
verbose: matches.opt_present("v"),
|
verbose: matches.opt_present("v"),
|
||||||
|
@ -279,8 +289,33 @@ fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Setting
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn relative_path<'a>(src: &PathBuf, dst: &PathBuf) -> Result<Cow<'a, Path>> {
|
||||||
|
let abssrc = canonicalize(src, CanonicalizeMode::Normal)?;
|
||||||
|
let absdst = canonicalize(dst, CanonicalizeMode::Normal)?;
|
||||||
|
let suffix_pos = abssrc
|
||||||
|
.components()
|
||||||
|
.zip(absdst.components())
|
||||||
|
.take_while(|(s, d)| s == d)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
let srciter = abssrc.components().skip(suffix_pos).map(|x| x.as_os_str());
|
||||||
|
|
||||||
|
let result: PathBuf = absdst
|
||||||
|
.components()
|
||||||
|
.skip(suffix_pos + 1)
|
||||||
|
.map(|_| OsStr::new(".."))
|
||||||
|
.chain(srciter)
|
||||||
|
.collect();
|
||||||
|
Ok(result.into())
|
||||||
|
}
|
||||||
|
|
||||||
fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> {
|
fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> {
|
||||||
let mut backup_path = None;
|
let mut backup_path = None;
|
||||||
|
let source: Cow<'_, Path> = if settings.relative {
|
||||||
|
relative_path(&src, dst)?
|
||||||
|
} else {
|
||||||
|
src.into()
|
||||||
|
};
|
||||||
|
|
||||||
if is_symlink(dst) || dst.exists() {
|
if is_symlink(dst) || dst.exists() {
|
||||||
match settings.overwrite {
|
match settings.overwrite {
|
||||||
|
@ -307,13 +342,13 @@ fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.symbolic {
|
if settings.symbolic {
|
||||||
symlink(src, dst)?;
|
symlink(&source, dst)?;
|
||||||
} else {
|
} else {
|
||||||
fs::hard_link(src, dst)?;
|
fs::hard_link(&source, dst)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if settings.verbose {
|
if settings.verbose {
|
||||||
print!("'{}' -> '{}'", dst.display(), src.display());
|
print!("'{}' -> '{}'", dst.display(), &source.display());
|
||||||
match backup_path {
|
match backup_path {
|
||||||
Some(path) => println!(" (backup: '{}')", path.display()),
|
Some(path) => println!(" (backup: '{}')", path.display()),
|
||||||
None => println!(),
|
None => println!(),
|
||||||
|
@ -359,7 +394,7 @@ fn existing_backup_path(path: &PathBuf, suffix: &str) -> PathBuf {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn symlink<P: AsRef<Path>>(src: P, dst: P) -> Result<()> {
|
pub fn symlink<P1: AsRef<Path>, P2: AsRef<Path>>(src: P1, dst: P2) -> Result<()> {
|
||||||
if src.as_ref().is_dir() {
|
if src.as_ref().is_dir() {
|
||||||
symlink_dir(src, dst)
|
symlink_dir(src, dst)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -416,3 +416,75 @@ fn test_symlink_missing_destination() {
|
||||||
file
|
file
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symlink_relative() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let file_a = "test_symlink_relative_a";
|
||||||
|
let link = "test_symlink_relative_link";
|
||||||
|
|
||||||
|
at.touch(file_a);
|
||||||
|
|
||||||
|
// relative symlink
|
||||||
|
ucmd.args(&["-r", "-s", file_a, link]).succeeds();
|
||||||
|
assert!(at.is_symlink(link));
|
||||||
|
assert_eq!(at.resolve_link(link), file_a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hardlink_relative() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let file_a = "test_hardlink_relative_a";
|
||||||
|
let link = "test_hardlink_relative_link";
|
||||||
|
|
||||||
|
at.touch(file_a);
|
||||||
|
|
||||||
|
// relative hardlink
|
||||||
|
ucmd.args(&["-r", "-v", file_a, link])
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("'{}' -> '{}'\n", link, file_a));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symlink_relative_path() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
let dir = "test_symlink_existing_dir";
|
||||||
|
let file_a = "test_symlink_relative_a";
|
||||||
|
let link = "test_symlink_relative_link";
|
||||||
|
let multi_dir =
|
||||||
|
"test_symlink_existing_dir/../test_symlink_existing_dir/../test_symlink_existing_dir/../";
|
||||||
|
let p = PathBuf::from(multi_dir).join(file_a);
|
||||||
|
at.mkdir(dir);
|
||||||
|
|
||||||
|
// relative symlink
|
||||||
|
// Thanks to -r, all the ../ should be resolved to a single file
|
||||||
|
ucmd.args(&["-r", "-s", "-v", &p.to_string_lossy(), link])
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("'{}' -> '{}'\n", link, file_a));
|
||||||
|
assert!(at.is_symlink(link));
|
||||||
|
assert_eq!(at.resolve_link(link), file_a);
|
||||||
|
|
||||||
|
// Run the same command without -r to verify that we keep the full
|
||||||
|
// crazy path
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
ucmd.args(&["-s", "-v", &p.to_string_lossy(), link])
|
||||||
|
.succeeds()
|
||||||
|
.stdout_only(format!("'{}' -> '{}'\n", link, &p.to_string_lossy()));
|
||||||
|
assert!(at.is_symlink(link));
|
||||||
|
assert_eq!(at.resolve_link(link), p.to_string_lossy());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_symlink_relative_dir() {
|
||||||
|
let (at, mut ucmd) = at_and_ucmd!();
|
||||||
|
|
||||||
|
let dir = "test_symlink_existing_dir";
|
||||||
|
let link = "test_symlink_existing_dir_link";
|
||||||
|
|
||||||
|
at.mkdir(dir);
|
||||||
|
|
||||||
|
ucmd.args(&["-s", "-r", dir, link]).succeeds().no_stderr();
|
||||||
|
assert!(at.dir_exists(dir));
|
||||||
|
assert!(at.is_symlink(link));
|
||||||
|
assert_eq!(at.resolve_link(link), dir);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue