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:
Sylvestre Ledru 2020-06-18 09:54:18 +02:00 committed by GitHub
parent 87af997c7a
commit f17a112781
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 115 additions and 10 deletions

8
Cargo.lock generated
View file

@ -1,5 +1,3 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "advapi32-sys"
version = "0.2.0"
@ -171,7 +169,7 @@ dependencies = [
"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)",
"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)",
"users 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
"uu_arch 0.0.1",
@ -1078,7 +1076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unindent"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -2284,7 +2282,7 @@ dependencies = [
"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.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 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>"

View file

@ -10,13 +10,17 @@
#[macro_use]
extern crate uucore;
use std::borrow::Cow;
use std::ffi::OsStr;
use std::fs;
use std::io::{stdin, Result};
#[cfg(any(unix, target_os = "redox"))]
use std::os::unix::fs::symlink;
#[cfg(windows)]
use std::os::windows::fs::{symlink_dir, symlink_file};
use std::path::{Path, PathBuf};
use uucore::fs::{canonicalize, CanonicalizeMode};
static NAME: &str = "ln";
static SUMMARY: &str = "";
@ -36,6 +40,7 @@ pub struct Settings {
backup: BackupMode,
suffix: String,
symbolic: bool,
relative: bool,
target_dir: Option<String>,
no_target_dir: 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 \
// symbolic link to a directory");
// 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")
.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX")
.optopt(
@ -106,6 +110,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
"no-target-directory",
"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")
.parse(args);
@ -168,6 +177,7 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
backup: backup_mode,
suffix: backup_suffix,
symbolic: matches.opt_present("s"),
relative: matches.opt_present("r"),
target_dir: matches.opt_str("t"),
no_target_dir: matches.opt_present("T"),
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<()> {
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() {
match settings.overwrite {
@ -307,13 +342,13 @@ fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> {
}
if settings.symbolic {
symlink(src, dst)?;
symlink(&source, dst)?;
} else {
fs::hard_link(src, dst)?;
fs::hard_link(&source, dst)?;
}
if settings.verbose {
print!("'{}' -> '{}'", dst.display(), src.display());
print!("'{}' -> '{}'", dst.display(), &source.display());
match backup_path {
Some(path) => println!(" (backup: '{}')", path.display()),
None => println!(),
@ -359,7 +394,7 @@ fn existing_backup_path(path: &PathBuf, suffix: &str) -> PathBuf {
}
#[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() {
symlink_dir(src, dst)
} else {

View file

@ -416,3 +416,75 @@ fn test_symlink_missing_destination() {
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);
}