OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

続・.NETでPathをちゃんと

先日アプリケーションのStartupPathをちゃんと取得するという話を書いたのだけど、
たかがPath、されどPath、ちゃんとやろうとすると
なかなか奥が深いというか面倒くさいので、もうちょっと書きます。

けっこう長くなったけど、トピックスは2つ。
・絶対パスか相対パスかを調べる
・パスを連結する

こんなんできるわ!という人も、読んでみると知らない発見があったり
なかったりすると思うので読んでみたらどうでしょうか。

絶対パスか相対パスかを調べる

「C# 絶対パスかどうか」とかで検索すると、
System.IO.Path.IsPathRootedを使えという情報がすぐにヒットするけれど、
これの判定が本当に自分の望むものになっているかどうか、ちょっと注意が必要。

bool res1 = Path.IsPathRooted(@"C:\hoge"));
bool res2 = Path.IsPathRooted(@"hoge"));
bool res3 = Path.IsPathRooted(@"\hoge"));

上のres1はTrue、res2はFalseになる。それは良いだろう。
問題はres3だが、これはTrueになるので注意。

他のPath関係のメソッドもそうなんだけど、どうも.NETは
先頭に区切り文字があるPathは基本的にそれがルートだとみなす。
Unix系の環境で動くことも意識してのことでしょうかね。
(実際、上のコードは'\'を'/'に置き換えても同じ結果を返す)

MSDNのサンプルはこのへんちょっとずるくて、
UNC形式のパスの例はあるんだけど、上のres3みたいな例がない。
なのではまりやすいですねこれは。

じゃあ区切り文字から始まるときにFalseを返してほしいときは
どうすればいいかというと、とりあえず僕は↓こうしてます。

string path1 = @"C:\hoge";
string path2 = @"hoge";
string path3 = @"\hoge";
bool res1 = Path.GetFullPath(path1) == path1; //True
bool res2 = Path.GetFullPath(path2) == path2; //False
bool res3 = Path.GetFullPath(path3) == path3; //False

例えば上のコードを「C:\MyDir」のディレクトリで実行した場合、
Path.GetFullPath(path2)は「C:\MyDir\hoge」となる。
Path.GetFullPath(path3)は、先頭にドライブレターをつけて
「C:\hoge」が返ってくるのが謎だけどとりあえず判定には使える。

注意点として、普通にWindows環境で実行するときに
Path.GetFullPath(@"C:/hoge")とかやると
区切り文字を直されて「C:\hoge」が返ってくるので、上の判定だとFalseになる。
必ずWindows上で実行するなら判定の前にString.Replaceメソッドで'/'を'\'に
全置換しておけばいいけど、Linuxで動かすかもしれなくて、
パスもどっちの文字を指定されるかわからないとかであれば、
以下の2行の処理を挟む必要があるかな。

path = path.Replace('/', Path.DirectorySeparatorChar);
path = path.Replace('\\', Path.DirectorySeparatorChar);

ちなみにLinuxのMono上では、Path.GetFullPathは以下のようにふるまうようだ。

// ※「/home/username/」から実行
string fp1 = Path.GetFullPath(@"hoge");  // "/home/username/hoge"
string fp2 = Path.GetFullPath(@"\hoge"); // "/home/username/\hoge"
string fp3 = Path.GetFullPath(@"/hoge"); // "/hoge"

パスを連結する

例えばユーザーに任意の相対パスを入力してもらって、
それにルートをつけて絶対パスに直すとかいうケースも、
ちゃんとやろうとすると色々気をつける点がある。

要するに、先頭に区切り文字を付けられるかどうかわからんから
単純に演算子で足すときは注意が必要だという話です。

const string Root = @"C:\hoge";

string input = Console.ReadLine();  //相対パスを受け取る

//先頭が'\'でなければ付与
if (input.FirstOrDefault() != '\\') input = "\\" + input;

string fullPath = Root + input;

こんなめんどくさいことをやらないといけなかったりする。
Path.Combine使えよ、と言われるかもしれないが、
これがまた微妙にクセのある動きをするのだ。

string full1 = Path.Combine(@"C:\MyDir",  @"hoge");
string full2 = Path.Combine(@"C:\MyDir\", @"hoge");
string full3 = Path.Combine(@"C:\MyDir",  @"\hoge");
string full4 = Path.Combine(@"C:",        @"MyDir\hoge");
string full5 = Path.Combine(@"C:",        @"\MyDir\hoge");

上の例はすべて「C:\MyDir\hoge」になってほしい気がするけども、
実はそうなるのは上の2つのみ。
full3は「\hoge」になってしまう。
さっきの話と同じで、先頭の区切り文字をルートとみなすからなんだろう。

full4はというと、これはそのままくっつけた「C:MyDir\hoge」になってしまう。
ドライブレターのあとの区切り文字は付与してくれない。
さらにやっかいなことに、じゃあ後ろにつけようと思ってfull5のように書くと、
これは先頭がルートとみなされて「\MyDir\hoge」が返ってくる。

という感じでいろいろ考えると、結局最初のように演算子で足しちゃうのが
一番楽なんじゃねえかという気がしてくる。

あとはWindows以外のことも考えるのであれば、区切り文字は
Path.DirectorySeparatorCharで見ておくほうがいいだろうというくらい。

もうちょっとだけつっこんで書くと、相対パスを指定するときに
「.\hoge」みたいなURI形式の階層表現をするユーザーもいたりするので、
それも対応したい場合は、System.Uriクラスのコンストラクタを使って
連結するという手もある。

Uri baseUri = new Uri(@"C:\MyDir\");
string full1 = new Uri(baseUri, @".\hoge").LocalPath;   //"C:\MyDir\hoge"
string full2 = new Uri(baseUri, @"..\hoge").LocalPath;  //"C:\hoge"

この場合はbaseUriの末に区切り文字があるかないかで結果が変わるので注意。

以上。なげえ。