├── go.sum ├── go.mod ├── tests ├── void │ ├── interactive.sh │ ├── run.sh │ └── Dockerfile ├── arch │ ├── run.sh │ └── Dockerfile └── debian │ ├── run.sh │ └── Dockerfile ├── .gitignore ├── distrodetector_test.go ├── cmd └── distro │ ├── README.md │ └── main.go ├── apple_test.go ├── utils.go ├── LICENSE ├── README.md ├── apple.go └── distrodetector.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/xyproto/distrodetector 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /tests/void/interactive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 3 | docker build -t d4:void . 4 | docker run -i -t d4:void /bin/bash 5 | -------------------------------------------------------------------------------- /tests/arch/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 3 | docker build --no-cache -t d4:arch . && docker run -e DISTRODETECT --rm --name d4_arch d4:arch 4 | -------------------------------------------------------------------------------- /tests/void/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 3 | docker build --no-cache -t d4:void . && docker run -e DISTRODETECT --rm --name d4_void d4:void 4 | -------------------------------------------------------------------------------- /tests/debian/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" 3 | docker build --no-cache -t d4:debian . && docker run -e DISTRODETECT --rm --name d4_debian d4:debian 4 | -------------------------------------------------------------------------------- /tests/arch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux/base 2 | ENV PROJECT distrodetector 3 | ENV PACKAGES git go base-devel 4 | RUN pacman -Syu --noconfirm && pacman -S --noconfirm $PACKAGES 5 | RUN git clone "https://github.com/xyproto/$PROJECT" "/$PROJECT" 6 | WORKDIR "/$PROJECT" 7 | CMD go test 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | cmd/distro/distro 15 | -------------------------------------------------------------------------------- /tests/void/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM voidlinux/voidlinux 2 | ENV PROJECT distrodetector 3 | ENV PACKAGES gcc git go vim nano bash st-terminfo unibilium ncurses-base 4 | RUN xbps-install -Syu && xbps-install -Syu && xbps-install -Sy $PACKAGES 5 | RUN git clone "https://github.com/xyproto/$PROJECT" "/$PROJECT" 6 | WORKDIR "/$PROJECT" 7 | ENV TERM xterm 8 | CMD go test 9 | -------------------------------------------------------------------------------- /distrodetector_test.go: -------------------------------------------------------------------------------- 1 | package distrodetector 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestName(t *testing.T) { 10 | dd := os.Getenv("DISTRODETECT") 11 | if dd != "" { 12 | detected := New().String() 13 | if detected != dd { 14 | t.Fatalf("%s should be %s!", detected, dd) 15 | } 16 | } 17 | fmt.Println(New()) 18 | } 19 | -------------------------------------------------------------------------------- /cmd/distro/README.md: -------------------------------------------------------------------------------- 1 | # distro 2 | 3 | ## Usage 4 | 5 | Output the distro name, version and codename (can be used as a drop-in replacement for `python-distro`): 6 | 7 | distro 8 | 9 | Output just the distro name: 10 | 11 | distro -n 12 | 13 | Output a combined string with all available information: 14 | 15 | distro -a 16 | 17 | ## Installation 18 | 19 | go get -u github.com/xyproto/distrodetector/cmd/distro 20 | -------------------------------------------------------------------------------- /tests/debian/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | ENV PROJECT distrodetector 3 | ENV GOVER 1.11.2 4 | ENV PACKAGES curl gcc git 5 | ENV PATH /go/bin:$PATH 6 | RUN apt-get update && apt-get -y upgrade && apt-get -y install $PACKAGES 7 | RUN curl -sOL "https://dl.google.com/go/go$GOVER.linux-amd64.tar.gz" 8 | RUN tar x -C / -f "go$GOVER.linux-amd64.tar.gz" 9 | RUN git clone "https://github.com/xyproto/$PROJECT" "/$PROJECT" 10 | WORKDIR "/$PROJECT" 11 | CMD go test 12 | -------------------------------------------------------------------------------- /apple_test.go: -------------------------------------------------------------------------------- 1 | package distrodetector 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | //func TestApple(t *testing.T) { 8 | // version := "10.12" 9 | // codename, err := codenameFromApple(version) 10 | // if err != nil { 11 | // t.Fatal(err) 12 | // } 13 | // correctCodename := "Sierra" 14 | // if codename != correctCodename { 15 | // t.Fatalf("Codename for %s should be %s, not %s!", version, correctCodename, codename) 16 | // } 17 | //} 18 | 19 | func TestLookupTable(t *testing.T) { 20 | version := "10.14" 21 | codename := AppleCodename(version) 22 | correctCodename := "Mojave" 23 | if codename != correctCodename { 24 | t.Fatalf("Codename for %s should be %s, not %s!", version, correctCodename, codename) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/distro/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/xyproto/distrodetector" 7 | ) 8 | 9 | const versionString = "distro 1.0" 10 | 11 | func main() { 12 | nFlag := flag.Bool("n", false, "output only the detected distro name") 13 | aFlag := flag.Bool("a", false, "output a combined string with all available information") 14 | vFlag := flag.Bool("v", false, "version info") 15 | flag.Parse() 16 | 17 | if *vFlag { 18 | fmt.Println(versionString) 19 | return 20 | } 21 | 22 | distro := distrodetector.New() 23 | if *nFlag { 24 | fmt.Println(distro.Name()) 25 | return 26 | } 27 | if *aFlag { 28 | fmt.Println(distro) 29 | return 30 | } 31 | // Output that should be possible to use as a drop-in replacement for python-distro 32 | name := distro.Name() 33 | version := distro.Version() 34 | if version == "" { 35 | version = "n/a" 36 | } 37 | codename := distro.Codename() 38 | if codename == "" { 39 | codename = "n/a" 40 | } 41 | fmt.Printf("Name: %s\nVersion: %s\nCodename: %s\n", name, version, codename) 42 | } 43 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package distrodetector 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | // capitalize capitalizes a string 9 | func capitalize(s string) string { 10 | switch len(s) { 11 | case 0: 12 | return "" 13 | case 1: 14 | return strings.ToUpper(s) 15 | default: 16 | return strings.ToUpper(string(s[0])) + s[1:] 17 | } 18 | } 19 | 20 | // containsDigit checks if a string contains at least one digit 21 | func containsDigit(s string) bool { 22 | for _, l := range s { 23 | if l >= '0' && l <= '9' { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | // Has returns the full path to the given executable, or the original string 31 | func Has(executable string) bool { 32 | _, err := exec.LookPath(executable) 33 | return err == nil 34 | } 35 | 36 | // Run a shell command and return the output, or an empty string 37 | func Run(shellCommand string) string { 38 | cmd := exec.Command("sh", "-c", shellCommand) 39 | stdoutStderr, err := cmd.CombinedOutput() 40 | if err != nil { 41 | return "" 42 | } 43 | return string(stdoutStderr) 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Alexander F. Rødseth 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # distrodetector 2 | 3 | [![GoDoc](https://godoc.org/github.com/xyproto/distrodetector?status.svg)](http://godoc.org/github.com/xyproto/distrodetector) [![License](http://img.shields.io/badge/license-BSD-green.svg?style=flat)](https://raw.githubusercontent.com/xyproto/distrodetector/master/LICENSE) [![Go Report Card](https://goreportcard.com/badge/github.com/xyproto/distrodetector)](https://goreportcard.com/report/github.com/xyproto/distrodetector) 4 | 5 | Detects which Linux distro or BSD a system is running. 6 | 7 | This is also a drop-in replacement for the `distro` command that comes with `python-distro`. 8 | 9 | Aims to detect: 10 | 11 | * The 100 most popular Linux distros and BSDs, according to distrowatch 12 | * macOS 13 | 14 | The `distro` utility and the `distrodetector` package has no external dependencies. 15 | 16 | Pull requests for additional systems are welcome! 17 | 18 | ## Installation of the distro utility 19 | 20 | Installation of the development version of the `distro` utility: 21 | 22 | go install github.com/xyproto/distrodetector/cmd/distro@latest 23 | 24 | Example use: 25 | 26 | distro 27 | 28 | ## Use of the Go package 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "github.com/xyproto/distrodetector" 36 | ) 37 | 38 | func main() { 39 | distro := distrodetector.New() 40 | fmt.Println(distro.Name()) 41 | } 42 | ``` 43 | ## Example output 44 | 45 | The parts can be retrieved separately with `.Platform()`, `.Name()`, `.Codename()` and `.Version()`. A combined string can be returned with the `.String()` function: 46 | 47 | Linux (Arch Linux) 48 | Linux (Ubuntu Bionic 18.04) 49 | macOS (High Sierra 10.13.3) 50 | Linux (Void Linux) 51 | 52 | ## Testing 53 | 54 | * More testing is always needed when detecting Linux distros and BSDs. 55 | * Please test the distro detection on your distro/BSD and submit an issue or pull request if it should fail. 56 | 57 | ## General Info 58 | 59 | * License: BSD-3 60 | * Version: 1.3.1 61 | * Author: Alexander F. Rødseth <xyproto@archlinux.org> 62 | -------------------------------------------------------------------------------- /apple.go: -------------------------------------------------------------------------------- 1 | package distrodetector 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | type versionInfo struct { 12 | Name string `xml:"name"` 13 | ConfigCode string `xml:"configCode"` 14 | Locale string `xml:"locale"` 15 | } 16 | 17 | // AppleCodename returns a codename, or an empty string. 18 | // Will first use the lookup table, and then try to fetch it from Apple over HTTP. 19 | func AppleCodename(version string) string { 20 | // See also: https://en.wikipedia.org/wiki/MacOS_version_history#Releases 21 | var appleCodeNames = map[string]string{ 22 | "10.0": "Cheetah", 23 | "10.1": "Puma", 24 | "10.2": "Jaguar", 25 | "10.3": "Panther", 26 | "10.4": "Tiger", 27 | "10.5": "Leopard", 28 | "10.6": "Snow Leopard", 29 | "10.7": "Lion", 30 | "10.8": "Mountain Lion", 31 | "10.9": "Mavericks", 32 | "10.10": "Yosemite", 33 | "10.11": "El Capitan", 34 | "10.12": "Sierra", 35 | "10.13": "High Sierra", 36 | "10.14": "Mojave", 37 | "10.15": "Catalina", 38 | "11.0": "Big Sur", 39 | "12.0": "Monterey", 40 | "13.0": "Ventura", 41 | "14.0": "Sonoma", 42 | } 43 | // Search the keys, longest keys first 44 | for keyLength := 5; keyLength >= 4; keyLength-- { 45 | for k, v := range appleCodeNames { 46 | if len(k) == keyLength { 47 | if strings.HasPrefix(version, k) { 48 | return v 49 | } 50 | } 51 | } 52 | } 53 | // No codename found, use one with a matching major version number 54 | majorVersionAndDot := version 55 | if strings.Contains(version, ".") { 56 | fields := strings.SplitN(version, ".", 2) 57 | majorVersionAndDot = fields[0] + "." 58 | } 59 | for k, v := range appleCodeNames { 60 | if strings.HasPrefix(k, majorVersionAndDot) { 61 | return v 62 | } 63 | } 64 | // No codename found 65 | return "" 66 | } 67 | 68 | // codenameFromApple attempts to fetch the correct codename from Apple, 69 | // given a version string. The URL that is used is: 70 | // https://support-sp.apple.com/sp/product?edid=%s 71 | func codenameFromApple(version string) (string, error) { 72 | URL := "https://support-sp.apple.com/sp/product?edid=" + version 73 | resp, err := http.Get(URL) 74 | if err != nil { 75 | return "", err 76 | } 77 | defer resp.Body.Close() 78 | data, err := ioutil.ReadAll(resp.Body) 79 | if err != nil { 80 | return "", err 81 | } 82 | //fmt.Println(string(data)) 83 | var vi versionInfo 84 | xml.Unmarshal(data, &vi) 85 | if vi.ConfigCode == "" { 86 | return "", errors.New("No codename returned from " + URL) 87 | } 88 | codename := vi.ConfigCode 89 | if strings.HasPrefix(codename, "macOS ") { 90 | return codename[6:], nil 91 | } 92 | if strings.HasPrefix(codename, "OS X ") { 93 | return codename[5:], nil 94 | } 95 | return codename, nil 96 | } 97 | -------------------------------------------------------------------------------- /distrodetector.go: -------------------------------------------------------------------------------- 1 | package distrodetector 2 | 3 | import ( 4 | "io/ioutil" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | const defaultName = "Unknown" 11 | 12 | // Used when checking for Linux distros and BSDs (and NAME= is not defined in /etc) 13 | var distroNames = []string{"Arch Linux", "Debian", "Ubuntu", "Void Linux", "FreeBSD", "NetBSD", "OpenBSD", "Manjaro", "Mint", "Elementary", "MX Linuyx", "Fedora", "openSUSE", "Solus", "Zorin", "CentOS", "KDE neon", "Lite", "Kali", "Antergos", "antiX", "Lubuntu", "PCLinuxOS", "Endless", "Peppermint", "SmartOS", "TrueOS", "Arco", "SparkyLinux", "deepin", "Puppy", "Slackware", "Bodhi", "Tails", "Xubuntu", "Archman", "Bluestar", "Mageia", "Deuvan", "Parrot", "Pop!", "ArchLabs", "Q4OS", "Kubuntu", "Nitrux", "Red Hat", "4MLinux", "Gentoo", "Pinguy", "LXLE", "KaOS", "Ultimate", "Alpine", "Feren", "KNOPPIX", "Robolinux", "Voyager", "Netrunner", "GhostBSD", "Budgie", "ClearOS", "Gecko", "SwagArch", "Emmabuntüs", "Scientific", "Omarine", "Neptune", "NixOS", "Slax", "Clonezilla", "DragonFly", "ExTiX", "OpenBSD", "Redcore", "Ubuntu Studio", "BunsenLabs", "BlackArch", "NuTyX", "ArchBang", "BackBox", "Sabayon", "AUSTRUMI", "Container", "ROSA", "SteamOS", "Tiny Core", "Kodachi", "Qubes", "siduction", "Parabola", "Trisquel", "Vector", "SolydXK", "Elive", "AV Linux", "Artix", "Raspbian", "Porteus"} 14 | 15 | // Distro represents the platform, contents of /etc/*release* and name of the 16 | // detected Linux distribution or BSD. 17 | type Distro struct { 18 | platform string 19 | etcContents string 20 | name string 21 | codename string 22 | version string 23 | } 24 | 25 | // readEtc returns the contents of /etc/*release* + /etc/issue, or an empty string 26 | func readEtc() string { 27 | filenames, err := filepath.Glob("/etc/*release*") 28 | if err != nil { 29 | return "" 30 | } 31 | filenames = append(filenames, "/etc/issue") 32 | var bs strings.Builder 33 | for _, filename := range filenames { 34 | // Try reading all the files 35 | data, err := ioutil.ReadFile(filename) 36 | if err != nil { 37 | continue 38 | } 39 | bs.Write(data) 40 | } 41 | return bs.String() 42 | } 43 | 44 | // expand expands "void" to "Void Linux", 45 | // but shortens "Debian GNU/Linux" to just "Debian". 46 | func expand(name string) string { 47 | rdict := map[string]string{"void": "Void Linux", "Debian GNU/Linux": "Debian"} 48 | if _, found := rdict[name]; found { 49 | return rdict[name] 50 | } 51 | return name 52 | } 53 | 54 | // Remove parenthesis and capitalize words within parenthesis 55 | func nopar(s string) string { 56 | if strings.Contains(s, "(") && strings.Contains(s, ")") { 57 | fields := strings.SplitN(s, "(", 2) 58 | a := fields[0] + capitalize(fields[1]) 59 | fields = strings.SplitN(a, ")", 2) 60 | return fields[0] + fields[1] 61 | } 62 | return s 63 | } 64 | 65 | // detectFromExecutables tries to detect distro information by looking for 66 | // or using existing binaries on the system. 67 | func (d *Distro) detectFromExecutables() { 68 | // TODO: Generate a list of all files in PATH before performing these checks 69 | // Executables related to package managers 70 | if Has("xbps-query") { 71 | d.name = "Void Linux" 72 | } else if Has("pacman") { 73 | d.name = "Arch Linux" 74 | } else if Has("dnf") { 75 | d.name = "Fedora" 76 | } else if Has("yum") { 77 | d.name = "Fedora" 78 | } else if Has("zypper") { 79 | d.name = "openSUSE" 80 | } else if Has("emerge") { 81 | d.name = "Gentoo" 82 | } else if Has("apk") { 83 | d.name = "Alpine" 84 | } else if Has("slapt-get") || Has("slackpkg") { 85 | d.name = "Slackware" 86 | } else if d.platform == "Darwin" { 87 | productName := strings.TrimSpace(Run("sw_vers -productName")) 88 | // Set the platform to either "macOS" or "OS X", if it is in the product name 89 | if strings.HasPrefix(productName, "Mac OS X") { 90 | d.platform = "OS X" 91 | } else if strings.Contains(productName, "macOS") { 92 | d.platform = "macOS" 93 | } else { 94 | d.platform = productName 95 | } 96 | // Version number 97 | d.version = strings.TrimSpace(Run("sw_vers -productVersion")) 98 | // Codename (like "High Sierra") 99 | d.codename = AppleCodename(d.version) 100 | // Mac doesn't really have a distro name, use the platform name 101 | d.name = d.platform 102 | } else if Has("/usr/sbin/pkg") { 103 | d.name = "FreeBSD" 104 | d.version = strings.TrimSpace(Run("/bin/freebsd-version -u")) 105 | // Only keep the version number, such as "11.2", ignore the "-RELEASE" part 106 | if strings.Contains(d.version, "-") { 107 | d.version = d.version[:strings.LastIndex(d.version, "-")] 108 | } 109 | // rpm and dpkg-query should come last, since many distros may include them 110 | } else if Has("rpm") { 111 | d.name = "Red Hat" 112 | } else if Has("dpkg-query") { 113 | d.name = "Debian" 114 | } 115 | } 116 | 117 | func (d *Distro) detectFromEtc() { 118 | // First check for Linux distros and BSD distros by grepping in /etc/*release* + /etc/issue 119 | for _, distroName := range distroNames { 120 | if d.Grep(distroName) { 121 | d.name = distroName 122 | break 123 | } 124 | } 125 | // Examine all lines of text in /etc/*release* + /etc/issue 126 | for _, line := range strings.Split(d.etcContents, "\n") { 127 | // Check if NAME= is defined in /etc/*release* + /etc/issue 128 | if strings.HasPrefix(line, "NAME=") { 129 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 130 | name := fields[1] 131 | if name != "" { 132 | if strings.HasPrefix(name, "\"") && strings.HasSuffix(name, "\"") { 133 | d.name = name[1 : len(name)-1] 134 | continue 135 | } 136 | d.name = name 137 | } 138 | // Check if DISTRIB_CODENAME= (Ubuntu) or VERSION= (Debian) is defined in /etc/*release* + /etc/issue 139 | } else if strings.HasPrefix(line, "DISTRIB_CODENAME=") || (d.codename == "" && strings.HasPrefix(line, "VERSION=")) { 140 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 141 | codename := fields[1] 142 | if codename != "" { 143 | if strings.HasPrefix(codename, "\"") && strings.HasSuffix(codename, "\"") { 144 | d.codename = nopar(capitalize(codename[1 : len(codename)-1])) 145 | continue 146 | } 147 | d.codename = nopar(capitalize(codename)) 148 | } 149 | // Check if DISTRIBVER = is defined in /etc/*release* (NetBSD) 150 | } else if strings.Contains(line, "DISTRIBVER =") { 151 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 152 | version := strings.TrimSpace(fields[1]) 153 | if version != "" { 154 | if strings.HasPrefix(version, "'") && strings.HasSuffix(version, "'") { 155 | if containsDigit(version) { 156 | d.version = version[1 : len(version)-1] 157 | } 158 | continue 159 | } 160 | if containsDigit(version) { 161 | d.version = version 162 | } 163 | } 164 | // Check if DISTRIB_RELEASE= is defined in /etc/*release* 165 | } else if strings.HasPrefix(line, "DISTRIB_RELEASE=") { 166 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 167 | version := fields[1] 168 | if version != "" { 169 | if strings.HasPrefix(version, "\"") && strings.HasSuffix(version, "\"") { 170 | if containsDigit(version) { 171 | d.version = version[1 : len(version)-1] 172 | } 173 | continue 174 | } 175 | if containsDigit(version) { 176 | d.version = version 177 | } 178 | } 179 | } else if d.version == "" && strings.HasPrefix(line, "OS_MAJOR_VERSION=") { 180 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 181 | version := fields[1] 182 | if version != "" { 183 | if strings.HasPrefix(version, "\"") && strings.HasSuffix(version, "\"") { 184 | if containsDigit(version) { 185 | d.version = version[1 : len(version)-1] 186 | } 187 | continue 188 | } 189 | if containsDigit(version) { 190 | d.version = version 191 | } 192 | } 193 | } else if d.version != "" && !strings.Contains(d.version, ".") && strings.HasPrefix(line, "OS_MINOR_VERSION=") { 194 | fields := strings.SplitN(strings.TrimSpace(line), "=", 2) 195 | version := fields[1] 196 | if version != "" { 197 | if strings.HasPrefix(version, "\"") && strings.HasSuffix(version, "\"") { 198 | if containsDigit(version) { 199 | d.version += "." + version[1:len(version)-1] 200 | } 201 | continue 202 | } 203 | if containsDigit(version) { 204 | d.version += "." + version 205 | } 206 | } 207 | } 208 | } 209 | // If the codename starts with a word with a digit, followed by a space, use that as the version number, 210 | // if the currently detected version number is empty. If not, strip the number from the codename. 211 | if strings.Contains(d.codename, " ") { 212 | fields := strings.SplitN(d.codename, " ", 2) 213 | if containsDigit(fields[0]) { 214 | if d.version == "" { 215 | d.version = fields[0] 216 | } 217 | d.codename = fields[1] 218 | } 219 | } 220 | } 221 | 222 | // New detects the platform and distro/BSD, then returns a pointer to 223 | // a Distro struct. 224 | func New() *Distro { 225 | var d Distro 226 | d.platform = capitalize(runtime.GOOS) 227 | d.etcContents = readEtc() 228 | // Distro name, if not detected 229 | d.name = defaultName 230 | d.codename = "" 231 | d.version = "" 232 | 233 | d.detectFromEtc() 234 | // Replacements 235 | d.name = expand(d.name) 236 | if d.name == defaultName { 237 | // This is only called if no distro has been detected so far 238 | d.detectFromExecutables() 239 | } 240 | return &d 241 | } 242 | 243 | // Grep /etc/*release* for the given string. 244 | // If the search fails, a case-insensitive string search is attempted. 245 | // The contents of /etc/*release* is cached. 246 | func (d *Distro) Grep(name string) bool { 247 | return strings.Contains(d.etcContents, name) || strings.Contains(strings.ToLower(d.etcContents), strings.ToLower(name)) 248 | } 249 | 250 | // Platform returns the name of the current platform. 251 | // This is the same as `runtime.GOOS`, but capitalized. 252 | func (d *Distro) Platform() string { 253 | return d.platform 254 | } 255 | 256 | // Name returns the detected name of the current distro/BSD, or "Unknown". 257 | func (d *Distro) Name() string { 258 | return d.name 259 | } 260 | 261 | // Codename returns the detected codename of the current distro/BSD, 262 | // or an empty string. 263 | func (d *Distro) Codename() string { 264 | return d.codename 265 | } 266 | 267 | // Version returns the detected release version of the current distro/BSD, 268 | // or an empty string. 269 | func (d *Distro) Version() string { 270 | return d.version 271 | } 272 | 273 | // EtcRelease returns the contents of /etc/*release + /etc/issue, or an empty string. 274 | // The contents are cached. 275 | func (d *Distro) EtcRelease() string { 276 | return d.etcContents 277 | } 278 | 279 | // String returns a string with the current platform, distro 280 | // codename and release version (if available). 281 | // Example strings: 282 | // 283 | // Linux (Ubuntu Bionic 18.04) 284 | // Darwin (10.13.3) 285 | func (d *Distro) String() string { 286 | var sb strings.Builder 287 | sb.WriteString(d.platform) 288 | sb.WriteString(" ") 289 | if d.name != "" || d.codename != "" || d.version != "" { 290 | sb.WriteString("(") 291 | needSpace := false 292 | if d.name != defaultName && d.name != "" { 293 | sb.WriteString(d.name) 294 | needSpace = true 295 | } 296 | if d.codename != "" { 297 | if needSpace { 298 | sb.WriteString(" ") 299 | } 300 | sb.WriteString(d.codename) 301 | needSpace = true 302 | } 303 | if d.version != "" { 304 | if needSpace { 305 | sb.WriteString(" ") 306 | } 307 | sb.WriteString(d.version) 308 | } 309 | sb.WriteString(")") 310 | } 311 | return sb.String() 312 | } 313 | --------------------------------------------------------------------------------