@@ -18,6 +18,7 @@ import (
1818 "regexp"
1919 "strconv"
2020 "strings"
21+ "sync"
2122 "sync/atomic"
2223 "time"
2324 "unicode"
@@ -179,20 +180,127 @@ func GtagsUpdateIndex(ctx context.Context, dir string) error {
179180}
180181
181182// englishEnv は文字化け防止のため LANG=C を追加した環境変数を返す。
183+ // extra に同名の変数があれば os.Environ() 側の既存値は除去する
184+ // (シェルで GTAGSDBPATH 等を設定されていた場合の重複を防ぐ)。
182185func englishEnv (extra ... string ) []string {
186+ override := map [string ]bool {"LANG" : true , "LC_ALL" : true }
187+ for _ , e := range extra {
188+ if i := strings .Index (e , "=" ); i > 0 {
189+ override [e [:i ]] = true
190+ }
191+ }
183192 env := os .Environ ()
184- // LANG/LC_ALL を C に上書きして英語出力に統一
185- filtered := env [:0 :0 ]
193+ filtered := make ([]string , 0 , len (env )+ len (extra )+ 2 )
186194 for _ , e := range env {
187- if ! strings .HasPrefix (e , "LANG =" ) && ! strings . HasPrefix ( e , "LC_ALL=" ) {
188- filtered = append ( filtered , e )
195+ if i := strings .Index (e , "=" ); i > 0 && override [ e [: i ]] {
196+ continue
189197 }
198+ filtered = append (filtered , e )
190199 }
191200 filtered = append (filtered , "LANG=C" , "LC_ALL=C" )
192201 filtered = append (filtered , extra ... )
193202 return filtered
194203}
195204
205+ // gtagsEnv は GTAGSDBPATH / GTAGSROOT を設定した環境変数を返す。
206+ // パスは ToSlash しない: Cygwin ビルドの global.exe は Windows パス
207+ // (バックスラッシュ) を受け付けるが、フォワードスラッシュだと検索が
208+ // 空を返す症状が実機で確認されている。
209+ func gtagsEnv (dir string ) []string {
210+ return englishEnv ("GTAGSDBPATH=" + dir , "GTAGSROOT=" + dir )
211+ }
212+
213+ // ===== Cygwin bash フォールバック =====
214+ //
215+ // 同梱の Cygwin ビルド global.exe が native Windows プロセス(Go) が作成した
216+ // pipe に書き込めず stdout が空になる症状への対策。
217+ // Cygwin bash を経由して /tmp に出力させ、それを読み戻すことで回避する。
218+ //
219+ // bash が PATH に無い環境ではフォールバックを無効化し、従来通り cmd.Output()
220+ // の結果(空かもしれない)をそのまま使う。Cygwin 必須化はしない。
221+
222+ var (
223+ _bashOnce sync.Once
224+ _bashPath string // Cygwin bash.exe のフルパス、未検出なら ""
225+ _cygTmpWindowsPath string // Cygwin /tmp の Windows パス、未検出なら ""
226+ )
227+
228+ // initBashRun は bash の検出と /tmp の Windows パス取得を一度だけ実行する。
229+ func initBashRun () {
230+ _bashOnce .Do (func () {
231+ p , err := exec .LookPath ("bash" )
232+ if err != nil {
233+ slog .Info ("gtags-bash-fallback" , "msg" , "bash not found in PATH, fallback disabled" )
234+ return
235+ }
236+ out , err := exec .Command (p , "-c" , "cygpath -w /tmp" ).Output ()
237+ if err != nil {
238+ slog .Info ("gtags-bash-fallback" , "msg" , "cygpath failed, fallback disabled" , "err" , err )
239+ return
240+ }
241+ _bashPath = p
242+ _cygTmpWindowsPath = strings .TrimSpace (string (out ))
243+ slog .Info ("gtags-bash-fallback" , "msg" , "ready" , "bash" , _bashPath , "tmp_win" , _cygTmpWindowsPath )
244+ })
245+ }
246+
247+ // windowsToCygwinPath は Windows パス (C:\foo\bar) を
248+ // Cygwin POSIX パス (/cygdrive/c/foo/bar) に変換する。
249+ func windowsToCygwinPath (p string ) string {
250+ p = filepath .ToSlash (p )
251+ if len (p ) >= 2 && p [1 ] == ':' {
252+ return "/cygdrive/" + strings .ToLower (p [0 :1 ]) + p [2 :]
253+ }
254+ return p
255+ }
256+
257+ // shellQuote は bash 用にシングルクォートで囲む。
258+ // 文字列内の ' を '\'' で escape する標準テクニック。
259+ func shellQuote (s string ) string {
260+ return "'" + strings .ReplaceAll (s , "'" , `'\''` ) + "'"
261+ }
262+
263+ // runGlobalViaBash は global.exe を Cygwin bash 経由で実行し結果バイト列を返す。
264+ // 戻り値の第2引数 attempted が true なら bash 経路を試みた(成功/失敗問わず)。
265+ // false の場合は bash が無いので呼び出し側はフォールバック断念。
266+ //
267+ // mode は global の検索オプション (`-xd` 定義 / `-xr` 参照 等)。
268+ func runGlobalViaBash (globalBin , dir , word , mode string ) (data []byte , attempted bool ) {
269+ initBashRun ()
270+ if _bashPath == "" {
271+ return nil , false
272+ }
273+ tmpName := fmt .Sprintf ("grepnavi-gtags-%d-%d.txt" , os .Getpid (), time .Now ().UnixNano ())
274+ cygTmp := "/tmp/" + tmpName
275+ winTmp := filepath .Join (_cygTmpWindowsPath , tmpName )
276+ defer os .Remove (winTmp )
277+
278+ globalCyg := windowsToCygwinPath (globalBin )
279+ // GTAGSDBPATH/GTAGSROOT は Windows パスのまま (Cygwin global は両方解釈する)。
280+ // global.exe の実行パスは Cygwin パス (/cygdrive/...) でなければ bash が認識しない。
281+ // 出力先は Cygwin パス (/tmp/...): bash の > リダイレクトは POSIX パス前提。
282+ cmdStr := fmt .Sprintf ("GTAGSDBPATH=%s GTAGSROOT=%s %s %s %s > %s 2>/dev/null" ,
283+ shellQuote (dir ), shellQuote (dir ),
284+ shellQuote (globalCyg ),
285+ mode ,
286+ shellQuote (word ),
287+ shellQuote (cygTmp ))
288+
289+ cmd := exec .Command (_bashPath , "-c" , cmdStr )
290+ // exit=1 は「ヒットなし」なので情報的、それ以外は警告
291+ if err := cmd .Run (); err != nil {
292+ if ee , ok := err .(* exec.ExitError ); ! ok || ee .ExitCode () != 1 {
293+ slog .Debug ("gtags-bash-fallback exec" , "err" , err , "cmd" , cmdStr )
294+ }
295+ }
296+ out , rerr := os .ReadFile (winTmp )
297+ if rerr != nil {
298+ // ファイル無し = 結果なし (global が "no results" で何も書かなかった等)
299+ return nil , true
300+ }
301+ return out , true
302+ }
303+
196304// sanitizeLine はShift-JIS等の不正バイト列を除去してUTF-8安全な文字列に変換する。
197305func sanitizeLine (s string ) string {
198306 return strings .ToValidUTF8 (s , "" )
@@ -275,8 +383,9 @@ func GtagsBuildIndexStream(ctx context.Context, dir string, w io.Writer) error {
275383// Windows ではパイプバッファリングにより出力が遅延するため、5 秒ごとにハートビートを送る。
276384func GtagsUpdateIndexStream (ctx context.Context , dir string , w io.Writer ) error {
277385 cmd := exec .CommandContext (ctx , resolveGlobalBin (), "-u" , "-v" )
278- gtagsPathU := filepath .ToSlash (dir )
279- cmd .Env = englishEnv ("GTAGSDBPATH=" + gtagsPathU , "GTAGSROOT=" + gtagsPathU )
386+ // Cygwin global.exe は Windows パス (バックスラッシュ) を受け付ける。
387+ // ToSlash でフォワードスラッシュ化すると検索が空を返す症状が出るため使わない。
388+ cmd .Env = gtagsEnv (dir )
280389
281390 pr , pw := io .Pipe ()
282391 cmd .Stdout = pw
@@ -391,10 +500,7 @@ func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, erro
391500 // キャンセルされても結果はキャッシュに入るので次回即返せる。
392501 cmd := exec .CommandContext (context .Background (), globalBin , "-xd" , word )
393502 cmd .Dir = dir
394- // MSYS2版 global.exe はバックスラッシュを解釈できないためフォワードスラッシュに変換する
395- gtagsPath := filepath .ToSlash (dir )
396- env := append (os .Environ (), "GTAGSDBPATH=" + gtagsPath , "GTAGSROOT=" + gtagsPath )
397- cmd .Env = env
503+ cmd .Env = gtagsEnv (dir )
398504 if devNull , err := os .Open (os .DevNull ); err == nil {
399505 cmd .Stdin = devNull
400506 defer devNull .Close ()
@@ -433,6 +539,19 @@ func GtagsFindDefinitions(ctx context.Context, word, dir string) ([]DefHit, erro
433539 "stderr" , strings .TrimSpace (stderr .String ()),
434540 "err" , err )
435541
542+ // Cygwin global.exe が native Windows pipe に書けず stdout が空のことがある。
543+ // exit=0 かつ stdout が空 = この症状の可能性 → bash 経由で再実行を試みる。
544+ if err == nil && len (bytes .TrimSpace (out )) == 0 {
545+ if bashOut , attempted := runGlobalViaBash (globalBin , dir , word , "-xd" ); attempted {
546+ if len (bytes .TrimSpace (bashOut )) > 0 {
547+ slog .Info ("gtags-find" , "msg" , "bash fallback succeeded" , "word" , word , "bytes" , len (bashOut ))
548+ out = bashOut
549+ } else {
550+ slog .Debug ("gtags-find" , "msg" , "bash fallback also empty (truly no results)" , "word" , word )
551+ }
552+ }
553+ }
554+
436555 if err != nil {
437556 if exitCode == 1 {
438557 slog .Debug ("gtags-find" , "msg" , "exit=1 not found (normal)" )
@@ -643,7 +762,7 @@ func GtagsDiagnose(dir, word string) {
643762 }
644763
645764 // ---- 4. DB読み取りテスト(共通シンボルで疎通確認)----
646- env := append ( os . Environ (), "GTAGSDBPATH=" + dir , "GTAGSROOT=" + dir )
765+ env := gtagsEnv ( dir )
647766 cmdLine := fmt .Sprintf ("%s -xd <word> (GTAGSDBPATH=%s GTAGSROOT=%s)" , bin , dir , dir )
648767 slog .Info ("gtags-diag [4] command-line" , "cmd" , cmdLine , "note" , "以下のコマンドをターミナルで実行して同じ結果か確認してください" )
649768
@@ -864,10 +983,10 @@ func GtagsFindHoverHits(ctx context.Context, word, dir string) ([]DefHit, error)
864983// GtagsFindRefs は GNU Global で word の参照箇所を検索する(callers 用)。
865984// 各参照行を囲む関数名・定義行を findContainingFunc で解決して返す。
866985func GtagsFindRefs (ctx context.Context , word , dir string ) ([]CallSite , error ) {
867- cmd := exec .CommandContext (context .Background (), resolveGlobalBin (), "-xr" , word )
986+ globalBin := resolveGlobalBin ()
987+ cmd := exec .CommandContext (context .Background (), globalBin , "-xr" , word )
868988 cmd .Dir = dir
869- gtagsRefPath := filepath .ToSlash (dir )
870- cmd .Env = append (os .Environ (), "GTAGSDBPATH=" + gtagsRefPath , "GTAGSROOT=" + gtagsRefPath )
989+ cmd .Env = gtagsEnv (dir )
871990 var stderr bytes.Buffer
872991 cmd .Stderr = & stderr
873992 out , err := cmd .Output ()
@@ -879,6 +998,15 @@ func GtagsFindRefs(ctx context.Context, word, dir string) ([]CallSite, error) {
879998 slog .Warn ("gtags-find-refs error" , "word" , word , "err" , err , "stderr" , stderr .String ())
880999 return nil , err
8811000 }
1001+ // Cygwin global.exe が native pipe に書けず stdout が空のことがある(GtagsFindDefinitions と同症状)。
1002+ if len (bytes .TrimSpace (out )) == 0 {
1003+ if bashOut , attempted := runGlobalViaBash (globalBin , dir , word , "-xr" ); attempted {
1004+ if len (bytes .TrimSpace (bashOut )) > 0 {
1005+ slog .Info ("gtags-find-refs" , "msg" , "bash fallback succeeded" , "word" , word , "bytes" , len (bashOut ))
1006+ out = bashOut
1007+ }
1008+ }
1009+ }
8821010 hits := gtagsParseOutput (out , "ref" , dir )
8831011 slog .Debug ("gtags-find-refs raw hits" , "word" , word , "count" , len (hits ))
8841012 var results []CallSite
0 commit comments