Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unix: Detect via locale.conf instead of locale command #14

Merged
merged 2 commits into from
Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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