相対パスのシンボリック・リンクを作る Automator ワークフロー


OS X には Unix 由来のシンボリック・リンクを取り扱う機能が備わっています。シンボリック・リンクとは別の場所にあるファイル／フォルダへのバイパスのようなもので、Mac で古くから使われているエイリアスは Mac 独自の機能で Terminal から使えないため、Terminal でも働くシンボリック・リンクは必要不可欠な存在です。
もともとは Terminal からコマンドを打ち込んで作成するものでしたが、今では GUI のメニューから簡単にシンボリック・リンクを作成できるツール類もいくつかあります。

## 困ったこと

私も XtraFinder の「シンボリックリンクを作成」メニューには大変お世話になっているのですが、ひとつ残念なことに、これで作られるシンボリック・リンクは __絶対パス__ で記述されたリンクなのです。
絶対パスのシンボリック・リンクだと何が困るのかと言えば、リンクを含むフォルダまでのパスが変化しただけで、リンクが働かなくなってしまうことです。

下の図を見てください。F3 フォルダの "sample.txt AbsLink" が絶対パスのシンボリック・リンク、"sample.txt RelLink" が相対パスのシンボリック・リンクで、どちらも F2 フォルダの sample.txt にリンクされています。この状態では、どちらのリンクも正しく機能しています。

```bash
$ tree F1
F1
├── F2
│   └── sample.txt
└── F3
    ├── sample.txt AbsLink -> /private/tmp/F1/F2/sample.txt
    └── sample.txt RelLink -> ../F2/sample.txt

2 directories, 3 files
$
```

とある事情から新しいフォルダ F0 を作り、F1 以下すべてを F0 のサブフォルダに移動したとしましょう。

```bash
$ tree F0
F0
└── F1
    ├── F2
    │   └── sample.txt
    └── F3
        ├── sample.txt AbsLink -> /private/tmp/F1/F2/sample.txt
        └── sample.txt RelLink -> ../F2/sample.txt

3 directories, 3 files
$
```

これだけの変化で絶対パスのシンボリック・リンクはエラーが出るようになりますが、相対パスのシンボリック・リンクは正しく動きます。

本当に困ったことは「それなら、相対パスでリンクしてくれるツールを探せばいいじゃな〜い」というわけで探してみたのですが、見つからなかったということです。

## 言い出しっぺの法則

長い前置きでしたが、ここから本題に入ります。
相対パスのシンボリック・リンクを作るためにはリンク元になるパスとリンクを格納する場所のパスのふたつの情報が必要なので、アプローチとしては以下の二案が考えられます。

1. リンク元のファイル／フォルダのパスを選択状態にして Automator に渡し、その後にリンクを作る場所のパスを何らかの方法で指定して処理する。
2. リンク元のファイル／フォルダのパス情報をひとまずどこかに待避しておいて、まずリンクを作る場所のパスを Automator に渡し、その後に待避しておいたリンク元の情報を取り出して処理する。

今回は OS X のサービスとして実装することを考えているので、ワークフローの実行中にユーザーの操作がはさまらない第２案で進めることにします。
リンクを作る場所のパスは最前面にある Finder ウィンドウのパスを使い、リンク元ファイルのパスはクリップボードにコピーされているファイル／フォルダのパス情報を使います。
Automator に用意されているアクションでは扱えないクリップボードの情報にアクセスするため、および、相対パスの作成が超簡単なため、今回は Ruby と __[RubyCocoa](http://sourceforge.net/projects/rubycocoa/)__ を利用させてもらうことにしました。

**2016-09-26: 追記**

RubyCocoa の更新が OS X 10.10 で止まってしまったようで、macOS 10.12 にアップデートしたらインストールできなくなってしまいました。
仕方がないのでしぶしぶと英文を読んだところ、AppleScript からクリップボードの情報を取り出す方法が判りましたので、AppleScript をメインに書き直すことにしました。(RubyCocoa 利用の方は、参考までに最後の方に残しておきます)

## Automator でサービスを作る (AppleScript 利用)

![全体像](https://cl.ly/3e041C2y071W/%E7%9B%B8%E5%AF%BE%E3%83%8F%E3%82%9A%E3%82%B9%E3%81%A6%E3%82%99%20SymLink%20%E3%82%92%E4%BD%9C%E3%82%8B_2016-09-26_001.png)

1. 通常のワークフローではなく、サービスを選択します。
2. サービスが受け取る項目は「入力なし」に、検索対象は「Finder」に設定します。

### 相対パスでシンボリック・リンクを作るアクション with Ruby ワンライナー

1. アクション「AppleScript を実行」を選び、以下のスクリプトを記述します。
2. ハンマーのボタンを押してエラーが出なければ、適当なファイル名を付けて保存してください。(サービスが置かれるべき場所 "~/Library/Serbices/" に自動的に保存されます)

```AppleScript
on run
	(* クリップボードからファイル/フォルダのフルパスを取得する *)
	set tgPath to missing value
	repeat with cbInfo in clipboard info
		if «class furl» is in cbInfo then
			set tgPath to POSIX path of (the clipboard as «class furl»)
		end if
	end repeat
	if tgPath is equal to missing value then
		display dialog "リンクしたいファイル/フォルダをクリップボードにコピーしてからサービスを実行してください。" buttons {"OK"} with icon stop
		return
	end if
	(* basename を切り出しておく *)
	set baseName to (do shell script "/usr/bin/basename" & space & quote & tgPath & quote)
	
	(* 最前面の Finder ウィンドウのフルパスを取得する *)
	tell application "Finder"
		if (0 < (count Finder windows)) then
			set fwPath to POSIX path of (target of window 1 as alias)
		else
			set fwPath to POSIX path of (desktop as alias)
		end if
	end tell
	
	(* Ruby のワンライナーで相対パスを取得する *)
	set relPath to (do shell script "/usr/bin/ruby -r 'pathname' -e 'print Pathname(ARGV[0]).relative_path_from(Pathname(ARGV[1]))'" & space & quote & tgPath & quote & space & quote & fwPath & quote)
	
	(* シンボリックリンクを作る *)
	do shell script "/bin/ln -s" & space & quote & relPath & quote & space & quote & fwPath & baseName & space & "Link" & quote
	
	return
end run
```

## このサービスの使い方

1. リンク元にしたいファイル／フォルダをクリップボードにコピーします。
2. シンボリック・リンクを置きたいフォルダを最前面で開きます。(Finder ウィンドウがひとつも開いていなければ、リンクはデスクトップに作成されます)
3. アプリケーションメニューから、このサービスを起動します。(メニューバーの　Finder → サービス)

## 中の人の紹介

### クリップボードの情報を取り出す AppleScript さん

クリップボードに何かコピーした状態で下記の AppleScript を実行すると、クリップボードにどんな情報が格納されているか判ります。

```AppleScript
on run
	return (clipboard info)
end run
```

例えば次のようなリストが得られるのですが、このリストは情報の種類を示すクラスとそのサイズが組になっています。

```AppleScript
{{«class furl», 32}, {«class icns», 1288541}, {«class ut16», 6}, {«class utf8», 6}, {«class 8BPS», 2368846}, {GIF picture, 82767}, {«class jp2 », 160619}, {JPEG picture, 79118}, {TIFF picture, 4197954}, {«class PNGf», 680840}, {«class BMP », 4194358}, {«class TPIC», 2376190}, {Unicode text, 4}, {string, 4}}
```

この中でクラス名 "«class furl»" の情報が、今回必要としているファイルのフルパスを格納しています。
ここから実際にパスを取り出すには、"the clipbord as «class furl»" と記述します。(クラス名の前後に付いている « と » も必要です)


```AppleScript
on run
	return (the clipboard as «class furl»)
end run
```

そうすると、こんな感じでパス名が取り出せるので、あとは POSIX path に変換していろいろと頑張っています。

```AppleScript
file "macOS SSD:Users:*******:Documents:"
```

AppleScript ではパス名の取得と編集を主に担当し、相対パスを取得するなど AppleScript が苦手とするところは do shell script で Ruby さんたちにお手伝いをお願いしています。

## 残っている課題

* 複数のファイル/フォルダをクリップボードに格納した状態でサービスを実行しても、最初のひとつしかリンクを作りません。頑張って必要な情報を取り出して処理を繰り返せばいいのですが、連休が終わってしまったので気力がなくなりました。

* * *

**以下の記述は、OS X Mavericks 時代の情報です**
## Automator でサービスを作る (RubyCocoa 利用)

![全体像](https://cl.ly/eOOY/Automator_003.png)

1. 通常のワークフローではなく、サービスを選択します。
2. サービスが受け取る項目は「入力なし」に、検索対象は「Finder」に設定します。

### 最前面の Finder ウィンドウのパスを取得するアクション

1. アクション「AppleScript を実行」を選び、以下のスクリプトを記述します。

```AppleScript
on run {input, parameters}
	(* 最前面の Finder ウィンドウのフルパスを取得する *)
	tell application "Finder"
		if (0 ≤ (count Finder windows)) then
			set f_path to POSIX path of (target of window 1 as alias) as text
		else
			set f_path to POSIX path of (desktop as alias) as text
		end if
	end tell
	
	return f_path
end run
```



### 相対パスでシンボリック・リンクを作るアクション

1. アクション「シェルスクリプトを実行」を選び、シェルを「/usr/bin/ruby」に、入力の引き渡し方法を「引数として」に設定して、以下のスクリプトを記述します。

```Ruby
require 'osx/cocoa'
require 'pathname'

# 最前面の Finder ウィンドウのフルパスを取得する
link_from_dir = Pathname(ARGV[0].dup.force_encoding('UTF-8'))

# クリップボードからファイルパスを取得する
target_full_path = OSX::NSPasteboard.generalPasteboard.pasteboardItems
  .map { |pbi| pbi.stringForType('public.file-url').to_s }.compact
  .map { |url| OSX::NSURL.URLWithString(url).path }

# 最前面の Finder ウィンドウに、取得したファイルへの SymLink を相対パスで作る
for link_to_path in target_full_path
  unless link_to_path.empty?
    link_to_dir = Pathname(link_to_path).dirname
    link_to_base = Pathname(link_to_path).basename
    link_to_rel_path = link_to_dir.relative_path_from(link_from_dir) + link_to_base
    (link_from_dir + (link_to_base.to_s + " Link")).make_symlink(link_to_rel_path)
  end
end
```

## このサービスの使い方

1. リンク元にしたいファイル／フォルダをクリップボードにコピーします。
2. シンボリック・リンクを置きたいフォルダを最前面で開きます。(Finder ウィンドウがひとつも開いていなければ、リンクはデスクトップに作成されます)
3. アプリケーションメニューから、このサービスを起動します。(メニューバーの　Finder → サービス)

## 簡単に中の人を解説

### Finder ウィンドウからパスを取得する AppleScript さん

当初はすべて RubyCocoa で済ませようと思っていたのですが、簡単にはできそうもなかったので急遽、御登場を願いました。
目的の処理を実質的に以下の３行で済ませてしまう AppleScript さん、もっとメジャーになってもいいのに……。

```AppleScript
tell application "Finder"
	set f_path to POSIX path of (target of window 1 as alias) as text
end tell
```

### いろいろ頑張ってる Ruby さん

AppleScript から渡されたパス名にマルチバイト文字が含まれている場合にそのまま引数で受け取ると、受け取った文字列の encoding が ASCII-8BIT になってしまい、後の方でいろいろと不具合の原因になったので、強制的に UTF-8 で受け取らせています。

```Ruby
# 最前面の Finder ウィンドウのフルパスを取得する
link_from_dir = Pathname(ARGV[0].dup.force_encoding('UTF-8'))
```

クリップボードにコピーしたファイル／フォルダからパス情報を取得するために RubyCocoa で NSPasteboard と NSURL を使っています。クリップボードにファイル／フォルダがコピーされていない場合に備えて stringForType('public.file-url') に .to_s を付けて、最悪でも空文字列になるようにしています。

```Ruby
# クリップボードからファイルパスを取得する
target_full_path = OSX::NSPasteboard.generalPasteboard.pasteboardItems
  .map { |pbi| pbi.stringForType('public.file-url').to_s }.compact
  .map { |url| OSX::NSURL.URLWithString(url).path }
```

リンクを置くパスから見たリンク先ファイル／フォルダまでの相対パスは、Pathname#relative_path_from メソッドで作成しています。

```Ruby
link_to_rel_path = link_to_dir.relative_path_from(link_from_dir) + link_to_base
```

今回はなんとか目的にかなうものが作れたのですが、もっと簡単な方法があれば是非とも知りたいところです。