Skip to content

Commit

Permalink
feature(ln): implement -r (#1540)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
sylvestre committed Jun 18, 2020
1 parent 87af997 commit f17a112
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 10 deletions.
8 changes: 3 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 40 additions & 5 deletions src/uu/ln/src/ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "";
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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);

Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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 {
Expand All @@ -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!(),
Expand Down Expand Up @@ -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 {
Expand Down
72 changes: 72 additions & 0 deletions tests/by-util/test_ln.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

0 comments on commit f17a112

Please sign in to comment.