1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

cdコマンドで「 - 」という名前のディレクトリに入りたい!

Posted at

はじめに

みなさん普段シェルを扱う中で、- (ハイフン, ダッシュ) という文字はよく使うと思います。その主な用途は以下の2つではないでしょうか。

  • オプションとして
  • 引数として (% cd -% git checkout -など)

本記事は、2点目の「引数としての-」について、「じゃあ-という名前のディレクトリがあったとき、% cd -で入れるのか?」という、しょうもない疑問の調査結果を書き留めたものになります。

% cd -とは

cdコマンドの引数に-を渡すと、直前にいたディレクトリへ移動することができます。

使用例 (mac OSのZshでの実行結果)
% pwd
~/hoge/fuga/hogehoge/fugafuga

% cd ~/tmp
% ...   # 何か作業をする

% cd -  # 直前にいた`~/hoge/fuga/hogehoge/fugafuga`に戻る
% pwd
~/hoge/fuga/hogehoge/fugafuga

直接繋がったディレクトリを行き来する分には、% cd ..やTAB補完を使えば事足ります。しかし上の例のように、

  • 一時的に遠く離れたディレクトリで作業 -> 戻る

といった際、移動先の長いパスを書かなくてもハイフンですぐに戻ることができます。

-を引数として渡せるコマンドは、cdコマンドだけではありません。例えばgitsuは、-を使う機会が多い部類なのではないかと思います。検索してみたら-に関するQiitaの記事もあり、面白かったです。

-ディレクトリに入りたい!

そんな便利な% cd -ですが、その存在は意外と知られていないのではないでしょうか。
そして、もしも-の意味を知らない人が% cd -を見たら、「-という名前のディレクトリに入ろうとしているのかな?」と思うに違いありません:triumph:

では、実際に-という名前のディレクトリに移動したいとき、cdコマンドにどんな引数を渡せばいいのでしょうか?

% pwd
~/tmp

% mkdir hoge
% cd hoge
% pwd
~/tmp/hoge

% mkdir -  # エスケープしてもしなくても、`-`というディレクトリが作られる
% ls
-

上記のように、カレントディレクトリに-というディレクトリがあり、直前には~/tmpというパスにいた状態で、いくつかのコマンドを試してみます。

cd -

まずは主役のこちら。

% cd -
% pwd
~/tmp

-はディレクトリとしては認識してもらえず、直前にいた~/tmpへ移動してしまいました。

cd \-

では、とりあえず\でエスケープしてみたらどうなるでしょうか?

% cd \-
% pwd
~/tmp

変わらず、-はディレクトリとしては認識してもらえませんでした。

cd "-"

grepコマンドとかでは、引数の文字列にメタ文字が含まれていても、ダブルクオーテーションで囲んであげればそのまま受け取ってもらえます。

% cd "-"
% pwd
~/tmp

しかし-はディレクトリとしては認識してもらえず...
どうやら-はメタ文字という訳ではなさそうです。

cd "\-"

やけくそです。

% cd "\-"
cd: no such file or directory: \-

だめですよね...

cd ./-

では、ちゃんとカレントディレクトリからパスを指定してあげましょう。

% cd ./-
% pwd
~/tmp/hoge/-

お、成功しました🎉

-って何者?

調べてみると、おそらく-Zshのメタ文字でも、正規表現のメタ文字1でもないみたいです。なのでエスケープしても特に効果がないのは当たり前でした...

この-を処理するのは、各コマンドの引数parserになります。つまり、引数-に対する挙動は各コマンドの実装次第で異なるということです。引数をパースする際、大抵の場合は1文字目から順番に見て処理を分岐していくので、./-という引数が正しくファイルパスとして認識されるのは直感的に納得です。

cdコマンドにおける-の扱い

先ほどは、% cd ./-であれば引数をファイルパスとして認識してもらえました。
てことは、「先頭の文字が-だとファイルパスとして認識してもらえない」ということなのでしょうか?

試しに、-ディレクトリの中にもう一つディレクトリを作成して、-から始まるパス (-/fuga) を渡してみましょう。

% ls
-

% mkdir ./-/fuga
% cd -/fuga
% pwd
~/tmp/hoge/-/fuga

問題なく行けました。 引数が-から始まる文字列だったとしても、ちゃんとファイルパスとして認識してもらえるようです。意外です:flushed:

run-helpコマンドを見てみると、cdコマンドにも-s-qなどのオプションは存在します。しかしcdコマンドでは、引数が-から始まっていても無条件にオプションとして処理するわけではないようです。

じゃあ-sというディレクトリは...?

引数が-から始まる文字列でも、オプションと誤認識せずファイルパスとして扱ってくれる...
それなら!!
オプションと同じ名前のディレクトリがあったら、引数はオプションかファイルパスのどちらとして認識されるのでしょうか!?
-sオプションを使って試してみましょう。(ちなみに-sオプションは、与えられたパス名にシンボリックリンクが含まれている場合はカレントディレクトリを変更しない、という効果です。)

% mkdir ./-s
% ls
-s

% cd -s
% pwd
~  # `cd`コマンドは、引数にパスがなければホームディレクトリへ移動する機能を持つ

-sはオプションとして認識されて、ホームディレクトリに移動しました。オプション名とディレクトリ名が同じ場合、認識はオプションが優先されるということですね。

一方で、cdコマンドのオプションに存在しない-zという名前のディレクトリで試してみると、引数はファイルパスとして認識されます。

% mkdir ./-z
% ls
-z

% cd -z
% pwd
~/tmp/-z

つまり、-の次の文字が「存在するオプション文字」ならオプションとして、そうでないならファイルパスとして認識するみたいです。

ならば、オプション文字から続くようなファイルパス (e.g. -s/hoge) もオプションとして認識されるのでしょうか?

% ls
-s

% mkdir ./-s/hoge
% cd -s/hoge
% pwd
~/tmp/-s/hoge

なんと、ファイルパスとして認識されました...:hushed:

オプションとして認識される条件は一体なんなのでしょうか...
例えば、-sの後ろに文字が続いていると「オプションではない」と判断されるのかもしれません。

% ls
-s

% cd -sa
cd: no such file or directory: -sa

% cd -s/
% pwd
~/tmp/-s

ビンゴ!
引数がオプションとして認識されるには、-の次の文字が「存在するオプション文字」であり、なおかつその文字で終わっている必要がありそうです。

まとめ(予想)

芋づる式に疑問が生まれ、「cdコマンドにおける-の扱い」がなんとなく見えてきました。「cdコマンドでの-の扱い」は以下のようなフローになっているのかな?と考えました。

if文 & 日本語
if 引数が一つ:
    if 一文字目が`-`である:
        if 二文字目がEOFである:  # つまり`-`単体
            "直前にいたパス"として認識
        else:
            if 二文字目がオプション文字である and 三文字目がEOFである:
                オプションとして認識
            else:
                ファイルパスとして認識
    else:
        ファイルパスとして認識
else:
    一つ目の引数をオプション, 二つ目をファイルパスとして認識

答え合わせ

Zshのソースコードはgithubにて公開されています。せっかくなので、cdコマンドが書かれているファイルをみてみましょう。
cdコマンドの実体は839行目にあるbin_cd関数です。

839行目
int
bin_cd(char *nam, char **argv, Options ops, int func)
{
    LinkNode dir;

    ...  // 省略

    // cd_get_dest関数が、引数を解析して移動を実行する関数
    if ( !(dir = cd_get_dest(nam, argv, OPT_ISSET(ops,'s'), func)) ) {
        // cd_get_dest関数の返り値がNULLならば異常終了
    	zsfree(getlinknode(dirstack));
    	unqueue_signals();
    	return 1;
    }

    cd_new_pwd(func, dir, OPT_ISSET(ops, 'q'));  // chpwd関数実行などadditionalな処理

    unqueue_signals();
    return 0;
}

引数を解析しているcd_get_dest関数。

static LinkNode
cd_get_dest(char *nam, char **argv, int hard, int func)
{
    LinkNode dir = NULL;
    LinkNode target;
    char *dest;

    // 引数の数によって処理分岐
    if (!argv[0]) {
        // 引数が一つもないとき
        ...
        
    	else if (func != BIN_POPD) {
    	    if (!home) {
        		zwarnnam(nam, "HOME not set");
        		return NULL;
    	    }
    	    zpushnode(dirstack, ztrdup(home));  // 目的地をhomeディレクトリに設定
    	}
    } else if (!argv[1]) {
        // 引数が一つのみのとき
    	int dd;
    	char *end;
    
    	doprintdir++;
        // POSIXではない && 引数が2文字以上 && 1文字目が'+'か'-'で2文字目以降に数字が含まれない
    	if (!isset(POSIXCD) && argv[0][1] && (argv[0][0] == '+' || argv[0][0] == '-')
    	    && strspn(argv[0]+1, "0123456789") == strlen(argv[0]+1)) {
    	    dd = zstrtol(argv[0] + 1, &end, 10);
            // 2文字目以降に数字が含まれないなら(3.5みたいな小数も検知対象)
            if (*end == '\0') {
                // (引数の1文字目が'+', PUSHDMINUSフラグが有効)のどちらかが真ならば
                if ((argv[0][0] == '+') ^ isset(PUSHDMINUS))
                    // ディレクトリスタックを古い方から数える
                    for (dir = firstnode(dirstack); dir && dd; dd--, incnode(dir));
                else
                    // ディレクトリスタックを新しい方から数える
                    for (dir = lastnode(dirstack); dir != (LinkNode) dirstack && dd;
                        dd--, dir = prevnode(dir));
                // dirがNULL or 現在地がディレクトリスタックのroot
                if (!dir || dir == (LinkNode) dirstack) {
                    zwarnnam(nam, "no such entry in dir stack");
                    return NULL;
                }
            }
    	}
    	if (!dir)
            // 引数が'-'ならoldpwdを、違うならその引数をdirstackにpush
            // `% cd -`の該当部分
    	    zpushnode(dirstack, ztrdup(strcmp(argv[0], "-")
    				       ? (doprintdir--, argv[0]) : oldpwd));
    } else {
        // 引数が2つ以上のとき
        ...
    }

    target = dir;
    ...
    // ディレクトリ移動の実行
    if (!(dest = cd_do_chdir(nam, getdata(dir), hard))) {
    	if (!target)
    	    zsfree(getlinknode(dirstack));
    	if (func == BIN_POPD)
    	    zsfree(remnode(dirstack, dir));
    	return NULL;
    }
    ...
    return target ? target : dir;
}

正しく読めている自信は全くないのですが、少なくとも

  • 大元としては引数の数によって分岐
  • % cd -の処理に該当していそうな部分が存在している

ということは確認できました。

他のコマンドでも扱いは同じなのか?

ここまではcdコマンドでのハイフンの扱いについて見てきましたが、コマンドが変わるとその扱いも変わってきます。

例えばmkdirコマンドについて、カレントディレクトリに存在する-の中にfugaディレクトリを作成しようとするケースを見てみます。

% ls
-
% mkdir -/fuga
mkdir: illegal option -- /
usage: mkdir [-pv] [-m mode] directory_name ...

-から始まるパスを指定しても、オプションとして誤認識されてしまうようです。
一方で、冒頭に書いたように-というディレクトリを作るべく-単体を渡すと、意図通りに動きます。
cdは内部コマンド(shell依存)、mkdirは外部コマンド(os依存)なので、ハイフンの扱いを一例として、実装方針は一概に同じとは言えないのが一因かもしれません。

結論

  • cdコマンドの場合、-というディレクトリにはcd ./-で入ることができる
  • コマンドライン引数における-の扱いは、それぞれのコマンドの実装次第で異なる

もはや存在しているのが当たり前に感じてしまうシェルですが、もちろん誰かが考えて実装した産物です。普段使う全てのツールの仕組みを理解するのは現実的に難しいですが、その一端でも気持ちを知れると楽しいなと思いました😁

  1. 拡張正規表現においては、-はbetween的な意味を持つメタ文字です。ただ、本記事の内容はunsetopt EXTENDED_GLOBされたZshでの実行結果になります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?