From f17a1127813e8a40a0a6069d420490d8d3645b1a Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Thu, 18 Jun 2020 09:54:18 +0200 Subject: [PATCH] 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 --- Cargo.lock | 8 ++--- src/uu/ln/src/ln.rs | 45 ++++++++++++++++++++++--- tests/by-util/test_ln.rs | 72 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8af4d39b0..0adfae28d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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)" = "" diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index a82b9b3b5..078b9c2ea 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -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, 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> { + 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>(src: P, dst: P) -> Result<()> { +pub fn symlink, P2: AsRef>(src: P1, dst: P2) -> Result<()> { if src.as_ref().is_dir() { symlink_dir(src, dst) } else { diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 5ea81579a..a84927c4c 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -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); +}