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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//  * 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 std::env;
use std::path::{Path, PathBuf};
use uucore::fs::{canonicalize, CanonicalizeMode};

static NAME: &str = "relpath";
static VERSION: &str = env!("CARGO_PKG_VERSION");

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

    let mut opts = getopts::Options::new();

    opts.optflag("h", "help", "Show help and exit");
    opts.optflag("V", "version", "Show version and exit");
    opts.optopt(
        "d",
        "",
        "If any of FROM and TO is not subpath of DIR, output absolute path instead of relative",
        "DIR",
    );

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            show_error!("{}", f);
            show_usage(&opts);
            return 1;
        }
    };

    if matches.opt_present("V") {
        version();
        return 0;
    }
    if matches.opt_present("h") {
        show_usage(&opts);
        return 0;
    }

    if matches.free.is_empty() {
        show_error!("Missing operand: TO");
        println!("Try `{} --help` for more information.", NAME);
        return 1;
    }

    let to = Path::new(&matches.free[0]);
    let from = if matches.free.len() > 1 {
        Path::new(&matches.free[1]).to_path_buf()
    } else {
        env::current_dir().unwrap()
    };
    let absto = canonicalize(to, CanonicalizeMode::Normal).unwrap();
    let absfrom = canonicalize(from, CanonicalizeMode::Normal).unwrap();

    if matches.opt_present("d") {
        let base = Path::new(&matches.opt_str("d").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
}

fn version() {
    println!("{} {}", NAME, VERSION)
}

fn show_usage(opts: &getopts::Options) {
    version();
    println!();
    println!("Usage:");
    println!("  {} [-d DIR] TO [FROM]", NAME);
    println!("  {} -V|--version", NAME);
    println!("  {} -h|--help", NAME);
    println!();
    print!(
        "{}",
        opts.usage(
            "Convert TO destination to the relative path from the FROM dir.\n\
             If FROM path is omitted, current working dir will be used."
        )
    );
}