フルパスから相対パスを求める

お題:フルパスから相対パスを求める - No Programming, No Life からインスパイヤされて、フルパスから相対パスへの変換を行います。

方法はいろいろあると思いますが、ここでは共通部分を見つけて、それを除いた状態からディレクトリをさかのぼる方法を取っています。他の解法もあると思いますので、いろいろな方法で試してみてはどうでしょうか。

#! /usr/local/bin/nawk -f
# abspath2relpath.awk
# usage: nawk -f abspath2relpath.awk path1 path2
# フルパスから相対パスを求める。

BEGIN {
    path1 = ARGV[1];
    path2 = ARGV[2];

    print abspath2relpath(path1, path2);
}

# abspath2relpath():    フルパスから相対パスを返す
#   in:     フルパス path1, path2
#   out:    フルパス path1 からみた path2 の相対パス
function abspath2relpath(path1, path2,
            depth1, depth2, same_depth, same_path, add_rel_path) {
    # 空の場合
    if (path1 == "" || path2 == "") {
        print "エラー" > "/dev/stderr";
        exit;
    }

    # 使用不可能な文字
    if (path1 ~ /[\\?*:"<>]/ || path2 ~ /[\\?*:"<>]/) {
        print "エラー" > "/dev/stderr";
        exit;
    }

    # / 以外で始まる場合
    if (path1 !~ /^\// || path2 !~ /^\//) {
        print "エラー" > "/dev/stderr";
        exit;
    }

    # 複数の / は / ひとつ
    gsub(/\/+/, "/", path1);
    gsub(/\/+/, "/", path2);

    depth1 = split(path1, arr_path1, /\//);
    depth2 = split(path2, arr_path2, /\//);
    # path1 と path2 が同じ場合
    if (path1 == path2) {
        return "./" arr_path2[depth2];
    }

    # フルパスから相対パスへの変換
    for (i = 1; i <= depth1; i++) {
        if (arr_path1[i] != arr_path2[i]) {
            break;
        }
        same_depth++;
    }

    for (i = 1; i <= same_depth; i++) {
        same_path = same_path "/" arr_path1[i];
    }
    sub(/^\//, "", same_path);
    same_path = same_path "/";

    # 共通の部分を除く
    sub(same_path, "", path1);
    sub(same_path, "", path2);

    depth1 = split(path1, arr_path1, /\//);
    depth2 = split(path2, arr_path2, /\//);

    add_rel_path = "./";
    for (i = 1; i <= depth1 - 1; i++) {
        add_rel_path = add_rel_path "../";
    }

    if (add_rel_path ~ /^\.\/\.\.\//) {
        sub(/^\.\/\.\.\//, "../", add_rel_path);
    }

    return add_rel_path path2;
}

美しくないところとしては 2 つのフルパスが全く同じ場合の処理を例外として扱っているところでしょうか。

実行にあたって、Makefile を作って実行します。 awk のようにテスト環境がない場合には Makefile を活用することで自動化することができます。 ここでは答え合わせまでは自動化していませんが、gawk のソースの test/Makefile などが参考になると思います。

AWK = gawk

all:

        @echo '同じディレクトリにあるファイル'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/bbb/to.txt
        @echo '親ディレクトリにあるファイル'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/to.txt
        @echo '子ディレクトリにあるファイル'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/bbb/ccc/to.txt
        @echo '親の子の子にあるファイル'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/ccc/ddd/to.txt
        @echo 'ルート越え'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from.txt /ddd/ccc/to.txt
        @echo 'ディレクトリからファイル'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/ /aaa/ddd/to
        @echo 'ファイルからディレクトリ'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/from /aaa/ccc/
        @echo 'ディレクトリからディレクトリ'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/ /aaa/ccc/
        @echo '同じパス'
        $(AWK) -f abspath2relpath.awk /aaa/bbb/ccc.txt /aaa/bbb/ccc.txt
        @echo '空'
        $(AWK) -f abspath2relpath.awk '' /bbb/to.txt
        @echo '使用不可能な文字'
        $(AWK) -f abspath2relpath.awk '/aaa/g*' /bbb/to.txt
        @echo '/ 以外で始まる場合はエラー'
        $(AWK) -f abspath2relpath.awk aaa/bbb/from.txt ./bbb/to.txt
        @echo '複数の / は / 一つ'
        $(AWK) -f abspath2relpath.awk //aaa///bbb////from.txt /////aaa//////bbb///////to.txt

では実行してみましょう。

$ make
同じディレクトリにあるファイル
gawk -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/bbb/to.txt
./to.txt
親ディレクトリにあるファイル
gawk -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/to.txt
../to.txt
子ディレクトリにあるファイル
gawk -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/bbb/ccc/to.txt
./ccc/to.txt
親の子の子にあるファイル
gawk -f abspath2relpath.awk /aaa/bbb/from.txt /aaa/ccc/ddd/to.txt
../ccc/ddd/to.txt
ルート越え
gawk -f abspath2relpath.awk /aaa/bbb/from.txt /ddd/ccc/to.txt
../../ddd/ccc/to.txt
ディレクトリからファイル
gawk -f abspath2relpath.awk /aaa/bbb/ /aaa/ddd/to
../ddd/to
ファイルからディレクトリ
gawk -f abspath2relpath.awk /aaa/bbb/from /aaa/ccc/
../ccc/
ディレクトリからディレクトリ
gawk -f abspath2relpath.awk /aaa/bbb/ /aaa/ccc/
../ccc/
同じパス
gawk -f abspath2relpath.awk /aaa/bbb/ccc.txt /aaa/bbb/ccc.txt
./ccc.txt
空
gawk -f abspath2relpath.awk '' /bbb/to.txt
エラー
使用不可能な文字
gawk -f abspath2relpath.awk '/aaa/g*' /bbb/to.txt
エラー
/ 以外で始まる場合はエラー
gawk -f abspath2relpath.awk aaa/bbb/from.txt ./bbb/to.txt
エラー
複数の / は / 一つ
gawk -f abspath2relpath.awk //aaa///bbb////from.txt /////aaa//////bbb///////to.txt
./to.txt

tag_nawk.png tag_nawk.png tag_nawk.png tag_nawk.png