ベースファイルからの相対パスを、ドットの連続「..」を含めて解決する

HTMLなどで、リンクは自身のパスからの相対パスで記述することが多いです。

たとえばEPUBリーダーを作っているなら、「OPS/xhtml/0001.xhtml」に含まれる画像やリンクは

<img src="../image/cover.jpg">
→ OPS/image/cover.jpg

<a href="./0002.xhtml">
→ OPS/xhtml/0002.xhtml

のように解釈する必要があります。
もちろん、EPUBファイルはファイルシステムに展開せず、ZIPのまま扱いたいですね。

こんな処理を自前で書いても良いことはないので、なるべく標準ライブラリに近いものを使わせて頂きましょう。

Ruby

真っ先に思いつくのはFile.expand_pathですが、これはあくまで実ファイル用です。「0002.xhtml」という文字列を渡したら、カレントディレクトリからの相対パスとして解釈されてしまい、ベースファイルパスを指定できません。

URI.joinは近いことができますが、scheme指定が必要なので使いづらいです。

この場合、Pathname.cleanpathを使いましょう。

require 'pathname'
base = Pathname.new('a/b/c/../d/./../e/f.txt')

base.cleanpath.to_s
=> "a/b/e/f.txt"

Pathname.new(base.parent + "../g.html").cleanpath.to_s
=> "a/b/g.html"

Pathname.new(base.parent + "/root.html").cleanpath.to_s
=> "/root.html"

シンプルで使いやすいですね。

ちなみに、Pathname.realpathは、cleanpathの動作に加えてシンボリックリンクの解決や実ファイルの存在チェックをします。

Java

AndroidでJavaを使って処理する場合はURIUtilsが便利です。

public static String resolvePath(String base, String reference) 
    throws URISyntaxException {
    URI context = new URI(base);
    return URIUtils.resolve(context, reference).toString();
}
// 挙動はJUnitで言うとこうなります
assertEquals("/", StringUtil.resolvePath("/", ""));

assertEquals("index.html", StringUtil.resolvePath("", "index.html"));
assertEquals("index.html", StringUtil.resolvePath("", "./index.html"));
assertEquals("index.html", StringUtil.resolvePath("", "../index.html"));
assertEquals("index.html", StringUtil.resolvePath("hoge.xml", "index.html"));
assertEquals("index.html", StringUtil.resolvePath("abc/def/hoge.xml", "../../index.html"));

assertEquals("/index.html", StringUtil.resolvePath("/", "index.html"));
assertEquals("/index.html", StringUtil.resolvePath("/hoge.xml", "index.html"));
assertEquals("/index.html", StringUtil.resolvePath("/abc/def/hoge.xml", "/index.html"));

assertEquals("abc/def/index.html", StringUtil.resolvePath("", "abc/def/index.html"));
assertEquals("abc/def/index.html", StringUtil.resolvePath("abc/def/ghi/hoge.xml", "../index.html"));
assertEquals("abc/def/index.html", StringUtil.resolvePath("abc/def/", "index.html"));

// ちょっと注意
assertEquals("index.html", StringUtil.resolvePath("/", "../index.html"));
assertEquals("index.html", StringUtil.resolvePath("/hoge.xml", "../index.html"));

C

libxml2のxmlBuildURIが便利です。

#include <stdio.h>
#include <libxml/uri.h>

int main() {
    xmlChar *base = (xmlChar *)"abc/def/index.xml";
    printf("%s\n", xmlBuildURI((xmlChar *)("../hoge.html"), base));
}
Ruby on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

baba

ゆとりプログラマー。 高校時代から趣味でプログラミングを初め、そのままコードを書き続けて現在に至る。慶應義塾大学環境情報学部(SFC)卒業。BPS設立初期に在学中から参加している最古参メンバーの一人。得意分野はWeb全般、Ruby on Rails、Androidアプリケーションなど。最近はBlinkと格闘中。軽度の資格マニアで、情報処理技術者試験(高度10区分)などを保有。

babaの書いた記事

週刊Railsウォッチ

インフラ

Rubyスタイルガイドを読む

BigBinary記事より

ActiveSupport探訪シリーズ