0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go製ツールの配布で気づいた、ショートカット起動とexeパスの落とし穴

0
Posted at

エグゼクティブサマリー

Goで作ったWindows向けの小さなexeツールを配布するとき、右クリックで作る通常のショートカットと、mklinkで作るシンボリックリンクは別物でした。
特に、exe起動時にログフォルダや設定フォルダを作るようなツールでは、相対パスやカレントディレクトリを基準にすると、想定と違う場所にフォルダが作られることがあります。

今回試した範囲では、mklinkで作ったexeリンクからGoのos.Executable()を呼ぶと、リンク元のパスが取得できました。

結論

Go製ツールで「exeが起動された場所」を基準にしたいなら、まずはos.Executable()で取得したパスを使うのがよさそうです。
右クリックで作るショートカットは.lnkファイルで、アプリから見ると「ショートカット自身の場所」ではなく、リンク先exeや作業フォルダの影響を受けます。

一方、mklinkで作るファイルシンボリックリンクは、ファイルシステム上ではその場所にexeがあるように見えます。
今回の検証では、以下のようにC:\git\test.exeから起動した場合、Go側でもC:\git\test.exeが取得できました。

C:\git>test.exe
C:\git\test.exe

ただし、Goの公式ドキュメントでは、シンボリックリンク経由で起動した場合にリンク側パスが返るか実体側パスが返るかはOSに依存し得るとされています。

背景

Goで小さな業務ツールを作って配布するとき、exeの隣にlogsconfigのようなフォルダを作りたくなることがあります。
たとえば、こんな構成です。

tool.exe
logs\
config\

ただ、ここで何も考えずに相対パスでフォルダを作ると、exeの場所ではなく、カレントディレクトリ側に作られることがあります。
ショートカット起動の場合、このカレントディレクトリはショートカットの「作業フォルダー」の設定に左右されます。
そのため、ユーザーはexeを起動したつもりでも、ログや設定ファイルが思った場所に出ない、ということが起きます。

補足:右クリックのショートカットは.lnk

Windowsで右クリックから作るショートカットは、基本的に.lnkファイルです。

これはファイルシステム上のリンクというより、Windows Shellがリンク先、作業ディレクトリ、引数、アイコンなどを保持して起動するためのファイルです。

つまり、アプリ側から見ると「どの.lnkから起動されたか」を自然に知るものではありません。

やったこと

Goで、自分自身のexeパスを表示するだけのプログラムを作りました。

package main

import (
	"fmt"
	"os"
)

func main() {
	exePath, err := os.Executable()
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Println(exePath)
}

ビルドします。

go build test.go -o test.exe

その後、実体を別フォルダ側に置き、C:\git\test.exeとしてmklinkを作りました。

mklink C:\git\test.exe C:\demo\tools\test.exe

作成結果は以下です。

C:\git\test.exe <<===>> C:\demo\tools\test.exe のシンボリック リンクが作成されました

そして、C:\gitから起動しました。

C:\git>test.exe
C:\git\test.exe

今回の環境では、Goのos.Executable()mklink側の入口パスを返しました。

補足:mklinkとショートカットの違い

今回の整理は以下です。

種類 作り方 特徴
通常のショートカット 右クリックから作成 .lnkファイル。リンク先や作業フォルダーを持つ
ファイルシンボリックリンク mklink link.exe target.exe ファイルシステム上でリンク先を参照する
ディレクトリシンボリックリンク mklink /d linkdir targetdir ディレクトリ用のシンボリックリンク
ハードリンク mklink /h link.exe target.exe 同じファイル実体に別名を付ける。元・先の概念が薄い
ジャンクション mklink /j linkdir targetdir ディレクトリ用。exe単体には使えない

/jはディレクトリジャンクションなので、exe単体には使えません。
exe単体をリンクしたい場合は、通常のmklinkを使います。

mklink C:\git\test.exe C:\path\to\real\test.exe

最終形

自分の用途では、通常のショートカットだけに頼るより、mklinkで入口パスを固定するほうが扱いやすそうでした。
たとえば、ユーザーには以下のパスを起動してもらいます。

C:\git\mytool.exe

実体は別の場所に置きます。

C:\tools\mytool\versions\1.0.0\mytool.exe

リンクはこうです。

mklink C:\git\mytool.exe C:\tools\mytool\versions\1.0.0\mytool.exe

Go側では、os.Executable()で取得したパスのディレクトリを基準にします。

package main

import (
	"fmt"
	"os"
	"path/filepath"
)

func main() {
	exePath, err := os.Executable()
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	exeDir := filepath.Dir(exePath)
	logDir := filepath.Join(exeDir, "logs")

	if err := os.MkdirAll(logDir, 0755); err != nil {
		fmt.Println("mkdir error:", err)
		return
	}

	fmt.Println("exePath:", exePath)
	fmt.Println("logDir :", logDir)
}

この形なら、少なくとも今回の検証環境では、C:\git\mytool.exeを入口として扱えます。

補足:本番利用時の注意点

os.Executable()は便利ですが、シンボリックリンク経由の場合の挙動は環境差があり得ます。
そのため、本番で使うなら最低限以下は試しておいたほうがよいです。

# 絶対パスで起動
C:\git\test.exe

# カレントディレクトリから起動
cd C:\git
.\test.exe

# PATH経由で起動
test.exe

また、ログや設定ファイルをexe直下に置く設計は、更新時に少し面倒になります。
配布や更新まで考えるなら、以下のように分けるほうが安全です。

本体:
%LOCALAPPDATA%\Company\MyTool\versions\1.0.0\mytool.exe

ログ:
%LOCALAPPDATA%\Company\MyTool\logs\

設定:
%APPDATA%\Company\MyTool\config.json

exeの隣に何かを作る設計は分かりやすいですが、ツール更新や複数ユーザー利用を考えると、ログや設定は別ディレクトリに逃がしたほうが無難です。

まとめ

今回の気づきは、右クリックのショートカットとmklinkはまったく別物だということです。
右クリックのショートカットは、あくまで.lnkという起動用ファイルです。

一方でmklinkは、ファイルシステム上のリンクとして扱われます。
Go製ツールで「exeの場所を基準にログやフォルダを作りたい」ときは、カレントディレクトリではなく、os.Executable()から取得したパスを基準にするのがよさそうです。

ただし、シンボリックリンク経由の挙動は環境差があり得るため、実際に配布する環境で検証してから使うのが安全です。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?