🏹

そのファイル、実はシンボリックリンクで共有されています…を正しく調べる方法

2021/11/22に公開

シンボリックリンクで共有されているケース

ファイルが シンボリック リンク によって実体が別にある(複数のパスで共有されている)か、共有されていないかを調べるには、ls -l コマンドを実行すれば分かると思っているのではないでしょうか。

$ ls -l
./sub1/a.ts -> ./main/a.ts

実は、ls -l だけでは分かりません。 なぜなら、共有されているファイルは、その親フォルダーやそのさらに親フォルダー… のどこかがシンボリックリンクになっていることがほとんどだからです。 そのフォルダーに対して ls -l しなければ分からないからです。

$ ls -l ./sub1/a.ts    #// ファイルを調べても分からない
./sub1/a.ts
$ ls -l ./main/a.ts    #// リンク先
./main/a.ts
$ ls -l                #// 親フォルダーを調べて初めて分かった
./sub1/ -> ./main/

本記事では、ファイルのパス(上記の ./sub1/a.ts や ./main/a.ts)だけ指定することで、親フォルダーやさらにその親フォルダー…の状況を調べて、ファイルが共有されているかどうかを調べるコマンドのテンプレートを提供します。 また、リンク元を一覧するテンプレートも提供します。

ファイルが シンボリック リンク のフォルダーの中にあるかどうかを調べる方法

bash (Linux, Windows Git) で下記のコマンドを実行します。 ただし、__FileFullPath__の部分を調べるファイルのフルパス(絶対パス)に置き換えてください。 その他の部分はそのままコピペできます。

_path=__FileFullPath__;  echo "$_path";  _link=$(readlink -f "$_path");  echo "$_link";  if [[ "$_path" == "$_link" ]];  then echo "not in link";  else echo "in link";  fi;  unset _path;  unset _link

もし、リンクの中にある場合、次のように表示されます。

in link

もし、リンクの中にない場合、次のように表示されます。

not in link

zsh (mac) の場合、コマンドは以下のようになります。

_path=__FileFullPath__;  echo "$_path";  _link=$(python -c "import os,sys;print(os.path.realpath('$_path'))");  echo "$_link";  if [[ "$_path" == "$_link" ]];  then echo "not in link";  else echo "in link";  fi;  unset _path;  unset _link

__FileFullPath__の部分は、$(pwd)/relative.txt のように相対パスのように置き換えることもできますが $(pwd)/../relative.txt のような ..(親)は書けません。

サンプル

たとえば、

./main/a.ts
./sub1 -> ./main

というフォルダー構成を作ってみて、以下のパスについてリンクかどうか調べてみましょう。

  • ./main/a.ts (not in link)
  • ./sub1/a.ts (in link)

bash (Linux) の場合、以下のようにコマンドを入力します。

$ mkdir  try_link
$ cd     try_link
$ mkdir  main
$ echo "a"  >  ./main/a.ts
$ ln -sf  ./main  ./sub1    #// シンボリックリンクを作ります
$ ls -l
drwxr-xr-x  user1  group1  12:00 main
lrwxrwxrwx  user1  group1  12:00 sub1 -> ./main

$ _path=$(pwd)/main/a.ts;  echo "$_path";  _link=$(readlink -f "$_path");  echo "$_link";  if [[ "$_path" == "$_link" ]];  then echo "not in link";  else echo "in link";  fi;  unset _path;  unset _link
not in link

$ _path=$(pwd)/sub1/a.ts;  echo "$_path";  _link=$(readlink -f "$_path");  echo "$_link";  if [[ "$_path" == "$_link" ]];  then echo "not in link";  else echo "in link";  fi;  unset _path;  unset _link
in link

$ cd ..
$ rm -rf  try_link

Windows の Git bash でシンボリックリンクを作る方法

Windows の場合、Git for Windows をインストールすると上記のコマンドがほぼそのまま使える bash(シェル)が使えるようになります。

  • https://git-scm.com/ >> Downloads >> Windows
  • ダウンロードしたファイル(例:Git-2.33.0.2-64-bit.exe)を開く
  • Next を9回押す
  • Configuring the line ending conversions: Checkout as-is, commit as-is
  • 他のインストール オプションはデフォルトを使用

シンボリックリンクを作る ln -sf コマンドに関しては、管理者の bash を新しく開き、代わりに以下のコマンドを入力します。

(管理者として実行するように開いた Git Bash で)

cd __ParentOfMakingLink__  #// シンボリックリンクを作るフォルダー
MSYS=winsymlinks:nativestrict ln -sf  __Target__  __LinkFileName__

管理者として実行するように Git Bash を開くには、Windows のスタートをクリックして、git bash と入力し、表示された Git Bash を右クリックして [管理者として実行] を選びます。

ネットには、管理者でなくてもシンボリックリンクを作れるようにグループ ポリシー を変更する方法が紹介されていますが、セキュリティ上の懸念があるのでお勧めしません。

https://docs.microsoft.com/ja-jp/windows/security/threat-protection/security-policy-settings/create-symbolic-links#セキュリティに関する考慮事項

(参考)動作のしくみ

実行したコマンドを整形すると以下のようになります。

_path=__FileFullPath__
_link=$(readlink -f "$_path")
if [[ "$_path" == "$_link" ]]; then
    echo "not in link"
;else
    echo "in link"
;fi;
unset _path;
unset _link

readlink -f コマンドでリンク先のフルパスが _link 変数に格納されます。 もし、シンボリックリンクではない場合は指定したファイルのフルパスが格納されます。 なので、フルパスを指定して、そのまま格納されたかそうでないかで判定しています。 もし、readlink -f コマンドに相対パスを指定するとフルパスが返ります。

なお、_link などアンダースコアから始まる変数を一時的に定義しています。 不要になったら削除しています。 もし他で同じ名前の変数が定義されていたら削除されてしまうので注意してください。

リンクではないファイルが共有されている可能性

シンボリックリンクか普通のファイルかを調べるだけで十分なこともありますが、ファイルが共有されているかどうかという観点では不十分なこともあります。

なぜなら、調べたファイルが普通のファイルであっても、そのファイルがどこかのシンボリックリンクのリンク先になっている場合、ファイルは共有されていることになるからです。

リンク元を含むシンボリックリンクを一覧する方法

調べるファイルがどこからリンクされているか(リンク元)を調べる方法を紹介します。

たとえば、

./main/a.ts
./sub1 -> ./main
./sub2 -> ./other
./sub3 -> ./main
./a.ts -> ./main/a.ts

のようにリンクが 4つある場合、./main/a.ts のリンク元は、

  • ./sub1/a.ts
  • ./sub3/a.ts
  • ./a.ts

です。 これらを調べたいところですが以下で説明する方法では、それらを含む大元のフォルダーやファイルへのシンボリックリンクを一覧します。

./sub1 -> ./main
./sub3 -> ./main
./a.ts -> ./main/a.ts

ファイルを調べてフォルダーを返すことは少しややこしいですが、大元が分かれば共有する全体的な範囲も分かるというメリットがあります。

準備

シンボリック リンク の一覧を一時ファイル _links.txt に保存する処理をします。 ただし、__Project__ の部分は編集してください。

cd  __Project__
find "." -type l -ls | grep -v /node_modules/ | awk '{print $11,$12,$13}' | tee ~/_links.txt
  • __Project__ の外にあるリンク元は検索対象外になります
  • grep -v /node_modules/ は検索対象外にするフォルダーです。 Node.js を使うプロジェクトの場合は必須ですが、その他のプロジェクトで入力しても問題ありません。

リンク先のファイルのフルパスを ~/_full_path.txt ファイルの中に書きます。 下記の __FullPath__ の部分を編集して他はコピペしてください。 $(pwd)/relative.txt のように編集すれば相対パスのように書くこともできますが ..(親)は書けません。

echo "__FullPath__" > ~/_full_path.txt;  cat ~/_full_path.txt

for 文の区切りを改行に設定します:

IFS=$'\n'

メイン処理

指定したファイルがリンク先に含まれているシンボリックリンクを検索して一覧します。 下記をそのままシェルへコピペします。

bash (Linux, Windows Git) の場合

for  line  in $(cat ~/_links.txt); do
    readlink -f ${line%% *} | xargs -i  grep {}  ~/_full_path.txt > /dev/null && echo "${line%% *} -> ${line##* } ($(readlink -f ${line%% *}))"
done

zsh (mac) の場合

for  line  in $(cat ~/_links.txt); do
    python -c "import os,sys;print(os.path.realpath('${line%% *}'))" | xargs -I{}  grep {}  ~/_full_path.txt > /dev/null && echo "${line%% *} -> ${line##* } ($(python -c "import os,sys;print(os.path.realpath('${line%% *}'))"))"
done

後始末

環境変数を戻し、一時ファイルを削除します。 そのままコピペします。

unset IFS
rm ~/_full_path.txt
rm ~/_links.txt

サンプル

実際に動かして動きを確認してみましょう。

bash (Linux) の場合、以下のようにコマンドを入力します。

$ mkdir  try_link
$ cd     try_link
$ mkdir  main
$ echo "a"  >  ./main/a.ts
$ ln -sf  ./main  ./sub1    #// シンボリックリンクを作ります
$ ln -sf  ./other ./sub2
$ ln -sf  ./main  ./sub3
$ ln -sf  ./main/a.ts  ./a.ts  #// シンボリックリンクを作ります(最後)
$ ls -l
lrwxrwxrwx  user1  group1  12:00 a.ts -> ./main/a.ts
drwxr-xr-x  user1  group1  12:00 main
lrwxrwxrwx  user1  group1  12:00 sub1 -> ./main
lrwxrwxrwx  user1  group1  12:00 sub2 -> ./other
lrwxrwxrwx  user1  group1  12:00 sub3 -> ./main

$ cd "."  #// 検索範囲
$ find "." -type l -ls | grep -v /node_modules/ | awk '{print $11,$12,$13}' | tee ~/_links.txt

$ echo "$(pwd)/main/a.ts" > ~/_full_path.txt;  cat ~/_full_path.txt  #// リンク先
/home/user1/try_link/main/a.ts
$ IFS=$'\n'

$ for  line  in $(cat ~/_links.txt); do
        readlink -f ${line%% *} | xargs -i  grep {}  ~/_full_path.txt > /dev/null && echo "${line%% *} -> ${line##* } ($(readlink -f ${line%% *}))"
    done
./a.ts -> ./main/a.ts (/home/user1/try_link/main/a.ts)
./sub1 -> ./main (/home/user1/try_link/main)
./sub3 -> ./main (/home/user1/try_link/main)

$ unset IFS
$ rm ~/_full_path.txt
$ rm ~/_links.txt

$ cd ..
$ rm -rf  try_link

bash (Windows Git) の場合、ln -sf コマンドの代わりに以下のようにしてシンボリックリンクを作ります。

管理者として Git Bash を開き、

cd __ParentOfMakingLink__  #// シンボリックリンクを作るフォルダー
MSYS=winsymlinks:nativestrict ln -sf  __Target__  __LinkFileName__

シンボリックリンクかどうか調べる必要性

そもそも、シンボリックリンクか普通のファイルかを調べる必要性があるのかピンと来ない方もおられると思いますのでユースケースを挙げておきます。

たとえば、インストールするプログラムのフォルダーの実体がバージョンごとに分かれていて、シンボリックリンクで共通のパスから起動できるようにしていることがよくあります。

/usr/bin/program.1.0.0/bin/program  #// 実体
/opt/program/bin/program            #// 起動パス

この場合のシンボリックリンクは下記のように作ります。

/opt/program  ->  /usr/bin/program.1.0.0

もし、シンボリックリンクであることを調べなければ、バージョンごとに別れたフォルダーの存在に気づかずに、バージョンで分けない構成でインストールしてしまうでしょう。

他にも、たとえば、編集した ソース ファイル が共有されていたら、オリジナルがどこにあるかを探して、オリジナルの近くにあるテストを早めに通すようにします。 そうしなければ、後から影響範囲が広い不具合が発生してしまい、以前編集したソースファイルの内容をまた思い出さなければならなくなるでしょう。

__Projects__/
    __Application__/
        src/main.tx
        src/lib/a.ts  -> __Library__/src/a.ts
    __Library__/
        src/a.ts
        test/a_test.ts

Discussion