1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//  * This file is part of the uutils coreutils package.
//  *
//  * (c) 2014 Vsevolod Velichko <torkvemada@sorokdva.net>
//  *
//  * For the full copyright and license information, please view the LICENSE
//  * file that was distributed with this source code.

// spell-checker:ignore (ToDO) subpath absto absfrom absbase

#[macro_use]
extern crate uucore;

use clap::{App, Arg};
use std::env;
use std::path::{Path, PathBuf};
use uucore::fs::{canonicalize, CanonicalizeMode};

static VERSION: &str = env!("CARGO_PKG_VERSION");
static ABOUT: &str = "Convert TO destination to the relative path from the FROM dir.
If FROM path is omitted, current working dir will be used.";

mod options {
    pub const DIR: &str = "DIR";
    pub const TO: &str = "TO";
    pub const FROM: &str = "FROM";
}

fn get_usage() -> String {
    format!("{} [-d DIR] TO [FROM]", executable!())
}

pub fn uumain(args: impl uucore::Args) -> i32 {
    let args = args.collect_str();
    let usage = get_usage();

    let matches = App::new(executable!())
        .version(VERSION)
        .about(ABOUT)
        .usage(&usage[..])
        .arg(
            Arg::with_name(options::DIR)
                .short("d")
                .takes_value(true)
                .help("If any of FROM and TO is not subpath of DIR, output absolute path instead of relative"),
        )
        .arg(
            Arg::with_name(options::TO)
                .required(true)
                .takes_value(true),
        )
        .arg(
            Arg::with_name(options::FROM)
                .takes_value(true),
        )
        .get_matches_from(args);

    let to = Path::new(matches.value_of(options::TO).unwrap()).to_path_buf(); // required
    let from = match matches.value_of(options::FROM) {
        Some(p) => Path::new(p).to_path_buf(),
        None => env::current_dir().unwrap(),
    };
    let absto = canonicalize(to, CanonicalizeMode::Normal).unwrap();
    let absfrom = canonicalize(from, CanonicalizeMode::Normal).unwrap();

    if matches.is_present(options::DIR) {
        let base = Path::new(&matches.value_of(options::DIR).unwrap()).to_path_buf();
        let absbase = canonicalize(base, CanonicalizeMode::Normal).unwrap();
        if !absto.as_path().starts_with(absbase.as_path())
            || !absfrom.as_path().starts_with(absbase.as_path())
        {
            println!("{}", absto.display());
            return 0;
        }
    }

    let mut suffix_pos = 0;
    for (f, t) in absfrom.components().zip(absto.components()) {
        if f == t {
            suffix_pos += 1;
        } else {
            break;
        }
    }

    let mut result = PathBuf::new();
    absfrom
        .components()
        .skip(suffix_pos)
        .map(|_| result.push(".."))
        .last();
    absto
        .components()
        .skip(suffix_pos)
        .map(|x| result.push(x.as_os_str()))
        .last();

    println!("{}", result.display());
    0
}