├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── go.yml ├── LICENSE ├── README.md ├── basedir ├── basedir.go └── basedir_test.go ├── cmd └── xdg-trash │ ├── .gitignore │ ├── main.go │ └── usage.go ├── desktop ├── doc.go ├── entry.go ├── entry_test.go ├── launch.go ├── type.go └── type_test.go ├── go.mod ├── go.sum ├── keyfile ├── boolean.go ├── boolean_test.go ├── errors.go ├── keyfile.go ├── keyfile_test.go ├── locale.go ├── locale_test.go ├── localestring.go ├── localestring_test.go ├── number.go ├── number_test.go ├── string.go └── string_test.go ├── open.go ├── trash ├── doc.go ├── home.go ├── info.go ├── info_test.go ├── trash.go └── trash_test.go └── userdirs ├── userdirs.go └── userdirs_test.go /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [{Makefile,*.go,*.md}] 10 | indent_style = tab 11 | 12 | [*.yml] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: "CodeQL" 4 | 5 | "on": 6 | push: 7 | branches: [master] 8 | pull_request: 9 | # The branches below must be a subset of the branches above 10 | branches: [master] 11 | schedule: 12 | - cron: '44 21 * * 5' 13 | 14 | jobs: 15 | analyze: 16 | name: Analyze 17 | runs-on: ubuntu-latest 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: ['go'] 27 | 28 | steps: 29 | - name: Checkout repository 30 | uses: actions/checkout@v4 31 | 32 | # Initializes the CodeQL tools for scanning. 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v3 35 | with: 36 | languages: ${{ matrix.language }} 37 | # If you wish to specify custom queries, you can do so here or in a 38 | # config file. By default, queries listed here will override any 39 | # specified in a config file. Prefix the list here with "+" to use 40 | # these queries and those in the config file. 41 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 42 | 43 | - name: Set up GITHUB_ENV 44 | run: | 45 | go version | sed 's/^go version /GOVERSION=/' | \ 46 | tr ' /' '-' >>$GITHUB_ENV 47 | go env GOPATH | sed 's/^/GOPATH=/' >>$GITHUB_ENV 48 | go env GOCACHE | sed 's/^/GOCACHE=/' >>$GITHUB_ENV 49 | 50 | - name: Set up Go cache 51 | uses: actions/cache@v4 52 | with: 53 | path: | 54 | ${{ env.GOCACHE }} 55 | ${{ env.GOPATH }}/pkg/mod 56 | key: > 57 | ${{ github.workflow }}-${{ runner.os }}-${{ env.GOVERSION }}-${{ 58 | hashFiles('**/go.sum') }} 59 | restore-keys: > 60 | ${{ github.workflow }}-${{ runner.os }}-${{ env.GOVERSION }}- 61 | 62 | - run: go build -v github.com/rkoesters/xdg/... 63 | 64 | - name: Perform CodeQL Analysis 65 | uses: github/codeql-action/analyze@v3 66 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: CI 4 | 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | go: 12 | - 1.11.x # debian buster 13 | - 1.13.x # ubuntu focal 14 | - 1.15.x # debian bullseye 15 | - 1.x # latest 16 | 17 | name: Go ${{ matrix.go }} 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go }} 28 | 29 | - name: Set up build dependencies 30 | run: | 31 | sudo apt-get update -y 32 | sudo apt-get install -y -q golint 33 | 34 | - name: Set up GITHUB_ENV 35 | run: > 36 | go version | 37 | sed 's/^go version /GOVERSION=/' | 38 | tr ' /' '-' >>$GITHUB_ENV 39 | 40 | go env GOPATH | sed 's/^/GOPATH=/' >>$GITHUB_ENV 41 | 42 | go env GOCACHE | sed 's/^/GOCACHE=/' >>$GITHUB_ENV 43 | 44 | - name: Set up Go cache 45 | uses: actions/cache@v4 46 | with: 47 | path: | 48 | ${{ env.GOCACHE }} 49 | ${{ env.GOPATH }}/pkg/mod 50 | key: ${{ env.GOVERSION }}-cache-${{ hashFiles('**/go.sum') }} 51 | restore-keys: ${{ env.GOVERSION }}-cache- 52 | 53 | - name: Set up user directories 54 | run: xdg-user-dirs-update 55 | 56 | - name: Set up trash 57 | run: | 58 | mkdir -p "$HOME/.local/share/Trash/files" 59 | touch "$HOME/.local/share/Trash/files/file.txt" 60 | mkdir -p "$HOME/.local/share/Trash/info" 61 | printf '[Trash Info]\nPath=%s\nDeletionDate=%s\n' "$HOME/file.txt" "$(date '+%Y-%m-%dT%H:%M:%S')" >"$HOME/.local/share/Trash/info/file.txt.trashinfo" 62 | 63 | - run: golint github.com/rkoesters/xdg/... 64 | - run: go build -v github.com/rkoesters/xdg/... 65 | - run: go test -v -cover github.com/rkoesters/xdg/... 66 | - run: go vet -v github.com/rkoesters/xdg/... 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Ryan Koesters. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | xdg 2 | === 3 | 4 | Package xdg provides access to the FreeDesktop.org (XDG) specs. 5 | 6 | [![GoDoc](https://godoc.org/github.com/rkoesters/xdg?status.svg)](https://godoc.org/github.com/rkoesters/xdg) 7 | [![Build Status](https://travis-ci.org/rkoesters/xdg.svg?branch=master)](https://travis-ci.org/rkoesters/xdg) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/rkoesters/xdg)](https://goreportcard.com/report/github.com/rkoesters/xdg) 9 | 10 | Documentation 11 | ------------- 12 | 13 | Documentation is available via godoc. Here are direct links to the 14 | documentation pages for each package: 15 | 16 | * [xdg](https://godoc.org/github.com/rkoesters/xdg) - Provides xdg.Open 17 | function to call `xdg-open` command. 18 | * [xdg/basedir](https://godoc.org/github.com/rkoesters/xdg/basedir) - 19 | Provides access to the xdg basedir spec. 20 | * [xdg/desktop](https://godoc.org/github.com/rkoesters/xdg/desktop) - 21 | Read desktop files (w/ localization support). 22 | * [xdg/keyfile](https://godoc.org/github.com/rkoesters/xdg/keyfile) - 23 | Provides access to xdg key file format (w/ localization support). 24 | * [xdg/trash](https://godoc.org/github.com/rkoesters/xdg/trash) - 25 | Provides access to xdg trash spec. 26 | * [xdg/userdirs](https://godoc.org/github.com/rkoesters/xdg/userdirs) - 27 | Provides access to common user directories. 28 | 29 | Testing 30 | ------- 31 | 32 | Tests can be run with `go test`. 33 | 34 | The tests for the [xdg/trash](trash) package expect the trash to exist 35 | (`$XDG_DATA_HOME/Trash/files` (or `$HOME/.local/share/Trash/files` if 36 | `$XDG_DATA_HOME` is undefined)). 37 | 38 | The tests for the [xdg/userdirs](userdirs) package require the 39 | `xdg-user-dir` command. 40 | 41 | TODO 42 | ---- 43 | 44 | - autostart 45 | - desktop.Launch (in progress on desktop-launch branch) 46 | - trash.New (requires checking for a "sticky bit" on the filesystem of 47 | the drive on which the trash exists; in addition to other (easier) 48 | checks) 49 | 50 | License 51 | ------- 52 | 53 | See [LICENSE](LICENSE). 54 | -------------------------------------------------------------------------------- /basedir/basedir.go: -------------------------------------------------------------------------------- 1 | // Package basedir provides access to XDG base directory spec. For more 2 | // information, please see the spec: 3 | // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 4 | package basedir 5 | 6 | import ( 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | ) 11 | 12 | var ( 13 | // Home is the user's home directory. 14 | Home string 15 | 16 | // DataHome is the path to the directory where user data files 17 | // should be written. 18 | DataHome string 19 | 20 | // StateHome is the path to the directory where log or history files 21 | // should be written 22 | StateHome string 23 | 24 | // ConfigHome is the path to the directory where user 25 | // configuration files should be written. 26 | ConfigHome string 27 | 28 | // CacheHome is the path to the directory where non-essential 29 | // (cached) data should be written. 30 | CacheHome string 31 | 32 | // RuntimeDir is the path to the directory where runtime files 33 | // should be placed. 34 | RuntimeDir string 35 | 36 | // DataDirs is a slice of paths that should be searched for data 37 | // files. 38 | DataDirs []string 39 | 40 | // ConfigDirs is a slice of paths that should be searched for 41 | // configuration files. 42 | ConfigDirs []string 43 | ) 44 | 45 | func init() { 46 | Home = os.Getenv("HOME") 47 | if Home == "" { 48 | u, err := user.Current() 49 | if err == nil { 50 | Home = u.HomeDir 51 | } else { 52 | Home = filepath.Join(os.TempDir(), os.Args[0]) 53 | } 54 | } 55 | 56 | DataHome = getPath("XDG_DATA_HOME", filepath.Join(Home, ".local/share")) 57 | StateHome = getPath("XDG_STATE_HOME", filepath.Join(Home, ".local/state")) 58 | ConfigHome = getPath("XDG_CONFIG_HOME", filepath.Join(Home, ".config")) 59 | CacheHome = getPath("XDG_CACHE_HOME", filepath.Join(Home, ".cache")) 60 | RuntimeDir = getPath("XDG_RUNTIME_DIR", CacheHome) 61 | DataDirs = getPathList("XDG_DATA_DIRS", []string{"/usr/local/share", "/usr/share"}) 62 | ConfigDirs = getPathList("XDG_CONFIG_DIRS", []string{"/etc/xdg"}) 63 | } 64 | 65 | func getPath(env, def string) string { 66 | path := os.Getenv(env) 67 | if path == "" || !filepath.IsAbs(path) { 68 | return def 69 | } 70 | return path 71 | } 72 | 73 | func getPathList(env string, def []string) []string { 74 | paths := filepath.SplitList(os.Getenv(env)) 75 | for i := 0; i < len(paths); i++ { 76 | // If the path isn't absolute, we need to ignore it. 77 | if !filepath.IsAbs(paths[i]) { 78 | paths = append(paths[:i], paths[i+1:]...) 79 | } 80 | } 81 | if len(paths) == 0 { 82 | return def 83 | } 84 | return paths 85 | } 86 | -------------------------------------------------------------------------------- /basedir/basedir_test.go: -------------------------------------------------------------------------------- 1 | package basedir 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestBaseDir(t *testing.T) { 9 | if Home == "" { 10 | t.Error("Home not set") 11 | } 12 | if DataHome == "" { 13 | t.Error("DataHome not set") 14 | } 15 | if ConfigHome == "" { 16 | t.Error("ConfigHome not set") 17 | } 18 | if CacheHome == "" { 19 | t.Error("CacheHome not set") 20 | } 21 | if RuntimeDir == "" { 22 | t.Error("RuntimeDir not set") 23 | } 24 | if len(DataDirs) == 0 { 25 | t.Error("DataDirs not set") 26 | } 27 | if len(ConfigDirs) == 0 { 28 | t.Error("ConfigDirs not set") 29 | } 30 | } 31 | 32 | func TestGetPath(t *testing.T) { 33 | const notSet = "not set" 34 | if getPath("HOME", notSet) == notSet { 35 | t.Error("Couldn't get HOME") 36 | } 37 | if getPath("does_not_exist", notSet) != notSet { 38 | t.Error("does_not_exist exists") 39 | } 40 | if getPath("USER", notSet) != notSet { 41 | t.Error("USER appears to be an absolute path") 42 | } 43 | } 44 | 45 | func TestGetpathlist(t *testing.T) { 46 | if getPathList("PATH", nil) == nil { 47 | t.Error("Couldn't get PATH") 48 | } 49 | if getPathList("does_not_exist", nil) != nil { 50 | t.Error("does_not_exist exists") 51 | } 52 | err := os.Setenv("xdg_test_var", "/a:c:/a/b:d/f") 53 | if err != nil { 54 | t.Error(err) 55 | } 56 | testVar := getPathList("xdg_test_var", nil) 57 | if testVar[0] != "/a" || testVar[1] != "/a/b" { 58 | t.Error("getPathList returned relative paths") 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/xdg-trash/.gitignore: -------------------------------------------------------------------------------- 1 | xdg-trash 2 | -------------------------------------------------------------------------------- /cmd/xdg-trash/main.go: -------------------------------------------------------------------------------- 1 | // xdg-trash is a command line interface to the trash package for the 2 | // purposes of debugging. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "github.com/rkoesters/xdg/trash" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "time" 13 | ) 14 | 15 | const ( 16 | countName = "count" 17 | emptyName = "empty" 18 | eraseName = "erase" 19 | infoName = "info" 20 | lsName = "ls" 21 | restoreName = "restore" 22 | rmName = "rm" 23 | ) 24 | 25 | var ( 26 | countFlag = flag.NewFlagSet(countName, flag.ExitOnError) 27 | emptyFlag = flag.NewFlagSet(emptyName, flag.ExitOnError) 28 | eraseFlag = flag.NewFlagSet(eraseName, flag.ExitOnError) 29 | infoFlag = flag.NewFlagSet(infoName, flag.ExitOnError) 30 | lsFlag = flag.NewFlagSet(lsName, flag.ExitOnError) 31 | restoreFlag = flag.NewFlagSet(restoreName, flag.ExitOnError) 32 | rmFlag = flag.NewFlagSet(rmName, flag.ExitOnError) 33 | ) 34 | 35 | func main() { 36 | log.SetFlags(0) 37 | log.SetPrefix(os.Args[0] + ": ") 38 | 39 | flag.Parse() 40 | 41 | if flag.NArg() < 1 { 42 | log.Println("command required") 43 | flag.Usage() 44 | os.Exit(1) 45 | } 46 | 47 | log.SetPrefix(os.Args[0] + " " + flag.Arg(0) + ": ") 48 | 49 | switch flag.Arg(0) { 50 | case countName: 51 | countFlag.Parse(flag.Args()[1:]) 52 | countMain() 53 | case emptyName: 54 | emptyFlag.Parse(flag.Args()[1:]) 55 | emptyMain() 56 | case eraseName: 57 | eraseFlag.Parse(flag.Args()[1:]) 58 | eraseMain() 59 | case infoName: 60 | infoFlag.Parse(flag.Args()[1:]) 61 | infoMain() 62 | case lsName: 63 | lsFlag.Parse(flag.Args()[1:]) 64 | lsMain() 65 | case restoreName: 66 | restoreFlag.Parse(flag.Args()[1:]) 67 | restoreMain() 68 | case rmName: 69 | rmFlag.Parse(flag.Args()[1:]) 70 | rmMain() 71 | default: 72 | log.SetPrefix(os.Args[0] + ": ") 73 | log.Printf("unknown command '%v'\n", flag.Arg(0)) 74 | flag.Usage() 75 | os.Exit(1) 76 | } 77 | } 78 | 79 | var ( 80 | countQuiet = countFlag.Bool("q", false, "suppress output, set exit status to count") 81 | ) 82 | 83 | func countMain() { 84 | if countFlag.NArg() != 0 { 85 | log.Print("count does not accept arguments") 86 | countFlag.Usage() 87 | os.Exit(1) 88 | } 89 | 90 | files, err := trash.Files() 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | if *countQuiet { 96 | os.Exit(len(files)) 97 | } else { 98 | fmt.Println(len(files)) 99 | } 100 | } 101 | 102 | func emptyMain() { 103 | if emptyFlag.NArg() != 0 { 104 | log.Print("empty does not accept arguments") 105 | emptyFlag.Usage() 106 | os.Exit(1) 107 | } 108 | 109 | err := trash.Empty() 110 | if err != nil { 111 | log.Fatal(err) 112 | } 113 | } 114 | 115 | var ( 116 | eraseRecursive = eraseFlag.Bool("r", false, "recursively erase") 117 | ) 118 | 119 | func eraseMain() { 120 | if eraseFlag.NArg() == 0 { 121 | eraseFlag.Usage() 122 | os.Exit(1) 123 | } 124 | 125 | for _, file := range eraseFlag.Args() { 126 | var err error 127 | 128 | if *eraseRecursive { 129 | err = trash.EraseAll(file) 130 | } else { 131 | err = trash.Erase(file) 132 | } 133 | 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | } 138 | } 139 | 140 | var ( 141 | infoAll = infoFlag.Bool("a", false, "show info for all files in the trash") 142 | infoCompact = infoFlag.Bool("1", false, "one file info per line") 143 | ) 144 | 145 | func infoMain() { 146 | if (*infoAll && infoFlag.NArg() != 0) || (!*infoAll && infoFlag.NArg() == 0) { 147 | infoFlag.Usage() 148 | os.Exit(1) 149 | } 150 | 151 | var files []string 152 | var err error 153 | 154 | if *infoAll { 155 | files, err = trash.Files() 156 | if err != nil { 157 | log.Fatal(err) 158 | } 159 | } else { 160 | files = infoFlag.Args() 161 | } 162 | 163 | for _, file := range files { 164 | info, err := trash.Stat(file) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | if *infoCompact { 170 | fmt.Print(file, ":", info.Path, ":", info.DeletionDate.Format(time.RFC3339), "\n") 171 | } else { 172 | fmt.Println("File:", file) 173 | fmt.Println("Original Path:", info.Path) 174 | fmt.Println("Deletion Date:", info.DeletionDate.Format(time.RFC822)) 175 | } 176 | } 177 | } 178 | 179 | func lsMain() { 180 | if lsFlag.NArg() != 0 { 181 | log.Print("ls does not accept arguments") 182 | lsFlag.Usage() 183 | os.Exit(1) 184 | } 185 | 186 | files, err := trash.Files() 187 | if err != nil { 188 | log.Fatal(err) 189 | } 190 | 191 | for _, fname := range files { 192 | fmt.Println(fname) 193 | } 194 | } 195 | 196 | func restoreMain() { 197 | if restoreFlag.NArg() == 0 { 198 | restoreFlag.Usage() 199 | os.Exit(1) 200 | } 201 | 202 | args := restoreFlag.Args() 203 | 204 | var dest *string 205 | 206 | if len(args) > 1 { 207 | d := args[len(args)-1] 208 | dest = &d 209 | args = args[:len(args)-1] 210 | } 211 | 212 | for i := 0; i < len(args); i++ { 213 | var err error 214 | 215 | file := args[i] 216 | 217 | if dest != nil { 218 | if len(args) > 1 { 219 | err = trash.RestoreTo(file, filepath.Join(*dest, file)) 220 | } else { 221 | err = trash.RestoreTo(file, *dest) 222 | } 223 | } else { 224 | err = trash.Restore(file) 225 | } 226 | 227 | if err != nil { 228 | log.Fatal(err) 229 | } 230 | } 231 | } 232 | 233 | func rmMain() { 234 | if rmFlag.NArg() == 0 { 235 | rmFlag.Usage() 236 | os.Exit(1) 237 | } 238 | 239 | for _, path := range rmFlag.Args() { 240 | err := trash.Trash(path) 241 | if err != nil { 242 | log.Fatal(err) 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /cmd/xdg-trash/usage.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func init() { 10 | flag.Usage = usage 11 | countFlag.Usage = countUsage 12 | emptyFlag.Usage = emptyUsage 13 | eraseFlag.Usage = eraseUsage 14 | infoFlag.Usage = infoUsage 15 | lsFlag.Usage = lsUsage 16 | restoreFlag.Usage = restoreUsage 17 | rmFlag.Usage = rmUsage 18 | } 19 | 20 | func usage() { 21 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], "COMMAND [FLAGS]") 22 | fmt.Fprintln(os.Stderr, "COMMANDS:") 23 | fmt.Fprintln(os.Stderr, "\tcount") 24 | fmt.Fprintln(os.Stderr, "\t\tprint the number of items in the trash") 25 | fmt.Fprintln(os.Stderr) 26 | fmt.Fprintln(os.Stderr, "\tempty") 27 | fmt.Fprintln(os.Stderr, "\t\tempty the trash") 28 | fmt.Fprintln(os.Stderr) 29 | fmt.Fprintln(os.Stderr, "\terase") 30 | fmt.Fprintln(os.Stderr, "\t\tdelete an item from the trash") 31 | fmt.Fprintln(os.Stderr) 32 | fmt.Fprintln(os.Stderr, "\tinfo") 33 | fmt.Fprintln(os.Stderr, "\t\tshow information about an item in the trash") 34 | fmt.Fprintln(os.Stderr) 35 | fmt.Fprintln(os.Stderr, "\tls") 36 | fmt.Fprintln(os.Stderr, "\t\tlist the items in the trash") 37 | fmt.Fprintln(os.Stderr) 38 | fmt.Fprintln(os.Stderr, "\trestore") 39 | fmt.Fprintln(os.Stderr, "\t\trestore a file from the trash") 40 | fmt.Fprintln(os.Stderr) 41 | fmt.Fprintln(os.Stderr, "\trm") 42 | fmt.Fprintln(os.Stderr, "\t\tmove a file to the trash") 43 | fmt.Fprintln(os.Stderr) 44 | fmt.Fprintln(os.Stderr, "For command help, run:", os.Args[0], "COMMAND -help") 45 | } 46 | 47 | func countUsage() { 48 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], countName, "[FLAGS]") 49 | fmt.Fprintln(os.Stderr, "FLAGS:") 50 | countFlag.PrintDefaults() 51 | } 52 | 53 | func emptyUsage() { 54 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], emptyName) 55 | } 56 | 57 | func eraseUsage() { 58 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], eraseName, "[FLAGS] FILE...") 59 | fmt.Fprintln(os.Stderr, "FLAGS:") 60 | eraseFlag.PrintDefaults() 61 | } 62 | 63 | func infoUsage() { 64 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], infoName, "[FLAGS] FILE...") 65 | fmt.Fprintln(os.Stderr, " ", os.Args[0], infoName, "[FLAGS] -a") 66 | fmt.Fprintln(os.Stderr, "FLAGS:") 67 | infoFlag.PrintDefaults() 68 | } 69 | 70 | func lsUsage() { 71 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], lsName) 72 | } 73 | 74 | func restoreUsage() { 75 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], restoreName, "FILE [DESTINATION]") 76 | fmt.Fprintln(os.Stderr, " ", os.Args[0], restoreName, "FILE... DIRECTORY") 77 | } 78 | 79 | func rmUsage() { 80 | fmt.Fprintln(os.Stderr, "Usage:", os.Args[0], rmName, "PATH...") 81 | } 82 | -------------------------------------------------------------------------------- /desktop/doc.go: -------------------------------------------------------------------------------- 1 | // Package desktop implements the Desktop Entry Spec. For more 2 | // information, please see the spec: 3 | // http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html 4 | package desktop 5 | -------------------------------------------------------------------------------- /desktop/entry.go: -------------------------------------------------------------------------------- 1 | package desktop 2 | 3 | import ( 4 | "errors" 5 | "github.com/rkoesters/xdg/keyfile" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // ErrMissingType means that the desktop entry is missing the 12 | // Type key, which is always required. 13 | ErrMissingType = errors.New("missing entry type") 14 | 15 | // ErrMissingName means that the desktop entry is missing the 16 | // Name key, which is required by the types Application, Link, 17 | // and Directory. 18 | ErrMissingName = errors.New("missing entry name") 19 | 20 | // ErrMissingURL means that the desktop entry is missing the URL 21 | // key, which is required by the type Link. 22 | ErrMissingURL = errors.New("missing entry url") 23 | ) 24 | 25 | const ( 26 | groupDesktopEntry = "Desktop Entry" 27 | groupDesktopActionPrefix = "Desktop Action " 28 | 29 | keyType = "Type" 30 | keyVersion = "Version" 31 | keyName = "Name" 32 | keyGenericName = "GenericName" 33 | keyNoDisplay = "NoDisplay" 34 | keyComment = "Comment" 35 | keyIcon = "Icon" 36 | keyHidden = "Hidden" 37 | keyOnlyShowIn = "OnlyShowIn" 38 | keyNotShowIn = "NotShowIn" 39 | keyDBusActivatable = "DBusActivatable" 40 | keyTryExec = "TryExec" 41 | keyExec = "Exec" 42 | keyPath = "Path" 43 | keyTerminal = "Terminal" 44 | keyActions = "Actions" 45 | keyMimeType = "MimeType" 46 | keyCategories = "Categories" 47 | keyImplements = "Implements" 48 | keyKeywords = "Keywords" 49 | keyStartupNotify = "StartupNotify" 50 | keyStartupWMClass = "StartupWMClass" 51 | keyURL = "URL" 52 | ) 53 | 54 | // Entry represents a desktop entry file. 55 | type Entry struct { 56 | // The type of desktop entry. It can be: Application, Link, or 57 | // Directory. 58 | Type Type 59 | // The version of spec that the file conforms to. 60 | Version string 61 | 62 | // The real name of the desktop entry. 63 | Name string 64 | // A generic name, for example: Text Editor or Web Browser. 65 | GenericName string 66 | // A short comment that describes the desktop entry. 67 | Comment string 68 | // The name of an icon that should be used for this desktop 69 | // entry. If it is not an absolute path, it should be searched 70 | // for using the Icon Theme Specification. 71 | Icon string 72 | // The URL for a Link type entry. 73 | URL string 74 | 75 | // Whether or not to display the file in menus. 76 | NoDisplay bool 77 | // Whether the use has deleted the desktop entry. 78 | Hidden bool 79 | // A list of desktop environments that the desktop entry should 80 | // only be shown in. 81 | OnlyShowIn []string 82 | // A list of desktop environments that the desktop entry should 83 | // not be shown in. 84 | NotShowIn []string 85 | 86 | // Whether DBus Activation is supported by this application. 87 | DBusActivatable bool 88 | // The path to an executable to test if the program is 89 | // installed. 90 | TryExec string 91 | // Program to execute. 92 | Exec string 93 | // The path that should be the programs working directory. 94 | Path string 95 | // Whether the program should be run in a terminal window. 96 | Terminal bool 97 | 98 | // A slice of actions. 99 | Actions []*Action 100 | // A slice of mimetypes supported by this program. 101 | MimeType []string 102 | // A slice of categories that the desktop entry should be shown 103 | // in in a menu. 104 | Categories []string 105 | // A slice of interfaces that this application implements. 106 | Implements []string 107 | // A slice of keywords. 108 | Keywords []string 109 | 110 | // Whether the program will send a "remove" message when started 111 | // with the DESKTOP_STARTUP_ID env variable is set. 112 | StartupNotify bool 113 | // The string that the program will set as WM Class or WM name 114 | // hint. 115 | StartupWMClass string 116 | 117 | // Extended pairs. These are all of the key=value pairs in which 118 | // the key follows the format X-PRODUCT-KEY. For example, 119 | // accessing X-Unity-IconBackgroundColor can be done with: 120 | // 121 | // entry.X["Unity"]["IconBackgroundColor"] 122 | // 123 | X map[string]map[string]string 124 | } 125 | 126 | // New reads a desktop file from r and returns an Entry that represents 127 | // the desktop file using the default locale. 128 | func New(r io.Reader) (*Entry, error) { 129 | return NewWithLocale(r, keyfile.DefaultLocale()) 130 | } 131 | 132 | // NewWithLocale reads a desktop file from r and returns an Entry that 133 | // represents the desktop file using the given locale l. 134 | func NewWithLocale(r io.Reader, l keyfile.Locale) (*Entry, error) { 135 | kf, err := keyfile.New(r) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | // Create the entry. 141 | e := new(Entry) 142 | 143 | e.Type = ParseType(kf.Value(groupDesktopEntry, keyType)) 144 | if kf.KeyExists(groupDesktopEntry, keyVersion) { 145 | e.Version, err = kf.String(groupDesktopEntry, keyVersion) 146 | if err != nil { 147 | return nil, err 148 | } 149 | } 150 | e.Name, err = kf.LocaleStringWithLocale(groupDesktopEntry, keyName, l) 151 | if err != nil { 152 | return nil, err 153 | } 154 | if kf.KeyExists(groupDesktopEntry, keyGenericName) { 155 | e.GenericName, err = kf.LocaleStringWithLocale(groupDesktopEntry, keyGenericName, l) 156 | if err != nil { 157 | return nil, err 158 | } 159 | } 160 | if kf.KeyExists(groupDesktopEntry, keyNoDisplay) { 161 | e.NoDisplay, err = kf.Bool(groupDesktopEntry, keyNoDisplay) 162 | if err != nil { 163 | return nil, err 164 | } 165 | } 166 | if kf.KeyExists(groupDesktopEntry, keyComment) { 167 | e.Comment, err = kf.LocaleStringWithLocale(groupDesktopEntry, keyComment, l) 168 | if err != nil { 169 | return nil, err 170 | } 171 | } 172 | if kf.KeyExists(groupDesktopEntry, keyIcon) { 173 | e.Icon, err = kf.LocaleStringWithLocale(groupDesktopEntry, keyIcon, l) 174 | if err != nil { 175 | return nil, err 176 | } 177 | } 178 | if kf.KeyExists(groupDesktopEntry, keyHidden) { 179 | e.Hidden, err = kf.Bool(groupDesktopEntry, keyHidden) 180 | if err != nil { 181 | return nil, err 182 | } 183 | } 184 | if kf.KeyExists(groupDesktopEntry, keyOnlyShowIn) { 185 | e.OnlyShowIn, err = kf.StringList(groupDesktopEntry, keyOnlyShowIn) 186 | if err != nil { 187 | return nil, err 188 | } 189 | } 190 | if kf.KeyExists(groupDesktopEntry, keyNotShowIn) { 191 | e.NotShowIn, err = kf.StringList(groupDesktopEntry, keyNotShowIn) 192 | if err != nil { 193 | return nil, err 194 | } 195 | } 196 | if kf.KeyExists(groupDesktopEntry, keyDBusActivatable) { 197 | e.DBusActivatable, err = kf.Bool(groupDesktopEntry, keyDBusActivatable) 198 | if err != nil { 199 | return nil, err 200 | } 201 | } 202 | if kf.KeyExists(groupDesktopEntry, keyTryExec) { 203 | e.TryExec, err = kf.String(groupDesktopEntry, keyTryExec) 204 | if err != nil { 205 | return nil, err 206 | } 207 | } 208 | if kf.KeyExists(groupDesktopEntry, keyExec) { 209 | e.Exec, err = kf.String(groupDesktopEntry, keyExec) 210 | if err != nil { 211 | return nil, err 212 | } 213 | } 214 | if kf.KeyExists(groupDesktopEntry, keyPath) { 215 | e.Path, err = kf.String(groupDesktopEntry, keyPath) 216 | if err != nil { 217 | return nil, err 218 | } 219 | } 220 | if kf.KeyExists(groupDesktopEntry, keyTerminal) { 221 | e.Terminal, err = kf.Bool(groupDesktopEntry, keyTerminal) 222 | if err != nil { 223 | return nil, err 224 | } 225 | } 226 | if kf.KeyExists(groupDesktopEntry, keyActions) { 227 | e.Actions, err = getActions(kf) 228 | if err != nil { 229 | return nil, err 230 | } 231 | } 232 | if kf.KeyExists(groupDesktopEntry, keyMimeType) { 233 | e.MimeType, err = kf.StringList(groupDesktopEntry, keyMimeType) 234 | if err != nil { 235 | return nil, err 236 | } 237 | } 238 | if kf.KeyExists(groupDesktopEntry, keyCategories) { 239 | e.Categories, err = kf.StringList(groupDesktopEntry, keyCategories) 240 | if err != nil { 241 | return nil, err 242 | } 243 | } 244 | if kf.KeyExists(groupDesktopEntry, keyImplements) { 245 | e.Implements, err = kf.StringList(groupDesktopEntry, keyImplements) 246 | if err != nil { 247 | return nil, err 248 | } 249 | } 250 | if kf.KeyExists(groupDesktopEntry, keyKeywords) { 251 | e.Keywords, err = kf.LocaleStringListWithLocale(groupDesktopEntry, keyKeywords, l) 252 | if err != nil { 253 | return nil, err 254 | } 255 | } 256 | if kf.KeyExists(groupDesktopEntry, keyStartupNotify) { 257 | e.StartupNotify, err = kf.Bool(groupDesktopEntry, keyStartupNotify) 258 | if err != nil { 259 | return nil, err 260 | } 261 | } 262 | if kf.KeyExists(groupDesktopEntry, keyStartupWMClass) { 263 | e.StartupWMClass, err = kf.String(groupDesktopEntry, keyStartupWMClass) 264 | if err != nil { 265 | return nil, err 266 | } 267 | } 268 | if e.Type == Link { 269 | e.URL, err = kf.String(groupDesktopEntry, keyURL) 270 | if err != nil { 271 | return nil, err 272 | } 273 | } 274 | 275 | // Validate the entry. 276 | if e.Type == None { 277 | return nil, ErrMissingType 278 | } 279 | if e.Type > None && e.Type < Unknown && e.Name == "" { 280 | return nil, ErrMissingName 281 | } 282 | if e.Type == Link && e.URL == "" { 283 | return nil, ErrMissingURL 284 | } 285 | 286 | // Search for extended keys. 287 | e.X = make(map[string]map[string]string) 288 | for _, k := range kf.Keys(groupDesktopEntry) { 289 | a := strings.SplitN(k, "-", 3) 290 | if a[0] != "X" || len(a) < 3 { 291 | continue 292 | } 293 | if e.X[a[1]] == nil { 294 | e.X[a[1]] = make(map[string]string) 295 | } 296 | e.X[a[1]][a[2]] = kf.Value(groupDesktopEntry, k) 297 | } 298 | 299 | return e, nil 300 | } 301 | 302 | // Action is an Action group. 303 | type Action struct { 304 | Name string 305 | Icon string 306 | Exec string 307 | } 308 | 309 | func getActions(kf *keyfile.KeyFile) ([]*Action, error) { 310 | var acts []*Action 311 | var act *Action 312 | var err error 313 | var list []string 314 | 315 | list, err = kf.StringList(groupDesktopEntry, keyActions) 316 | if err != nil { 317 | return nil, err 318 | } 319 | for _, a := range list { 320 | g := groupDesktopActionPrefix + a 321 | 322 | act = new(Action) 323 | 324 | act.Name, err = kf.String(g, keyName) 325 | if err != nil { 326 | return nil, err 327 | } 328 | act.Icon, err = kf.String(g, keyIcon) 329 | if err != nil { 330 | return nil, err 331 | } 332 | act.Exec, err = kf.String(g, keyExec) 333 | if err != nil { 334 | return nil, err 335 | } 336 | 337 | acts = append(acts, act) 338 | } 339 | return acts, nil 340 | } 341 | -------------------------------------------------------------------------------- /desktop/entry_test.go: -------------------------------------------------------------------------------- 1 | package desktop 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | // specExample is the example desktop file given in the spec. 9 | const specExample = ` 10 | [Desktop Entry] 11 | Version=1.0 12 | Type=Application 13 | Name=Foo Viewer 14 | Comment=The best viewer for Foo objects available! 15 | TryExec=fooview 16 | Exec=fooview %F 17 | Icon=fooview 18 | MimeType=image/x-foo; 19 | Actions=Gallery;Create; 20 | 21 | [Desktop Action Gallery] 22 | Exec=fooview --gallery 23 | Name=Browse Gallery 24 | 25 | [Desktop Action Create] 26 | Exec=fooview --create-new 27 | Name=Create a new Foo! 28 | Icon=fooview-new 29 | ` 30 | 31 | func TestSpecExample(t *testing.T) { 32 | d, err := New(strings.NewReader(specExample)) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | 37 | if d.Type != Application { 38 | t.Error("Type") 39 | } 40 | 41 | arr := map[string]string{ 42 | d.Version: "1.0", 43 | d.Name: "Foo Viewer", 44 | d.Comment: "The best viewer for Foo objects available!", 45 | d.TryExec: "fooview", 46 | d.Exec: "fooview %F", 47 | d.Icon: "fooview", 48 | } 49 | for act, exp := range arr { 50 | if act != exp { 51 | t.Log("expected: " + exp) 52 | t.Log("actual: " + act) 53 | t.Fail() 54 | } 55 | } 56 | } 57 | 58 | func TestActions(t *testing.T) { 59 | d, err := New(strings.NewReader(specExample)) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | 64 | a := d.Actions 65 | if a[0].Name != "Browse Gallery" { 66 | t.Fail() 67 | } 68 | if a[0].Exec != "fooview --gallery" { 69 | t.Fail() 70 | } 71 | if a[1].Name != "Create a new Foo!" { 72 | t.Fail() 73 | } 74 | if a[1].Exec != "fooview --create-new" { 75 | t.Fail() 76 | } 77 | if a[1].Icon != "fooview-new" { 78 | t.Fail() 79 | } 80 | } 81 | 82 | const fullExample = ` 83 | # This example will use all the keys. 84 | [Desktop Entry] 85 | Type=Application 86 | Version=1.0 87 | 88 | Name=fullExample 89 | GenericName=Desktop Entry Test 90 | Comment=This is a test comment. 91 | Icon=test-icon 92 | 93 | NoDisplay=true 94 | Hidden=true 95 | OnlyShowIn=Unity;Gnome; 96 | NotShowIn=KDE;xfce; 97 | 98 | DBusActivatable=true 99 | TryExec=echo 100 | Exec=echo %F 101 | Path=/ 102 | Terminal=true 103 | 104 | Actions=NewFile;TacoSalad; 105 | MimeType=text/plain;text/markdown; 106 | Categories=Tests;Golang; 107 | Implements=Interface; 108 | Keywords=full;test;golang;xdg;desktop; 109 | 110 | StartupNotify=true 111 | StartupWMClass=test 112 | 113 | X-Unity-IconBackgroundColor=#000000 114 | X-Gnome-Something=foo 115 | X-KDE-plasma=workspaces 116 | 117 | [Desktop Action NewFile] 118 | Name=New File 119 | Exec=echo 120 | Icon=file 121 | 122 | [Desktop Action TacoSalad] 123 | Name=Taco Salad 124 | Exec=echo Taco Salad 125 | Icon=taco 126 | ` 127 | 128 | func TestPrintIt(t *testing.T) { 129 | d, err := New(strings.NewReader(fullExample)) 130 | if err != nil { 131 | t.Error(err) 132 | } 133 | 134 | t.Logf("%#v", d) 135 | } 136 | 137 | const linkExample = ` 138 | [Desktop Entry] 139 | Version=1.0 140 | Type=Link 141 | Name=Go 142 | Name[en_US]=Go 143 | URL=http://www.golang.org/ 144 | ` 145 | 146 | func TestLinkExample(t *testing.T) { 147 | d, err := New(strings.NewReader(linkExample)) 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | 152 | if d.Type != Link { 153 | t.Error("Type") 154 | } 155 | 156 | arr := map[string]string{ 157 | d.Version: "1.0", 158 | d.Name: "Go", 159 | d.URL: "http://www.golang.org/", 160 | } 161 | for act, exp := range arr { 162 | if act != exp { 163 | t.Log("expected: " + exp) 164 | t.Log("actual: " + act) 165 | t.Fail() 166 | } 167 | } 168 | } 169 | 170 | func TestEntryValidation(t *testing.T) { 171 | _, err := New(strings.NewReader("[Desktop Entry]\n")) 172 | if err != ErrMissingType { 173 | t.Errorf("expected ErrMissingType, got %v", err) 174 | } 175 | 176 | _, err = New(strings.NewReader("[Desktop Entry]\nType=Application\n")) 177 | if err != ErrMissingName { 178 | t.Errorf("expected ErrMissingName, got %v", err) 179 | } 180 | 181 | _, err = New(strings.NewReader("[Desktop Entry]\nType=Link\n")) 182 | if err != ErrMissingName { 183 | t.Errorf("expected ErrMissingName, got %v", err) 184 | } 185 | 186 | _, err = New(strings.NewReader("[Desktop Entry]\nType=Directory\n")) 187 | if err != ErrMissingName { 188 | t.Errorf("expected ErrMissingName, got %v", err) 189 | } 190 | 191 | _, err = New(strings.NewReader("[Desktop Entry]\nType=Link\nName=No URL\n")) 192 | if err != ErrMissingURL { 193 | t.Errorf("expected ErrMissingURL, got %v", err) 194 | } 195 | } 196 | 197 | func TestInvalidFileFormat(t *testing.T) { 198 | _, err := New(strings.NewReader("")) 199 | if err == nil { 200 | t.Error("err == nil on empty file") 201 | } 202 | 203 | _, err = New(strings.NewReader("hello, world!")) 204 | if err == nil { 205 | t.Error("err == nil on invalid file") 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /desktop/launch.go: -------------------------------------------------------------------------------- 1 | package desktop 2 | 3 | import () 4 | 5 | // Launch TODO 6 | func (de *Entry) Launch(uris ...string) error { 7 | panic("not implemented") 8 | } 9 | -------------------------------------------------------------------------------- /desktop/type.go: -------------------------------------------------------------------------------- 1 | package desktop 2 | 3 | // Type is the type of desktop entry. 4 | type Type uint8 5 | 6 | // These are the possible desktop entry types. 7 | const ( 8 | None Type = iota // No type. This is bad. 9 | Application 10 | Link 11 | Directory 12 | Unknown // Any unknown type. 13 | ) 14 | 15 | // ParseType converts the given string s into a Type. 16 | func ParseType(s string) Type { 17 | switch s { 18 | case None.String(): 19 | return None 20 | case Application.String(): 21 | return Application 22 | case Link.String(): 23 | return Link 24 | case Directory.String(): 25 | return Directory 26 | default: 27 | return Unknown 28 | } 29 | } 30 | 31 | // String returns the Type as a string. 32 | func (t Type) String() string { 33 | switch t { 34 | case None: 35 | return "" 36 | case Application: 37 | return "Application" 38 | case Link: 39 | return "Link" 40 | case Directory: 41 | return "Directory" 42 | default: 43 | return "Unknown" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /desktop/type_test.go: -------------------------------------------------------------------------------- 1 | package desktop 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestType(t *testing.T) { 8 | m := map[Type]string{ 9 | None: "", 10 | Application: "Application", 11 | Link: "Link", 12 | Directory: "Directory", 13 | Unknown: "Unknown", 14 | } 15 | 16 | for k, v := range m { 17 | if k != ParseType(v) { 18 | t.Fail() 19 | } 20 | if k.String() != v { 21 | t.Fail() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rkoesters/xdg 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rkoesters/xdg/c0064b6bca3c585b9509b2334232ea70170645f1/go.sum -------------------------------------------------------------------------------- /keyfile/boolean.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // Bool returns the value for group 'g' and key 'k' as a bool. 8 | func (kf *KeyFile) Bool(g, k string) (bool, error) { 9 | return strconv.ParseBool(kf.Value(g, k)) 10 | } 11 | -------------------------------------------------------------------------------- /keyfile/boolean_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | const testBool = ` 9 | [Header 1] 10 | yes=true 11 | no=false 12 | ` 13 | 14 | func TestBool(t *testing.T) { 15 | kf, err := New(strings.NewReader(testBool)) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | t.Log(kf) 20 | 21 | b, err := kf.Bool("Header 1", "yes") 22 | if b != true || err != nil { 23 | t.Fail() 24 | } 25 | b, err = kf.Bool("Header 1", "no") 26 | if b != false || err != nil { 27 | t.Fail() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /keyfile/errors.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // These errors can be returned if there is a problem processing a 8 | // keyfile's contents. 9 | var ( 10 | ErrInvalid = errors.New("invalid keyfile format") 11 | ErrBadEscapeSequence = errors.New("bad escape sequence") 12 | ErrUnexpectedEndOfString = errors.New("unexpected end of string") 13 | ) 14 | -------------------------------------------------------------------------------- /keyfile/keyfile.go: -------------------------------------------------------------------------------- 1 | // Package keyfile implements the ini file format that is used in many 2 | // of the xdg specs. 3 | // 4 | // WARNING: This package is meant for internal use and the API may 5 | // change without warning. 6 | package keyfile 7 | 8 | import ( 9 | "bufio" 10 | "bytes" 11 | "io" 12 | "strings" 13 | ) 14 | 15 | // KeyFile is an implementation of the keyfile format used by several 16 | // FreeDesktop.org (xdg) specs. The key values without a header section 17 | // can be accessed using an empty string as the group. 18 | type KeyFile struct { 19 | m map[string]map[string]string 20 | } 21 | 22 | // New creates a new KeyFile and returns it. 23 | func New(r io.Reader) (*KeyFile, error) { 24 | kf := new(KeyFile) 25 | kf.m = make(map[string]map[string]string) 26 | hdr := "" 27 | kf.m[hdr] = make(map[string]string) 28 | 29 | sc := bufio.NewScanner(r) 30 | for sc.Scan() { 31 | line := strings.TrimSpace(sc.Text()) 32 | switch { 33 | case len(line) == 0: 34 | // Empty line. 35 | case line[0] == '#': 36 | // Comment. 37 | case line[0] == '[' && line[len(line)-1] == ']': 38 | // Group header. 39 | hdr = line[1 : len(line)-1] 40 | kf.m[hdr] = make(map[string]string) 41 | case strings.Contains(line, "="): 42 | // Entry. 43 | p := strings.SplitN(line, "=", 2) 44 | p[0] = strings.TrimSpace(p[0]) 45 | p[1] = strings.TrimSpace(p[1]) 46 | kf.m[hdr][p[0]] = p[1] 47 | default: 48 | return nil, ErrInvalid 49 | } 50 | } 51 | return kf, nil 52 | } 53 | 54 | // Groups returns a slice of groups that exist for the KeyFile. 55 | func (kf *KeyFile) Groups() []string { 56 | groups := make([]string, 0, len(kf.m)) 57 | for k := range kf.m { 58 | groups = append(groups, k) 59 | } 60 | return groups 61 | } 62 | 63 | // GroupExists returns a bool indicating whether the given group 'g' 64 | // exists. 65 | func (kf *KeyFile) GroupExists(g string) bool { 66 | _, exists := kf.m[g] 67 | return exists 68 | } 69 | 70 | // Keys returns a slice of keys that exist for the given group 'g'. 71 | func (kf *KeyFile) Keys(g string) []string { 72 | keys := make([]string, 0, len(kf.m[g])) 73 | for k := range kf.m[g] { 74 | keys = append(keys, k) 75 | } 76 | return keys 77 | } 78 | 79 | // KeyExists returns a bool indicating whether the given group 'g' and 80 | // key 'k' exists. 81 | func (kf *KeyFile) KeyExists(g, k string) bool { 82 | _, exists := kf.m[g][k] 83 | return exists 84 | } 85 | 86 | // Value returns the raw string for group 'g' and key 'k'. Value will 87 | // return a blank string if the key doesn't exist; use GroupExists or 88 | // KeyExists to if you need to treat a missing value differently then a 89 | // blank value. 90 | func (kf *KeyFile) Value(g, k string) string { 91 | return kf.m[g][k] 92 | } 93 | 94 | // ValueList returns a slice of raw strings for group 'g' and key 'k'. 95 | // ValueList will return an empty slice if the key doesn't exist; use 96 | // GroupExists or KeyExists to if you need to treat a missing value 97 | // differently then a blank value. 98 | func (kf *KeyFile) ValueList(g, k string) ([]string, error) { 99 | var buf bytes.Buffer 100 | var isEscaped bool 101 | var list []string 102 | 103 | for _, r := range kf.Value(g, k) { 104 | if isEscaped { 105 | if r == ';' { 106 | buf.WriteRune(';') 107 | } else { 108 | // The escape sequence isn't '\;', so we 109 | // want to copy it over as is. 110 | buf.WriteRune('\\') 111 | buf.WriteRune(r) 112 | } 113 | isEscaped = false 114 | } else { 115 | switch r { 116 | case '\\': 117 | isEscaped = true 118 | case ';': 119 | list = append(list, buf.String()) 120 | buf.Reset() 121 | default: 122 | buf.WriteRune(r) 123 | } 124 | } 125 | } 126 | if isEscaped { 127 | return nil, ErrUnexpectedEndOfString 128 | } 129 | 130 | last := buf.String() 131 | if last != "" { 132 | list = append(list, last) 133 | } 134 | 135 | return list, nil 136 | } 137 | -------------------------------------------------------------------------------- /keyfile/keyfile_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const testParser = ` 10 | # This right here is a test file. 11 | [Header 1] 12 | key=value 13 | # This line is used to test extraneous white space. 14 | cat = dog 15 | [Header 2] 16 | # This line tests for extra equal signs. 17 | man = bear = pig 18 | ` 19 | 20 | func TestParser(t *testing.T) { 21 | kf, err := New(strings.NewReader(testParser)) 22 | if err != nil { 23 | t.Error(err) 24 | } 25 | t.Log(kf) 26 | 27 | s := kf.Value("Header 1", "key") 28 | if s != "value" { 29 | t.Error("basic usage") 30 | } 31 | s = kf.Value("Header 1", "cat") 32 | if s != "dog" { 33 | t.Error("whitespace") 34 | } 35 | s = kf.Value("Header 2", "man") 36 | if s != "bear = pig" { 37 | t.Error("equal signs") 38 | } 39 | 40 | // Groups() and Keys() should always lead to valid values. 41 | for _, group := range kf.Groups() { 42 | if !kf.GroupExists(group) { 43 | t.Errorf("GroupExists == false for group='%v'", group) 44 | } 45 | 46 | for _, key := range kf.Keys(group) { 47 | if !kf.KeyExists(group, key) { 48 | t.Errorf("KeyExists == false for group='%v' key='%v'", group, key) 49 | } 50 | } 51 | } 52 | } 53 | 54 | const testInvalid = ` 55 | # This example will have an invalid line. 56 | [Header 1] 57 | key=value 58 | hello world! 59 | ` 60 | 61 | func TestInvalid(t *testing.T) { 62 | _, err := New(strings.NewReader(testInvalid)) 63 | if err != ErrInvalid { 64 | t.Fail() 65 | } 66 | } 67 | 68 | const testList = ` 69 | # This tests that the formatting functions work. 70 | [Header 1] 71 | list=man;bear;pig; 72 | list2=man\;bear;pig\r; 73 | list3=man;bear;pig 74 | ` 75 | 76 | func TestList(t *testing.T) { 77 | kf, err := New(strings.NewReader(testList)) 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | 82 | expect := []string{"man", "bear", "pig"} 83 | actual, err := kf.ValueList("Header 1", "list") 84 | if err != nil { 85 | t.Error(err) 86 | } 87 | t.Log(expect) 88 | t.Log(actual) 89 | if !reflect.DeepEqual(actual, expect) { 90 | t.Fail() 91 | } 92 | 93 | expect = []string{"man;bear", "pig\\r"} 94 | actual, err = kf.ValueList("Header 1", "list2") 95 | if err != nil { 96 | t.Error(err) 97 | } 98 | t.Log(expect) 99 | t.Log(actual) 100 | if !reflect.DeepEqual(actual, expect) { 101 | t.Fail() 102 | } 103 | 104 | expect = []string{"man", "bear", "pig"} 105 | actual, err = kf.ValueList("Header 1", "list3") 106 | if err != nil { 107 | t.Error(err) 108 | } 109 | t.Log(expect) 110 | t.Log(actual) 111 | if !reflect.DeepEqual(actual, expect) { 112 | t.Fail() 113 | } 114 | } 115 | 116 | func TestExists(t *testing.T) { 117 | kf, err := New(strings.NewReader("")) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | 122 | if !kf.GroupExists("") { 123 | t.Fail() 124 | } 125 | 126 | if kf.GroupExists("group") { 127 | t.Fail() 128 | } 129 | 130 | if kf.KeyExists("", "") { 131 | t.Fail() 132 | } 133 | 134 | if kf.KeyExists("group", "") { 135 | t.Fail() 136 | } 137 | 138 | if kf.KeyExists("", "key") { 139 | t.Fail() 140 | } 141 | 142 | if kf.KeyExists("group", "key") { 143 | t.Fail() 144 | } 145 | } 146 | 147 | const testListUnexpectedEndOfString = ` 148 | list=asdf;asdf;\` 149 | 150 | func TestListUnexpectedEndOfString(t *testing.T) { 151 | kf, err := New(strings.NewReader(testListUnexpectedEndOfString)) 152 | if err != nil { 153 | t.FailNow() 154 | } 155 | 156 | _, err = kf.ValueList("", "list") 157 | if err != ErrUnexpectedEndOfString { 158 | t.Fail() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /keyfile/locale.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "os" 7 | ) 8 | 9 | // Locale represents a locale for use in parsing translatable strings. 10 | type Locale struct { 11 | lang string 12 | country string 13 | encoding string 14 | modifier string 15 | } 16 | 17 | var defaultLocale *Locale 18 | 19 | // DefaultLocale returns the locale specified by the environment. 20 | func DefaultLocale() Locale { 21 | if defaultLocale == nil { 22 | val := os.Getenv("LANGUAGE") 23 | if val == "" { 24 | val = os.Getenv("LC_ALL") 25 | if val == "" { 26 | val = os.Getenv("LC_MESSAGES") 27 | if val == "" { 28 | val = os.Getenv("LANG") 29 | } 30 | } 31 | } 32 | 33 | l, err := ParseLocale(val) 34 | if err == nil { 35 | defaultLocale = &l 36 | } else { 37 | defaultLocale = &Locale{} 38 | } 39 | } 40 | return *defaultLocale 41 | } 42 | 43 | // ErrBadLocaleFormat is returned by ParseLocale when the given string 44 | // is not formatted properly (for example, missing the lang component). 45 | var ErrBadLocaleFormat = errors.New("bad locale format") 46 | 47 | // ParseLocale parses a locale in the format 48 | // 49 | // lang_COUNTRY.ENCODING@MODIFIER 50 | // 51 | // where "_COUNTRY", ".ENCODING", and "@MODIFIER" can be omitted. A 52 | // blank string, "C", and "POSIX" are special cases that evaluate to a 53 | // blank Locale. 54 | func ParseLocale(s string) (Locale, error) { 55 | // A blank string, "C", and "POSIX" are valid locales, they 56 | // evaluate to a blank Locale. 57 | if s == "" || s == "C" || s == "POSIX" { 58 | return Locale{}, nil 59 | } 60 | 61 | var buf bytes.Buffer 62 | var l Locale 63 | 64 | i := 0 65 | 66 | // lang 67 | for i < len(s) && s[i] != '_' && s[i] != '.' && s[i] != '@' { 68 | buf.WriteByte(s[i]) 69 | i++ 70 | } 71 | l.lang = buf.String() 72 | buf.Reset() 73 | 74 | // lang is required. 75 | if l.lang == "" { 76 | return Locale{}, ErrBadLocaleFormat 77 | } 78 | 79 | // COUNTRY 80 | if i < len(s) && s[i] == '_' { 81 | i++ 82 | for i < len(s) && s[i] != '.' && s[i] != '@' { 83 | buf.WriteByte(s[i]) 84 | i++ 85 | } 86 | l.country = buf.String() 87 | buf.Reset() 88 | } 89 | 90 | // ENCODING 91 | if i < len(s) && s[i] == '.' { 92 | i++ 93 | for i < len(s) && s[i] != '@' { 94 | buf.WriteByte(s[i]) 95 | i++ 96 | } 97 | l.encoding = buf.String() 98 | buf.Reset() 99 | } 100 | 101 | // MODIFIER 102 | if i < len(s) && s[i] == '@' { 103 | i++ 104 | for i < len(s) { 105 | buf.WriteByte(s[i]) 106 | i++ 107 | } 108 | l.modifier = buf.String() 109 | } 110 | 111 | return l, nil 112 | } 113 | 114 | // String returns the given locale as a formatted string. The returned 115 | // string is in the same format expected by ParseLocale. 116 | func (l Locale) String() string { 117 | var buf bytes.Buffer 118 | 119 | buf.WriteString(l.lang) 120 | 121 | if l.country != "" { 122 | buf.WriteRune('_') 123 | buf.WriteString(l.country) 124 | } 125 | 126 | if l.encoding != "" { 127 | buf.WriteRune('.') 128 | buf.WriteString(l.encoding) 129 | } 130 | 131 | if l.modifier != "" { 132 | buf.WriteRune('@') 133 | buf.WriteString(l.modifier) 134 | } 135 | 136 | return buf.String() 137 | } 138 | 139 | // Variants returns a sorted slice of Locales that should be checked for 140 | // when resolving a localestring. 141 | func (l Locale) Variants() []Locale { 142 | variants := make([]Locale, 0, 4) 143 | 144 | hasLang := l.lang != "" 145 | hasCountry := l.country != "" 146 | hasModifier := l.modifier != "" 147 | 148 | if hasLang && hasCountry && hasModifier { 149 | variants = append(variants, Locale{ 150 | lang: l.lang, 151 | country: l.country, 152 | modifier: l.modifier, 153 | }) 154 | } 155 | 156 | if hasLang && hasCountry { 157 | variants = append(variants, Locale{ 158 | lang: l.lang, 159 | country: l.country, 160 | }) 161 | } 162 | 163 | if hasLang && hasModifier { 164 | variants = append(variants, Locale{ 165 | lang: l.lang, 166 | modifier: l.modifier, 167 | }) 168 | } 169 | 170 | if hasLang { 171 | variants = append(variants, Locale{ 172 | lang: l.lang, 173 | }) 174 | } 175 | 176 | return variants 177 | } 178 | -------------------------------------------------------------------------------- /keyfile/locale_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParseLocale(t *testing.T) { 9 | var localeStr string 10 | var locale Locale 11 | var err error 12 | 13 | localeStr = "" 14 | locale, err = ParseLocale(localeStr) 15 | if err != nil { 16 | t.Error(err) 17 | } 18 | if localeStr != locale.String() { 19 | t.Errorf("'%v' != '%v'", localeStr, locale) 20 | } 21 | 22 | localeStr = "en_US.UTF-8@mod" 23 | locale, err = ParseLocale(localeStr) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | if localeStr != locale.String() { 28 | t.Errorf("'%v' != '%v'", localeStr, locale) 29 | } 30 | 31 | localeStr = "en_US@mod" 32 | locale, err = ParseLocale(localeStr) 33 | if err != nil { 34 | t.Error(err) 35 | } 36 | if localeStr != locale.String() { 37 | t.Errorf("'%v' != '%v'", localeStr, locale) 38 | } 39 | 40 | localeStr = "en_US" 41 | locale, err = ParseLocale(localeStr) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | if localeStr != locale.String() { 46 | t.Errorf("'%v' != '%v'", localeStr, locale) 47 | } 48 | 49 | localeStr = "en@mod" 50 | locale, err = ParseLocale(localeStr) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | if localeStr != locale.String() { 55 | t.Errorf("'%v' != '%v'", localeStr, locale) 56 | } 57 | 58 | localeStr = "en" 59 | locale, err = ParseLocale(localeStr) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | if localeStr != locale.String() { 64 | t.Errorf("'%v' != '%v'", localeStr, locale) 65 | } 66 | } 67 | 68 | func TestDefaultLocale(t *testing.T) { 69 | t.Log(DefaultLocale()) 70 | } 71 | 72 | func TestBadLocale(t *testing.T) { 73 | _, err := ParseLocale("_US") 74 | if err != ErrBadLocaleFormat { 75 | t.Fail() 76 | } 77 | 78 | _, err = ParseLocale(".UTF-8") 79 | if err != ErrBadLocaleFormat { 80 | t.Fail() 81 | } 82 | 83 | _, err = ParseLocale("@Latn") 84 | if err != ErrBadLocaleFormat { 85 | t.Fail() 86 | } 87 | } 88 | 89 | func TestSpecialLocale(t *testing.T) { 90 | blank, err := ParseLocale("") 91 | if err != nil { 92 | t.Fail() 93 | } 94 | 95 | c, err := ParseLocale("C") 96 | if err != nil || !reflect.DeepEqual(c, blank) { 97 | t.Fail() 98 | } 99 | 100 | posix, err := ParseLocale("POSIX") 101 | if err != nil || !reflect.DeepEqual(posix, blank) { 102 | t.Fail() 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /keyfile/localestring.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // LocaleString returns the value for group 'g' and key 'k' using the 8 | // system's locale. 9 | func (kf *KeyFile) LocaleString(g, k string) (string, error) { 10 | return kf.LocaleStringWithLocale(g, k, DefaultLocale()) 11 | } 12 | 13 | // LocaleStringWithLocale returns the value for group 'g', key 'k', and 14 | // locale 'l'. 15 | func (kf *KeyFile) LocaleStringWithLocale(g, k string, l Locale) (string, error) { 16 | for _, locale := range l.Variants() { 17 | key := fmt.Sprintf("%v[%v]", k, locale) 18 | if kf.KeyExists(g, key) { 19 | return kf.String(g, key) 20 | } 21 | } 22 | return kf.String(g, k) 23 | } 24 | 25 | // LocaleStringList returns a slice of strings for group 'g' and key 26 | // 'k'. 27 | func (kf *KeyFile) LocaleStringList(g, k string) ([]string, error) { 28 | return kf.LocaleStringListWithLocale(g, k, DefaultLocale()) 29 | } 30 | 31 | // LocaleStringListWithLocale returns a slice of strings for group 'g', 32 | // key 'k', and locale 'l'. 33 | func (kf *KeyFile) LocaleStringListWithLocale(g, k string, l Locale) ([]string, error) { 34 | for _, locale := range l.Variants() { 35 | key := fmt.Sprintf("%v[%v]", k, locale) 36 | if kf.KeyExists(g, key) { 37 | return kf.StringList(g, key) 38 | } 39 | } 40 | return kf.StringList(g, k) 41 | } 42 | -------------------------------------------------------------------------------- /keyfile/localestring_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | const localeStringExample = ` 10 | [Header 1] 11 | Key=Value 0 12 | Key[en_US]=Value 1 13 | Key[en_UK]=Value 2 14 | List=one;two;three; 15 | List[en_US]=four;five;six; 16 | List[en_UK]=seven;eight;nine; 17 | ` 18 | 19 | func TestLocaleString(t *testing.T) { 20 | kf, err := New(strings.NewReader(localeStringExample)) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | t.Log(kf) 25 | 26 | _, err = kf.LocaleString("Header 1", "Key") 27 | if err != nil { 28 | t.Error(err) 29 | } 30 | 31 | s, err := kf.LocaleStringWithLocale("Header 1", "Key", Locale{}) 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | if s != "Value 0" { 36 | t.Errorf("expected=Value 0 real=%v", s) 37 | } 38 | 39 | locale, err := ParseLocale("en_US") 40 | if err != nil { 41 | t.Error(err) 42 | } 43 | s, err = kf.LocaleStringWithLocale("Header 1", "Key", locale) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | if s != "Value 1" { 48 | t.Errorf("expected=Value 1 real=%v", s) 49 | } 50 | 51 | locale, err = ParseLocale("en_UK") 52 | if err != nil { 53 | t.Error(err) 54 | } 55 | t.Log(locale) 56 | s, err = kf.LocaleStringWithLocale("Header 1", "Key", locale) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | if s != "Value 2" { 61 | t.Errorf("expected=Value 2 real=%v", s) 62 | } 63 | 64 | locale, err = ParseLocale("en_US.UTF-8@MOD") 65 | if err != nil { 66 | t.Error(err) 67 | } 68 | s, err = kf.LocaleStringWithLocale("Header 1", "Key", locale) 69 | if err != nil { 70 | t.Error(err) 71 | } 72 | if s != "Value 1" { 73 | t.Errorf("expected=Value 1 real=%v", s) 74 | } 75 | 76 | locale, err = ParseLocale("en_UK.UTF-8@MOD") 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | t.Log(locale) 81 | s, err = kf.LocaleStringWithLocale("Header 1", "Key", locale) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | if s != "Value 2" { 86 | t.Errorf("expected=Value 2 real=%v", s) 87 | } 88 | } 89 | 90 | func TestLocaleStringList(t *testing.T) { 91 | kf, err := New(strings.NewReader(localeStringExample)) 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | t.Log(kf) 96 | 97 | _, err = kf.LocaleStringList("Header 1", "List") 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | 102 | s, err := kf.LocaleStringListWithLocale("Header 1", "List", Locale{}) 103 | if err != nil { 104 | t.Error(err) 105 | } 106 | if !reflect.DeepEqual(s, []string{"one", "two", "three"}) { 107 | t.Errorf("expected=Value 0 real=%v", s) 108 | } 109 | 110 | locale, err := ParseLocale("en_US") 111 | if err != nil { 112 | t.Error(err) 113 | } 114 | s, err = kf.LocaleStringListWithLocale("Header 1", "List", locale) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | if !reflect.DeepEqual(s, []string{"four", "five", "six"}) { 119 | t.Errorf("expected=Value 1 real=%v", s) 120 | } 121 | 122 | locale, err = ParseLocale("en_UK") 123 | if err != nil { 124 | t.Error(err) 125 | } 126 | t.Log(locale) 127 | s, err = kf.LocaleStringListWithLocale("Header 1", "List", locale) 128 | if err != nil { 129 | t.Error(err) 130 | } 131 | if !reflect.DeepEqual(s, []string{"seven", "eight", "nine"}) { 132 | t.Errorf("expected=Value 2 real=%v", s) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /keyfile/number.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | // Number returns the value for group 'g' and key 'k' as a float64. 8 | func (kf *KeyFile) Number(g, k string) (float64, error) { 9 | return strconv.ParseFloat(kf.Value(g, k), 64) 10 | } 11 | -------------------------------------------------------------------------------- /keyfile/number_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | const testNumber = ` 9 | [Header 1] 10 | zero=0 11 | one=1 12 | ten=10 13 | negative-five=-5 14 | pi=3.1415 15 | ` 16 | 17 | func TestNumber(t *testing.T) { 18 | kf, err := New(strings.NewReader(testNumber)) 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | t.Log(kf) 23 | 24 | n, err := kf.Number("Header 1", "zero") 25 | if n != 0 || err != nil { 26 | t.Fail() 27 | } 28 | 29 | n, err = kf.Number("Header 1", "one") 30 | if n != 1 || err != nil { 31 | t.Fail() 32 | } 33 | 34 | n, err = kf.Number("Header 1", "ten") 35 | if n != 10 || err != nil { 36 | t.Fail() 37 | } 38 | 39 | n, err = kf.Number("Header 1", "negative-five") 40 | if n != -5 || err != nil { 41 | t.Fail() 42 | } 43 | 44 | n, err = kf.Number("Header 1", "pi") 45 | if n != 3.1415 || err != nil { 46 | t.Fail() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /keyfile/string.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // String returns the value for group 'g' and key 'k' as a string. 8 | // String will return a blank string if the key doesn't exist; use 9 | // GroupExists or KeyExists to if you need to treat a missing value 10 | // differently then a blank value. 11 | func (kf *KeyFile) String(g, k string) (string, error) { 12 | return unescapeString(kf.Value(g, k)) 13 | } 14 | 15 | // StringList returns a slice of strings for group 'g' and key 'k'. 16 | // StringList will return an empty slice if the key doesn't exist; use 17 | // GroupExists or KeyExists to if you need to treat a missing value 18 | // differently then a blank value. 19 | func (kf *KeyFile) StringList(g, k string) ([]string, error) { 20 | vlist, err := kf.ValueList(g, k) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | slist := make([]string, len(vlist), len(vlist)) 26 | for i, val := range vlist { 27 | slist[i], err = unescapeString(val) 28 | if err != nil { 29 | return nil, err 30 | } 31 | } 32 | 33 | return slist, nil 34 | } 35 | 36 | func unescapeString(s string) (string, error) { 37 | var buf bytes.Buffer 38 | var isEscaped bool 39 | var err error 40 | 41 | for _, r := range s { 42 | if isEscaped { 43 | switch r { 44 | case 's': 45 | _, err = buf.WriteRune(' ') 46 | case 'n': 47 | _, err = buf.WriteRune('\n') 48 | case 't': 49 | _, err = buf.WriteRune('\t') 50 | case 'r': 51 | _, err = buf.WriteRune('\r') 52 | case '\\': 53 | _, err = buf.WriteRune('\\') 54 | default: 55 | err = ErrBadEscapeSequence 56 | } 57 | 58 | if err != nil { 59 | return "", err 60 | } 61 | 62 | isEscaped = false 63 | } else { 64 | if r == '\\' { 65 | isEscaped = true 66 | } else { 67 | buf.WriteRune(r) 68 | } 69 | } 70 | } 71 | if isEscaped { 72 | return "", ErrUnexpectedEndOfString 73 | } 74 | return buf.String(), nil 75 | } 76 | -------------------------------------------------------------------------------- /keyfile/string_test.go: -------------------------------------------------------------------------------- 1 | package keyfile 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestUnescapeString(t *testing.T) { 9 | goodData := map[string]string{ 10 | "": "", 11 | "asdf": "asdf", 12 | "asdf\\\\asdf": "asdf\\asdf", 13 | "\\sasdf": " asdf", 14 | "hello\\nworld": "hello\nworld", 15 | "asdf\\tasdf": "asdf\tasdf", 16 | "asdf\\rasdf": "asdf\rasdf", 17 | } 18 | for input, expected := range goodData { 19 | str, err := unescapeString(input) 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | if str != expected { 24 | t.Errorf("error escapeing '%v'", input) 25 | } 26 | } 27 | 28 | badData := map[string]error{ 29 | "\\": ErrUnexpectedEndOfString, 30 | "\\p": ErrBadEscapeSequence, 31 | } 32 | for input, expected := range badData { 33 | _, err := unescapeString(input) 34 | if err != expected { 35 | t.Error(err) 36 | } 37 | } 38 | } 39 | 40 | func TestBadStringList(t *testing.T) { 41 | const badList = `list=asdf;asdf\` 42 | 43 | kf, err := New(strings.NewReader(badList)) 44 | if err != nil { 45 | t.Fail() 46 | } else { 47 | _, err = kf.StringList("", "list") 48 | if err != ErrUnexpectedEndOfString { 49 | t.Fail() 50 | } 51 | } 52 | 53 | const badStringInList = `list=asdf;as\qasd;` 54 | 55 | kf, err = New(strings.NewReader(badStringInList)) 56 | if err != nil { 57 | t.Fail() 58 | } else { 59 | _, err = kf.StringList("", "list") 60 | if err != ErrBadEscapeSequence { 61 | t.Fail() 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /open.go: -------------------------------------------------------------------------------- 1 | // Package xdg provides access to the XDG specs. Most of the 2 | // functionality can be found in the subpackages. 3 | package xdg 4 | 5 | import ( 6 | "errors" 7 | "os/exec" 8 | ) 9 | 10 | // These errors can be returned by Open. 11 | var ( 12 | ErrSyntax = errors.New("error in command line syntax") 13 | ErrFileNotFound = errors.New("one of the files passed on the command line did not exist") 14 | ErrToolNotFound = errors.New("a required tool could not be found") 15 | ErrFailed = errors.New("the action failed") 16 | ) 17 | 18 | // Open runs the command xdg-open with the given uri as an argument. 19 | func Open(uri string) error { 20 | c := exec.Command("xdg-open", uri) 21 | err := c.Run() 22 | if err != nil { 23 | switch err.Error() { 24 | case "exit status 1": 25 | return ErrSyntax 26 | case "exit status 2": 27 | return ErrFileNotFound 28 | case "exit status 3": 29 | return ErrToolNotFound 30 | case "exit status 4": 31 | return ErrFailed 32 | default: 33 | return err 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /trash/doc.go: -------------------------------------------------------------------------------- 1 | // Package trash provides easy access to the trash bin. For more 2 | // information, see the trash spec: 3 | // https://standards.freedesktop.org/trash-spec/trashspec-latest.html 4 | // 5 | // TODO: Make New work. 6 | package trash 7 | -------------------------------------------------------------------------------- /trash/home.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "github.com/rkoesters/xdg/basedir" 5 | "path/filepath" 6 | ) 7 | 8 | var hometrash *Dir 9 | 10 | func init() { 11 | hometrash = &Dir{filepath.Join(basedir.DataHome, "Trash")} 12 | } 13 | 14 | // Files returns a slice of the files in the default trash. 15 | func Files() ([]string, error) { return hometrash.Files() } 16 | 17 | // Stat returns the Info for the given file in the trash. 18 | func Stat(s string) (*Info, error) { return hometrash.Stat(s) } 19 | 20 | // Trash moves the given file to the trash. 21 | func Trash(p string) error { return hometrash.Trash(p) } 22 | 23 | // Restore moves the file from the trash to its original location. 24 | func Restore(s string) error { return hometrash.Restore(s) } 25 | 26 | // RestoreTo moves the file from the trash to the specified location. 27 | func RestoreTo(s, p string) error { return hometrash.RestoreTo(s, p) } 28 | 29 | // Erase removes the given file from the trash. 30 | func Erase(s string) error { return hometrash.Erase(s) } 31 | 32 | // EraseAll removes the given file and all children from the trash. 33 | func EraseAll(s string) error { return hometrash.EraseAll(s) } 34 | 35 | // Empty erases all files in the trash. 36 | func Empty() error { return hometrash.Empty() } 37 | 38 | // IsEmpty returns whether or not the trash is empty. 39 | func IsEmpty() bool { return hometrash.IsEmpty() } 40 | -------------------------------------------------------------------------------- /trash/info.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "fmt" 5 | "github.com/rkoesters/xdg/keyfile" 6 | "io" 7 | "net/url" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | const ( 13 | trashInfo = "Trash Info" 14 | 15 | timeFormat = "2006-01-02T15:04:05" 16 | ) 17 | 18 | // Info represents a .trashinfo file. 19 | type Info struct { 20 | Path string 21 | DeletionDate time.Time 22 | } 23 | 24 | // NewInfo creates a new Info using the given io.Reader. 25 | func NewInfo(r io.Reader) (*Info, error) { 26 | kf, err := keyfile.New(r) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | info := new(Info) 32 | tmp, err := kf.String(trashInfo, "Path") 33 | if err != nil { 34 | return nil, err 35 | } 36 | info.Path, err = url.QueryUnescape(tmp) 37 | if err != nil { 38 | return nil, err 39 | } 40 | tmp, err = kf.String(trashInfo, "DeletionDate") 41 | if err != nil { 42 | return nil, err 43 | } 44 | info.DeletionDate, err = time.ParseInLocation(timeFormat, tmp, time.Local) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return info, nil 49 | } 50 | 51 | // String returns Info as a string in the INI format. 52 | func (i *Info) String() string { 53 | return fmt.Sprintf( 54 | "[Trash Info]\nPath=%v\nDeletionDate=%v\n", 55 | queryEscape(i.Path), 56 | i.DeletionDate.Format(timeFormat), 57 | ) 58 | } 59 | 60 | // queryEscape is a wrapper function around url.QueryEscape that doesn't 61 | // escape '/'. 62 | func queryEscape(s string) string { 63 | // The first for loop is the workaround for "/". 64 | a := strings.Split(s, "/") 65 | for i := 0; i < len(a); i++ { 66 | // The second for loop is the workaround for " ". 67 | b := strings.Split(a[i], " ") 68 | for j := 0; j < len(b); j++ { 69 | b[j] = url.QueryEscape(b[j]) 70 | } 71 | a[i] = strings.Join(b, "%20") 72 | } 73 | return strings.Join(a, "/") 74 | } 75 | -------------------------------------------------------------------------------- /trash/info_test.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | const trashinfo = `[Trash Info] 9 | Path=/home/user/file.txt 10 | DeletionDate=2006-01-02T15:04:05 11 | ` 12 | 13 | func TestInfo(t *testing.T) { 14 | r := strings.NewReader(trashinfo) 15 | info, err := NewInfo(r) 16 | if err != nil { 17 | t.Error(err) 18 | } 19 | t.Log(trashinfo) 20 | t.Log(info) 21 | 22 | if info.String() != trashinfo { 23 | t.Fail() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /trash/trash.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | // Dir represents a trash directory. 12 | type Dir struct { 13 | path string 14 | } 15 | 16 | // New creates a new Trash directory. 17 | func New(root string) (*Dir, error) { 18 | // TODO: We need to check permissions and stuff on creation of a 19 | // new trash. This means we need to check for a sticky bit and a 20 | // bunch of other stuff. 21 | panic("TODO") 22 | } 23 | 24 | func (d *Dir) file2path(s string) string { 25 | return filepath.Join(d.path, "files", s) 26 | } 27 | 28 | func (d *Dir) info2path(s string) string { 29 | return filepath.Join(d.path, "info", s+".trashinfo") 30 | } 31 | 32 | // Files returns a slice of the files in the trash. 33 | func (d *Dir) Files() ([]string, error) { 34 | dir, err := os.Open(filepath.Join(d.path, "files")) 35 | if err != nil { 36 | return nil, err 37 | } 38 | defer dir.Close() 39 | return dir.Readdirnames(0) 40 | } 41 | 42 | // Stat returns the Info for the given file in the trash. 43 | func (d *Dir) Stat(s string) (*Info, error) { 44 | f, err := os.Open(d.info2path(s)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer f.Close() 49 | return NewInfo(f) 50 | } 51 | 52 | // Trash moves the file at the given path to the trash. 53 | func (d *Dir) Trash(p string) error { 54 | // Find an open file name. 55 | tname := filepath.Base(p) 56 | for i := 2; d.exists(tname); i++ { 57 | tname = filepath.Base(p) + "." + strconv.Itoa(i) 58 | } 59 | 60 | // First, write the trashinfo file. 61 | abs, err := filepath.Abs(p) 62 | if err != nil { 63 | return err 64 | } 65 | info := &Info{abs, time.Now()} 66 | err = ioutil.WriteFile(d.info2path(tname), []byte(info.String()), os.ModePerm) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | // Next, move the file to the trash. 72 | return os.Rename(p, d.file2path(tname)) 73 | } 74 | 75 | // Restore moves the file from the trash to its original location. 76 | func (d *Dir) Restore(s string) error { 77 | info, err := d.Stat(s) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return d.RestoreTo(s, info.Path) 83 | } 84 | 85 | // RestoreTo moves the file from the trash to the specified location. 86 | func (d *Dir) RestoreTo(s, p string) error { 87 | _, err := os.Stat(p) 88 | if err == nil { 89 | return os.ErrExist 90 | } 91 | 92 | return os.Rename(d.file2path(s), p) 93 | } 94 | 95 | func (d *Dir) exists(s string) bool { 96 | _, err := os.Stat(d.file2path(s)) 97 | if os.IsNotExist(err) { 98 | return false 99 | } 100 | return true 101 | } 102 | 103 | // Erase removes the given file from the trash. 104 | func (d *Dir) Erase(s string) error { 105 | err := os.Remove(d.file2path(s)) 106 | if err != nil { 107 | return err 108 | } 109 | err = os.Remove(d.info2path(s)) 110 | if err != nil { 111 | return err 112 | } 113 | return nil 114 | } 115 | 116 | // EraseAll removes the given file and any children it contains. 117 | func (d *Dir) EraseAll(s string) error { 118 | err := os.RemoveAll(d.file2path(s)) 119 | if err != nil { 120 | return err 121 | } 122 | err = os.RemoveAll(d.info2path(s)) 123 | if err != nil { 124 | return nil 125 | } 126 | return nil 127 | } 128 | 129 | // Empty erases all the files in the trash. 130 | func (d *Dir) Empty() error { 131 | files, err := d.Files() 132 | if err != nil { 133 | return err 134 | } 135 | for _, i := range files { 136 | err = d.EraseAll(i) 137 | if err != nil { 138 | return err 139 | } 140 | } 141 | return nil 142 | } 143 | 144 | // IsEmpty returns whether or not the trash is empty. 145 | func (d *Dir) IsEmpty() bool { 146 | files, _ := d.Files() 147 | return len(files) == 0 148 | } 149 | -------------------------------------------------------------------------------- /trash/trash_test.go: -------------------------------------------------------------------------------- 1 | package trash 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestListFiles(t *testing.T) { 8 | files, err := Files() 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | for _, i := range files { 13 | t.Log(i) 14 | } 15 | } 16 | 17 | func TestStat(t *testing.T) { 18 | files, err := Files() 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | for _, i := range files { 23 | info, err := Stat(i) 24 | if err != nil { 25 | t.Error(err) 26 | } 27 | t.Log(info) 28 | } 29 | } 30 | 31 | func TestIsEmpty(t *testing.T) { 32 | // We can't expect a value for IsEmpty because it depends on the 33 | // contents of the user's trash which could be empty or not. So 34 | // we just call the function to make sure it doesn't panic or 35 | // anything extreme. 36 | t.Logf("IsEmpty()=%v", IsEmpty()) 37 | } 38 | -------------------------------------------------------------------------------- /userdirs/userdirs.go: -------------------------------------------------------------------------------- 1 | // Package userdirs provides easy access to "well known" user 2 | // directories. For more information, see: 3 | // https://www.freedesktop.org/wiki/Software/xdg-user-dirs/ 4 | package userdirs 5 | 6 | import ( 7 | "github.com/rkoesters/xdg/basedir" 8 | "github.com/rkoesters/xdg/keyfile" 9 | "io" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // The default set of userdirs. Most people will only need to use this. 16 | var ( 17 | Desktop string 18 | Documents string 19 | Download string 20 | Music string 21 | Pictures string 22 | PublicShare string 23 | Templates string 24 | Videos string 25 | ) 26 | 27 | func init() { 28 | f, err := os.Open(filepath.Join(basedir.ConfigHome, "user-dirs.dirs")) 29 | if err != nil { 30 | return 31 | } 32 | defer f.Close() 33 | 34 | dirs, err := New(f) 35 | if err != nil { 36 | return 37 | } 38 | 39 | Desktop = dirs.Desktop 40 | Documents = dirs.Documents 41 | Download = dirs.Download 42 | Music = dirs.Music 43 | Pictures = dirs.Pictures 44 | PublicShare = dirs.PublicShare 45 | Templates = dirs.Templates 46 | Videos = dirs.Videos 47 | } 48 | 49 | // UserDirs is a set of user directories that are common in graphical 50 | // environments. 51 | type UserDirs struct { 52 | Desktop string 53 | Documents string 54 | Download string 55 | Music string 56 | Pictures string 57 | PublicShare string 58 | Templates string 59 | Videos string 60 | } 61 | 62 | // New creates a new UserDirs struct by reading from the given 63 | // io.Reader. 64 | func New(r io.Reader) (*UserDirs, error) { 65 | kf, err := keyfile.New(r) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | dirs := new(UserDirs) 71 | 72 | dirs.Desktop, err = parse(kf.String("", "XDG_DESKTOP_DIR")) 73 | if err != nil { 74 | return nil, err 75 | } 76 | dirs.Documents, err = parse(kf.String("", "XDG_DOCUMENTS_DIR")) 77 | if err != nil { 78 | return nil, err 79 | } 80 | dirs.Download, err = parse(kf.String("", "XDG_DOWNLOAD_DIR")) 81 | if err != nil { 82 | return nil, err 83 | } 84 | dirs.Music, err = parse(kf.String("", "XDG_MUSIC_DIR")) 85 | if err != nil { 86 | return nil, err 87 | } 88 | dirs.Pictures, err = parse(kf.String("", "XDG_PICTURES_DIR")) 89 | if err != nil { 90 | return nil, err 91 | } 92 | dirs.PublicShare, err = parse(kf.String("", "XDG_PUBLICSHARE_DIR")) 93 | if err != nil { 94 | return nil, err 95 | } 96 | dirs.Templates, err = parse(kf.String("", "XDG_TEMPLATES_DIR")) 97 | if err != nil { 98 | return nil, err 99 | } 100 | dirs.Videos, err = parse(kf.String("", "XDG_VIDEOS_DIR")) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return dirs, nil 106 | } 107 | 108 | // parse takes a given string and returns it as a path. 109 | func parse(s string, err error) (string, error) { 110 | if err != nil { 111 | return "", err 112 | } 113 | s = strings.Trim(s, "\"") 114 | if strings.HasPrefix(s, "$HOME") { 115 | s = filepath.Join(basedir.Home, strings.TrimPrefix(s, "$HOME")) 116 | } 117 | if s == "" { 118 | s = basedir.Home 119 | } 120 | return s, nil 121 | } 122 | -------------------------------------------------------------------------------- /userdirs/userdirs_test.go: -------------------------------------------------------------------------------- 1 | package userdirs 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | // XdgUserDir runs the xdg-user-dir command with the given argument. 10 | func XdgUserDir(s string) string { 11 | out, err := exec.Command("xdg-user-dir", s).Output() 12 | if err != nil { 13 | panic(err) 14 | } 15 | return strings.TrimSpace(string(out)) 16 | } 17 | 18 | func doTest(t *testing.T, s1, s2 string) { 19 | t.Log(s1) 20 | t.Log(s2) 21 | if s1 != s2 { 22 | t.Fail() 23 | } 24 | } 25 | 26 | func TestDesktop(t *testing.T) { 27 | doTest(t, Desktop, XdgUserDir("DESKTOP")) 28 | } 29 | 30 | func TestDocuments(t *testing.T) { 31 | doTest(t, Documents, XdgUserDir("DOCUMENTS")) 32 | } 33 | 34 | func TestDownload(t *testing.T) { 35 | doTest(t, Download, XdgUserDir("DOWNLOAD")) 36 | } 37 | 38 | func TestMusic(t *testing.T) { 39 | doTest(t, Music, XdgUserDir("MUSIC")) 40 | } 41 | 42 | func TestPictures(t *testing.T) { 43 | doTest(t, Pictures, XdgUserDir("PICTURES")) 44 | } 45 | 46 | func TestPublicShare(t *testing.T) { 47 | doTest(t, PublicShare, XdgUserDir("PUBLICSHARE")) 48 | } 49 | 50 | func TestTemplates(t *testing.T) { 51 | doTest(t, Templates, XdgUserDir("TEMPLATES")) 52 | } 53 | 54 | func TestVideos(t *testing.T) { 55 | doTest(t, Videos, XdgUserDir("VIDEOS")) 56 | } 57 | --------------------------------------------------------------------------------