Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ jobs:
with:
go-version-file: go.mod
- name: Install depends
run: go get .
run: go get ./...
- name: Build
run: go build -v ./...
- name: Vet
run: go vet ./...
- name: Test
run: go test ./...
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# xdg-cli
# cli
## xdg-cli
Library to create `urfave/cli/v3`’s `ValueSource` with XDG Base Directory files.

## os-release
Library to parse os-release.

## Author
Louis Royer and the NextMN Contributors

Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
module github.com/nextmn/cli-xdg
module github.com/nextmn/cli

go 1.25.5
go 1.26.0

require (
github.com/adrg/xdg v0.5.3
github.com/urfave/cli/v3 v3.8.0
)

require golang.org/x/sys v0.26.0 // indirect
require golang.org/x/sys v0.41.0 // indirect
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
15 changes: 15 additions & 0 deletions os-release/err.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright Louis Royer and the NextMN contributors. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.
// SPDX-License-Identifier: MIT

package osrelease

import (
"errors"
)

var (
ErrNoOsReleaseFile = errors.New("cannot open os-release file")
ErrCannotParseFile = errors.New("cannot parse os-release file")
)
141 changes: 141 additions & 0 deletions os-release/os-release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright Louis Royer and the NextMN contributors. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.
// SPDX-License-Identifier: MIT

package osrelease

import (
"bufio"
"errors"
"io"
"os"
"strings"
)

// OsRelease contains operating system identification data,
// programmatic fields only (see `OS-RELEASE(5)`)
type OsRelease struct {
// Operating system identifier, excluding any version information.
// If not set, a default `ID=linux` may be used
Id string
// List of operating system identifiers that are closely related
// to the local operating system in regards
// to packaging and programming interface
IdLike []string

// Specific variant or edition of the operating system
VariantId string

// Operating system version,
// excluding any OS name information or release code name
VersionId string
// Operating system release code name,
// excluding any OS name information or release version
VersionCodename string

// System image originally used as the installation base
// Optional field
BuildId string

// Specific image of the operating system
// Optional field
ImageId string
// OS image version
ImageVersion string
}

// Escape field value
func escape(field string) (string, error) {
if len(field) == 0 {
return "", ErrCannotParseFile
}
switch field[0] {
case '\'':
field = strings.Trim(field, "'")
case '"':
field = strings.Trim(field, "\"")
}

builder := strings.Builder{}
esc := false
for _, b := range field {
if b == '\\' && !esc {
esc = true
} else if strings.ContainsRune("$'\"\\`", b) == esc {
// Shell special characters ("$", quotes, backslash, backtick) must be escaped
builder.WriteRune(b)
esc = false
} else {
return "", ErrCannotParseFile
}
}
return builder.String(), nil
}

// Create OsRelease from a file
func FromFile(file io.Reader) (*OsRelease, error) {
osRelease := OsRelease{}
scanner := bufio.NewScanner(file)
var err error
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "#") {
continue
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
key, value, found := strings.Cut(line, "=")
if !found {
return nil, ErrCannotParseFile
}
switch key {
case "ID":
osRelease.Id, err = escape(value)
case "ID_LIKE":
if tmp, err2 := escape(value); err2 == nil {
osRelease.IdLike = strings.Fields(tmp)
} else {
err = err2
}
case "VARIANT_ID":
osRelease.VariantId, err = escape(value)
case "VERSION_ID":
osRelease.VersionId, err = escape(value)
case "VERSION_CODENAME":
osRelease.VersionCodename, err = escape(value)
case "BUILD_ID":
osRelease.BuildId, err = escape(value)
case "IMAGE_ID":
osRelease.ImageId, err = escape(value)
case "IMAGE_VERSION":
osRelease.ImageVersion, err = escape(value)
default:
err = nil
}
if err != nil {
return &OsRelease{}, err
}
}
if osRelease.Id == "" {
osRelease.Id = "linux"
}
return &osRelease, nil
}

// Create OsRelease for the current operating system
func New() (*OsRelease, error) {
// main file is /etc/os-release (symlinks followed)
f, err := os.Open("/etc/os-release")
if err != nil {
// fallback on /usr/lib/os-release
f, err = os.Open("/usr/lib/os-release")
if err != nil {
return nil, errors.Join(ErrNoOsReleaseFile, err)
}
}
defer f.Close()
return FromFile(f)
}
57 changes: 57 additions & 0 deletions os-release/os-release_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright Louis Royer and the NextMN contributors. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.
// SPDX-License-Identifier: MIT

package osrelease

import (
"strings"
"testing"
)

func TestFromFile(t *testing.T) {
debian := `
PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"
NAME="Debian GNU/Linux"
VERSION_ID="12"
VERSION="12 (bookworm)"
VERSION_CODENAME=bookworm
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
`
rel, err := FromFile(strings.NewReader(debian))
if err != nil {
t.Fatal(err)
}
if rel.Id != "debian" {
t.Errorf("osRelease.Id = %s; want debian", rel.Id)
}
if rel.VersionId != "12" {
t.Errorf("osRelease.VersionId = %s; want 12", rel.VersionId)
}
if rel.VersionCodename != "bookworm" {
t.Errorf("osRelease.VersionCodename = %s; want bookworm", rel.VersionCodename)
}

alpine := `
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.22.1
PRETTY_NAME="Alpine Linux v3.22"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
`
rel, err = FromFile(strings.NewReader(alpine))
if err != nil {
t.Fatal(err)
}
if rel.Id != "alpine" {
t.Errorf("osRelease.Id = %s; want alpine", rel.Id)
}
if rel.VersionId != "3.22.1" {
t.Errorf("osRelease.VersionId = %s; want 3.22.1", rel.VersionId)
}
}
File renamed without changes.
Loading