Skip to content

Commit

Permalink
unix: Detect via locale.conf instead of locale command (#14)
Browse files Browse the repository at this point in the history
* unix: Detect via locale.conf instead of locale command

Signed-off-by: Xuanwo <github@xuanwo.io>

* tests: Hack /etc/locale.conf

Signed-off-by: Xuanwo <github@xuanwo.io>
  • Loading branch information
Xuanwo authored Jun 3, 2020
1 parent 786e297 commit a6fd0c7
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 22 deletions.
2 changes: 1 addition & 1 deletion locale_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
var detectors = []detector{
detectViaEnvLanguage,
detectViaEnvLc,
detectViaLocale,
detectViaLocaleConf,
detectViaUserDefaultsSystem,
}

Expand Down
2 changes: 1 addition & 1 deletion locale_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ package locale
var detectors = []detector{
detectViaEnvLanguage,
detectViaEnvLc,
detectViaLocale,
detectViaLocaleConf,
}
2 changes: 1 addition & 1 deletion locale_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ package locale
var detectors = []detector{
detectViaEnvLanguage,
detectViaEnvLc,
detectViaLocale,
detectViaLocaleConf,
}
2 changes: 1 addition & 1 deletion locale_openbsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ package locale
var detectors = []detector{
detectViaEnvLanguage,
detectViaEnvLc,
detectViaLocale,
detectViaLocaleConf,
}
2 changes: 1 addition & 1 deletion locale_solaris.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ package locale
var detectors = []detector{
detectViaEnvLanguage,
detectViaEnvLc,
detectViaLocale,
detectViaLocaleConf,
}
84 changes: 71 additions & 13 deletions locale_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
//
Expand All @@ -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.
Expand All @@ -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.
Expand Down
117 changes: 113 additions & 4 deletions locale_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ package locale

import (
"errors"
"io/ioutil"
"os"
"path"
"strings"
"sync"
"testing"
"time"

. "github.com/smartystreets/goconvey/convey"
)
Expand Down Expand Up @@ -112,11 +115,87 @@ 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("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() {
var localeExist bool
_, err := os.Stat("/etc/locale.conf")
if err == nil {
localeExist = true
}

fp := getLocaleConfPath()

Convey("The error should not be nil", func() {
Convey("The path should be equal", func() {
So(fp == "/etc/locale.conf", ShouldEqual, localeExist)
})
})
})
}

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() {
Expand All @@ -125,12 +204,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()
Expand Down

0 comments on commit a6fd0c7

Please sign in to comment.