From 65954b5b7086c29de64e6ed9a4c4de3b16c4e6df Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Wed, 3 Jun 2020 12:42:04 +0800 Subject: [PATCH 1/2] unix: Detect via locale.conf instead of locale command Signed-off-by: Xuanwo --- locale_darwin.go | 2 +- locale_freebsd.go | 2 +- locale_linux.go | 2 +- locale_openbsd.go | 2 +- locale_solaris.go | 2 +- locale_unix.go | 84 +++++++++++++++++++++++++++------ locale_unix_test.go | 111 ++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 183 insertions(+), 22 deletions(-) diff --git a/locale_darwin.go b/locale_darwin.go index ced765a..f54d015 100644 --- a/locale_darwin.go +++ b/locale_darwin.go @@ -12,7 +12,7 @@ import ( var detectors = []detector{ detectViaEnvLanguage, detectViaEnvLc, - detectViaLocale, + detectViaLocaleConf, detectViaUserDefaultsSystem, } diff --git a/locale_freebsd.go b/locale_freebsd.go index 30d9525..12dad33 100644 --- a/locale_freebsd.go +++ b/locale_freebsd.go @@ -5,5 +5,5 @@ package locale var detectors = []detector{ detectViaEnvLanguage, detectViaEnvLc, - detectViaLocale, + detectViaLocaleConf, } diff --git a/locale_linux.go b/locale_linux.go index 30d9525..12dad33 100644 --- a/locale_linux.go +++ b/locale_linux.go @@ -5,5 +5,5 @@ package locale var detectors = []detector{ detectViaEnvLanguage, detectViaEnvLc, - detectViaLocale, + detectViaLocaleConf, } diff --git a/locale_openbsd.go b/locale_openbsd.go index 30d9525..12dad33 100644 --- a/locale_openbsd.go +++ b/locale_openbsd.go @@ -5,5 +5,5 @@ package locale var detectors = []detector{ detectViaEnvLanguage, detectViaEnvLc, - detectViaLocale, + detectViaLocaleConf, } diff --git a/locale_solaris.go b/locale_solaris.go index 30d9525..12dad33 100644 --- a/locale_solaris.go +++ b/locale_solaris.go @@ -5,5 +5,5 @@ package locale var detectors = []detector{ detectViaEnvLanguage, detectViaEnvLc, - detectViaLocale, + detectViaLocaleConf, } diff --git a/locale_unix.go b/locale_unix.go index 5b7f71a..b415164 100644 --- a/locale_unix.go +++ b/locale_unix.go @@ -5,12 +5,22 @@ package locale import ( "bufio" - "bytes" "os" - "os/exec" + "path" "strings" ) +// Unless we call LookupEnv more than 9 times, we should not use Environ. +// +// goos: linux +// goarch: amd64 +// pkg: github.com/Xuanwo/go-locale +// BenchmarkLookupEnv +// BenchmarkLookupEnv-8 37024654 32.4 ns/op +// BenchmarkEnviron +// BenchmarkEnviron-8 4275735 281 ns/op +// PASS + // envs is the env to be checked. // // LC_ALL will overwrite all LC_* options. @@ -50,11 +60,22 @@ func detectViaEnvLc() ([]string, error) { return nil, &Error{"detect via env lc", ErrNotDetected} } -func detectViaLocale() ([]string, error) { - cmd := exec.Command("locale") +func detectViaLocaleConf() (_ []string, err error) { + defer func() { + if err != nil { + err = &Error{"detect via locale conf", err} + } + }() + + fp := getLocaleConfPath() + if fp == "" { + return nil, ErrNotDetected + } - var out bytes.Buffer - cmd.Stdout = &out + f, err := os.Open(fp) + if err != nil { + return nil, err + } // Output should be like: // @@ -72,13 +93,8 @@ func detectViaLocale() ([]string, error) { // LC_MEASUREMENT="en_US.UTF-8" // LC_IDENTIFICATION="en_US.UTF-8" // LC_ALL= - err := cmd.Run() - if err != nil { - return nil, &Error{"detect via locale", err} - } - m := make(map[string]string) - s := bufio.NewScanner(&out) + s := bufio.NewScanner(f) for s.Scan() { value := strings.Split(s.Text(), "=") // Ignore not set locale value. @@ -94,7 +110,49 @@ func detectViaLocale() ([]string, error) { return []string{parseEnvLc(x)}, nil } } - return nil, &Error{"detect via locale", ErrNotDetected} + return nil, ErrNotDetected +} + +// getLocaleConfPath will try to get correct locale conf path. +// +// Following path could be returned: +// - "$XDG_CONFIG_HOME/locale.conf" (follow XDG Base Directory specification) +// - "$HOME/.config/locale.conf" (user level locale config) +// - "/etc/locale.conf" (system level locale config) +// - "" (empty means no valid path found, caller need to handle this.) +// +// ref: +// - POSIX Locale: https://pubs.opengroup.org/onlinepubs/9699919799/ +// - XDG Base Directory: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html +func getLocaleConfPath() string { + // Try to loading from $XDG_CONFIG_HOME/locale.conf + xdg, ok := os.LookupEnv("XDG_CONFIG_HOME") + if ok { + fp := path.Join(xdg, "locale.conf") + _, err := os.Stat(fp) + if err == nil { + return fp + } + } + + // Try to loading from $HOME/.config/locale.conf + home, ok := os.LookupEnv("HOME") + if ok { + fp := path.Join(home, ".config", "locale.conf") + _, err := os.Stat(fp) + if err == nil { + return fp + } + } + + // Try to loading from /etc/locale.conf + fp := "/etc/locale.conf" + _, err := os.Stat(fp) + if err == nil { + return fp + } + + return "" } // parseEnvLanguage will parse LANGUAGE env. diff --git a/locale_unix_test.go b/locale_unix_test.go index 56cb360..b7357b7 100644 --- a/locale_unix_test.go +++ b/locale_unix_test.go @@ -5,10 +5,13 @@ package locale import ( "errors" + "io/ioutil" "os" + "path" "strings" "sync" "testing" + "time" . "github.com/smartystreets/goconvey/convey" ) @@ -112,11 +115,81 @@ func TestDetectViaEnvLc(t *testing.T) { }) } -func TestDetectViaLocale(t *testing.T) { - Convey("detect via locale", t, func() { - lang, err := detectViaLocale() +func TestGetLocaleConfPath(t *testing.T) { + Convey("get locale conf path", t, func() { + // Make sure env has clear before current test. + setupEnv() + + Reset(func() { + // Reset all env after every Convey. + setupEnv() + }) - Convey("The error should not be nil", func() { + Convey("When user set XDG_CONFIG_HOME", func() { + tmpDir := setupLocaleConf("locale.conf") + Reset(func() { + _ = os.RemoveAll(tmpDir) + }) + + err := os.Setenv("XDG_CONFIG_HOME", tmpDir) + if err != nil { + t.Error(err) + } + + fp := getLocaleConfPath() + + Convey("The path should be equal", func() { + So(fp, ShouldEqual, path.Join(tmpDir, "locale.conf")) + }) + }) + + Convey("When user set HOME", func() { + tmpDir := setupLocaleConf(".config/locale.conf") + Reset(func() { + _ = os.RemoveAll(tmpDir) + }) + + err := os.Setenv("HOME", tmpDir) + if err != nil { + t.Error(err) + } + + fp := getLocaleConfPath() + + Convey("The path should be equal", func() { + So(fp, ShouldEqual, path.Join(tmpDir, ".config/locale.conf")) + }) + }) + + Convey("When fallback to system level locale.conf", func() { + fp := getLocaleConfPath() + + Convey("The path should be equal", func() { + So(fp, ShouldEqual, "/etc/locale.conf") + }) + }) + }) +} + +func TestDetectViaLocaleConf(t *testing.T) { + Convey("detect via locale conf", t, func() { + setupEnv() + Reset(func() { + setupEnv() + }) + + tmpDir := setupLocaleConf("locale.conf") + Reset(func() { + _ = os.RemoveAll(tmpDir) + }) + err := os.Setenv("XDG_CONFIG_HOME", tmpDir) + if err != nil { + t.Error(err) + } + + lang, err := detectViaLocaleConf() + + Convey("The error should be nil", func() { So(err, ShouldBeNil) }) Convey("The lang should not be empty", func() { @@ -125,12 +198,42 @@ func TestDetectViaLocale(t *testing.T) { }) } +func BenchmarkLookupEnv(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _ = os.LookupEnv("LANGUAGE") + } +} + +func BenchmarkEnviron(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = os.Environ() + } +} + var env struct { Env map[string]string sync.Mutex sync.Once } +func setupLocaleConf(filePath string) (dir string) { + confContent := `LANG=en_US.UTF-8` + tmpDir := "/tmp/" + time.Now().String() + baseDir := path.Dir(path.Join(tmpDir, filePath)) + + err := os.MkdirAll(baseDir, 0755) + if err != nil { + panic(err) + } + + err = ioutil.WriteFile(path.Join(tmpDir, filePath), []byte(confContent), 0644) + if err != nil { + panic(err) + } + + return tmpDir +} + func setupEnv() { env.Lock() defer env.Unlock() From a1e13d67755ce7735f4c14c3ee7cdd6501e43732 Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Wed, 3 Jun 2020 14:05:59 +0800 Subject: [PATCH 2/2] tests: Hack /etc/locale.conf Signed-off-by: Xuanwo --- locale_unix_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locale_unix_test.go b/locale_unix_test.go index b7357b7..e1f1217 100644 --- a/locale_unix_test.go +++ b/locale_unix_test.go @@ -162,10 +162,16 @@ func TestGetLocaleConfPath(t *testing.T) { }) Convey("When fallback to system level locale.conf", func() { + var localeExist bool + _, err := os.Stat("/etc/locale.conf") + if err == nil { + localeExist = true + } + fp := getLocaleConfPath() Convey("The path should be equal", func() { - So(fp, ShouldEqual, "/etc/locale.conf") + So(fp == "/etc/locale.conf", ShouldEqual, localeExist) }) }) })