├── .gitignore ├── LICENSE.md ├── README.md ├── banner.go ├── bin └── build ├── bitbucket-pipelines.yml ├── cache ├── assets.go ├── cache.go └── cacher.go ├── color └── color.go ├── config └── config.go ├── dist ├── darwin-amd64.tar.bz2 ├── darwin-arm64.tar.bz2 ├── linux-amd64.tar.bz2 ├── linux-arm64.tar.bz2 └── windows-amd64.tar.bz2 ├── go.mod ├── go.sum ├── log.go ├── main.go ├── page ├── page.go ├── v1.go └── v2.go ├── pages └── pages.go ├── platform └── platform.go └── terminal.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Daniel J Robbins II 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tldr in golang 2 | 3 | [TLDR pages](https://tldr-pages.github.io/) - Simplified and community-driven man pages 4 | 5 | ![Terminal](terminal.png) 6 | 7 | ## Install 8 | 9 | ``` 10 | go install bitbucket.org/djr2/tldr@latest 11 | ``` 12 | 13 | ## Download 14 | 15 | * [Distributions](https://bitbucket.org/djr2/tldr/src/master/dist/) 16 | 17 | ## Building and Build Requirements 18 | 19 | * go 20 | * upx -- https://github.com/upx/upx 21 | * tar 22 | 23 | ``` 24 | bin/build 25 | ``` 26 | 27 | The build script will compile and compress the tldr executables. 28 | 29 | The build script currently supports the following platforms and architectures; 30 | 31 | * darwin arm64 32 | * darwin amd64 33 | * linux arm64 34 | * linux amd64 35 | * windows amd64 36 | 37 | To a build a specific platform version run the below commands. 38 | It is important to replace `[platform]` with the desired operating system and 39 | `[arch]` with the desired platform architecture to build the executable correctly. 40 | 41 | Supported Go build platforms and architectures can be found here; 42 | https://golang.org/doc/install/source#environment 43 | 44 | It is not necessary to run upx but it greatly reduces executable size. 45 | 46 | ```bash 47 | GOOS=[platform] GOARCH=[arch] go build -ldflags="-s -w" -o tldr 48 | upx --brute tldr # executable compression 49 | ``` 50 | 51 | ## Usage 52 | 53 | ``` 54 | Usage: 55 | -c page 56 | clear cache for a tldr page 57 | page -- Use `clearall` to clear entire cache 58 | -p is required if clearing cache for a specific platform 59 | -debug string 60 | enables debug logging (default "disable") 61 | -p platform 62 | platform of the tldr page 63 | platform -- common, linux, osx, sunos, windows (default "common") 64 | ``` 65 | 66 | ### View a tldr 67 | ``` 68 | tldr 69 | ``` 70 | 71 | ### View a tldr for a specific platform 72 | ``` 73 | tldr -p osx 74 | ``` 75 | 76 | ### Clear a tldr 77 | ``` 78 | tldr -c 79 | ``` 80 | 81 | ### Clear a tldr for a specific platform 82 | ``` 83 | tldr -c -p osx 84 | ``` 85 | 86 | ### Clear entire cache 87 | ``` 88 | tldr -c clearall 89 | ``` 90 | 91 | ## Configuration 92 | 93 | A configuration is created the first time `tldr` is run. 94 | 95 | The configuration is located at; 96 | ``` 97 | $HOME/.tldr/config.json 98 | ``` 99 | 100 | Pages repository URI, Zip URI, and all of the output colors are 101 | configurable. 102 | 103 | Below is the default configuration. 104 | 105 | ``` 106 | { 107 | "pages_uri": "", 108 | "zip_uri": "", 109 | "banner_color_1": 36, 110 | "banner_color_2": 34, 111 | "tldr_color": 97, 112 | "header_color": 34, 113 | "header_decor_color": 97, 114 | "platform_color": 90, 115 | "description_color": 0, 116 | "example_color": 36, 117 | "hypen_color": 0, 118 | "syntax_color": 31, 119 | "variable_color": 0 120 | } 121 | ``` 122 | 123 | If plain (default) terminal text is desired set all color options to `0`. 124 | 125 | `pages_uri` and `zip_uri` when left blank will use the official TLDR 126 | locations. 127 | 128 | These can be used to test pages from a custom repository 129 | or any zip collection that follows the official TLDR directory format 130 | and file specification. 131 | 132 | Pages: `https://raw.githubusercontent.com/tldr-pages/tldr/main/pages/` 133 | 134 | Zip: `https://tldr-pages.github.io/assets/tldr.zip` 135 | 136 | To reset the configuration back to its defaults delete `config.json` 137 | and it will be recreated. Or copy and paste the configuration from 138 | this README above. 139 | 140 | ## License 141 | 142 | [MIT License](https://bitbucket.org/djr2/tldr/src/master/LICENSE.md) 143 | -------------------------------------------------------------------------------- /banner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "bitbucket.org/djr2/tldr/color" 7 | "bitbucket.org/djr2/tldr/config" 8 | ) 9 | 10 | func banner() { 11 | cfg := config.Config 12 | fmt.Print("" + 13 | color.Color(cfg.BannerColor1) + ` ___________ _____ _____ ` + "\n" + 14 | color.Color(cfg.BannerColor2) + ` /__ __/ / / _ \/ _ \ ` + "\n" + 15 | color.Color(cfg.BannerColor2) + ` / / / / / // / // / ` + "\n" + 16 | color.Color(cfg.BannerColor1) + ` / / / /__/ // / / \ \ ` + "\n" + 17 | color.Color(cfg.BannerColor2) + ` /__/ /_____/______/__/ \_/ ` + color.ColorBold(cfg.TLDRColor) + "https://tldr-pages.github.io\n\n" + color.Reset, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /bin/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | dist_dir=./dist 4 | 5 | dists=( 6 | "darwin-amd64" 7 | "darwin-arm64" 8 | "linux-amd64" 9 | "linux-arm64" 10 | "windows-amd64" 11 | ) 12 | 13 | for dist in "${dists[@]}" 14 | do 15 | dist_os=$(echo $dist | awk -F '-' '{ print $1 }') 16 | dist_arch=$(echo $dist | awk -F '-' '{ print $2 }') 17 | build_dir=$dist_dir/build/$dist 18 | 19 | if [[ "$dist_os" == "windows" ]]; then 20 | build_file=$build_dir/tldr.exe 21 | else 22 | build_file=$build_dir/tldr 23 | fi 24 | 25 | GOOS=$dist_os GOARCH=$dist_arch go build -ldflags="-s -w" -o $build_file 26 | upx --brute $build_file 27 | tar -jcvf "$dist_dir/$dist.tar.bz2" $dist_dir/build/$dist 28 | rm -rf $dist_dir/build 29 | done 30 | -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | # This is a sample build configuration for Go. 2 | # Check our guides at https://confluence.atlassian.com/x/5Q4SMw for more examples. 3 | # Only use spaces to indent your .yml configuration. 4 | # ----- 5 | # You can specify a custom docker image from Docker Hub as your build environment. 6 | image: golang:1.9.2 7 | 8 | pipelines: 9 | default: 10 | - step: 11 | script: # Modify the commands below to build your repository. 12 | - PACKAGE_PATH="${GOPATH}/src/bitbucket.org/${BITBUCKET_REPO_OWNER}/${BITBUCKET_REPO_SLUG}" 13 | - mkdir -pv "${PACKAGE_PATH}" 14 | - tar -cO --exclude-vcs --exclude=bitbucket-pipelines.yml . | tar -xv -C "${PACKAGE_PATH}" 15 | - cd "${PACKAGE_PATH}" 16 | - go get -v 17 | - go build -v 18 | - go test -v -------------------------------------------------------------------------------- /cache/assets.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "archive/zip" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | 13 | "bitbucket.org/djr2/tldr/pages" 14 | ) 15 | 16 | func getAssets() { // nolint: gocyclo 17 | zipFile := cacheDir + "/assets.zip" 18 | if info, err := os.Stat(zipFile); err == nil { 19 | if info.ModTime().Add(time.Hour * 720).After(time.Now()) { 20 | return 21 | } 22 | } 23 | 24 | page := pages.Pages{} 25 | resp := page.Zip() 26 | 27 | contents, err := ioutil.ReadAll(resp) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | file, err := os.Create(zipFile) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | _, err = file.Write(contents) 38 | defer file.Close() // nolint: errcheck 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | r, err := zip.OpenReader(zipFile) 44 | defer r.Close() // nolint: errcheck, megacheck 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | for _, f := range r.File { 50 | df, oerr := f.Open() 51 | if oerr != nil { 52 | log.Fatal(oerr) 53 | } 54 | 55 | filePath := filepath.Join(cacheDir, f.Name) 56 | if f.FileInfo().IsDir() { 57 | derr := os.MkdirAll(filePath, os.ModePerm) 58 | if derr != nil { 59 | log.Fatal(derr) 60 | } 61 | } else { 62 | var fileDir string 63 | if lastIndex := strings.LastIndex(filePath, string(os.PathSeparator)); lastIndex > -1 { 64 | fileDir = filePath[:lastIndex] 65 | } 66 | 67 | err = os.MkdirAll(fileDir, os.ModePerm) 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | 77 | _, err = io.Copy(f, df) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | if err := f.Close(); err != nil { 82 | log.Println(err) 83 | } 84 | } 85 | if err := df.Close(); err != nil { 86 | log.Println(err) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "bitbucket.org/djr2/tldr/platform" 8 | "github.com/mitchellh/go-homedir" 9 | ) 10 | 11 | var cacheDir string 12 | 13 | func init() { 14 | h, err := homedir.Dir() 15 | if err != nil { 16 | log.Fatal(err) 17 | } 18 | 19 | cacheDir = h + "/" + ".tldr" 20 | 21 | if _, err := os.Stat(cacheDir); err != nil { 22 | if err := os.Mkdir(cacheDir, 0700); err != nil { 23 | log.Fatal(err) 24 | } 25 | } 26 | 27 | getAssets() 28 | } 29 | 30 | func newCacher(name string, plat platform.Platform) *cacher { 31 | return &cacher{name: name + ".md", plat: plat} 32 | } 33 | 34 | func validPlatform(plat platform.Platform) platform.Platform { 35 | if plat == platform.UNKNOWN { 36 | return platform.Actual() 37 | } 38 | return plat 39 | } 40 | 41 | // Find attempts to find the requested tldr page from the local cache. If a 42 | // local cache page is not found it will attempt to retrieve the page from 43 | // tldr pages repository 44 | func Find(name string, plat platform.Platform) (*os.File, platform.Platform) { 45 | cacher := newCacher(name, validPlatform(plat)) 46 | return cacher.search(), cacher.plat 47 | } 48 | 49 | // Remove will delete a local tldr page from the cache or if `clearall` is 50 | // provided as the name it will remove all tldr pages from the cache. 51 | func Remove(name string, plat platform.Platform) { 52 | cacher := newCacher(name, validPlatform(plat)) 53 | cacher.remove() 54 | } 55 | -------------------------------------------------------------------------------- /cache/cacher.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "bitbucket.org/djr2/tldr/pages" 13 | "bitbucket.org/djr2/tldr/platform" 14 | ) 15 | 16 | type cacher struct { 17 | plat platform.Platform 18 | name string 19 | } 20 | 21 | func (c *cacher) platformDir() string { 22 | return cacheDir + "/pages/" + c.plat.String() 23 | } 24 | 25 | func (c *cacher) file() string { 26 | return c.platformDir() + "/" + c.name 27 | } 28 | 29 | func (c *cacher) cmd() string { 30 | return strings.TrimSuffix(c.name, `.md`) 31 | } 32 | 33 | func (c *cacher) search() *os.File { 34 | var tried []platform.Platform 35 | c.plat = validPlatform(c.plat) 36 | tried = append(tried, c.plat) 37 | cached := c.find() 38 | if cached == nil { 39 | c.plat = platform.COMMON 40 | tried = append(tried, c.plat) 41 | } 42 | cached = c.find() 43 | if cached == nil { 44 | cached = c.extendedSearch(tried) 45 | } 46 | if cached != nil { 47 | info, err := cached.Stat() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | if info.ModTime().Add(time.Hour * 720).Before(time.Now()) { 52 | log.Println("Cache older than 30 days") 53 | return c.save() 54 | } 55 | } 56 | return cached 57 | } 58 | 59 | func (c *cacher) extendedSearch(tried []platform.Platform) *os.File { 60 | for _, plat := range platform.Platforms() { 61 | c.plat = validPlatform(platform.Parse(plat)) 62 | if file := c.find(); file != nil { 63 | return file 64 | } 65 | } 66 | for _, plat := range tried { 67 | c.plat = plat 68 | if file := c.save(); file != nil { 69 | return file 70 | } 71 | } 72 | return nil 73 | } 74 | 75 | func (c *cacher) find() *os.File { 76 | for _, fileInfo := range c.readDir() { 77 | if fileInfo.Name() == c.name { 78 | file, err := os.Open(c.file()) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | return file 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (c *cacher) download() io.ReadCloser { 89 | page := pages.New(c.name, c.plat) 90 | c.plat = page.Platform 91 | c.createDir() 92 | return page.Body() 93 | } 94 | 95 | func (c *cacher) save() *os.File { 96 | down := c.download() 97 | if down == nil { 98 | return nil 99 | } 100 | 101 | buf, err := ioutil.ReadAll(down) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | file, err := os.Create(c.file()) 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | ret, err := file.Write(buf) 112 | defer file.Close() // nolint: errcheck 113 | if err != nil { 114 | log.Fatal(err) 115 | } 116 | 117 | log.Println("Created:", c.file(), "bytes:", strconv.Itoa(ret)) 118 | return c.search() 119 | } 120 | 121 | func (c *cacher) remove() { 122 | if c.name == "clearall.md" { 123 | if err := os.RemoveAll(cacheDir + "/pages"); err != nil { 124 | log.Fatal(err) 125 | } 126 | if err := os.Remove(cacheDir + "/assets.zip"); err != nil { 127 | log.Fatal(err) 128 | } 129 | log.Println("Cache cleared") 130 | os.Exit(0) 131 | } 132 | 133 | if c.search() == nil { 134 | log.Fatal("Command: ", c.cmd(), " not cached ", c.file()) 135 | } 136 | 137 | if err := os.Remove(c.file()); err != nil { 138 | log.Fatal(err) 139 | } 140 | 141 | log.Println("Removed:", c.cmd(), c.file()) 142 | os.Exit(0) 143 | } 144 | 145 | func (c *cacher) createDir() { 146 | _, err := os.Stat(c.platformDir()) 147 | if err != nil { 148 | if err := os.Mkdir(c.platformDir(), 0700); err != nil { 149 | log.Fatal(err) 150 | } 151 | } 152 | } 153 | 154 | func (c *cacher) readDir() []os.FileInfo { 155 | c.createDir() 156 | srcDir, err := ioutil.ReadDir(c.platformDir()) 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | return srcDir 161 | } 162 | -------------------------------------------------------------------------------- /color/color.go: -------------------------------------------------------------------------------- 1 | package color 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // nolint: varcheck, deadcode, megacheck 9 | const ( 10 | Reset = "\033[0m" 11 | Normal = 0 12 | Bold = 1 13 | Dim = 2 14 | Underline = 4 15 | Blink = 5 16 | Reverse = 7 17 | Hidden = 8 18 | Black = 30 19 | Red = 31 20 | Green = 32 21 | Yellow = 33 22 | Blue = 34 23 | Magenta = 35 24 | Cyan = 36 25 | BrightGray = 37 26 | DarkGray = 90 27 | BrightRed = 91 28 | BrightGreen = 92 29 | BrightYellow = 93 30 | BrightBlue = 94 31 | BrightPurple = 95 32 | BrightCyan = 96 33 | White = 97 34 | ) 35 | 36 | // Color creates ANSI color syntax for terminal output 37 | func Color(code int, flags ...int) string { 38 | var strFlags []string 39 | for _, f := range flags { 40 | strFlags = append(strFlags, strconv.Itoa(f)) 41 | } 42 | return "\033[" + strings.Join(strFlags, ";") + ";" + strconv.Itoa(code) + "m" 43 | } 44 | 45 | // ColorBold is an alias for bold ANSI color formatting 46 | func ColorBold(code int) string { // nolint: golint 47 | return Color(code, Bold) 48 | } 49 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | "bitbucket.org/djr2/tldr/color" 10 | "github.com/mitchellh/go-homedir" 11 | ) 12 | 13 | // Config provides the configuration variables read from config.json 14 | var Config Options 15 | 16 | // Options defines the available configuration options 17 | type Options struct { 18 | PagesURI string `json:"pages_uri"` 19 | ZipURI string `json:"zip_uri"` 20 | BannerColor1 int `json:"banner_color_1"` 21 | BannerColor2 int `json:"banner_color_2"` 22 | TLDRColor int `json:"tldr_color"` 23 | HeaderColor int `json:"header_color"` 24 | HeaderDecorColor int `json:"header_decor_color"` 25 | PlatformColor int `json:"platform_color"` 26 | DescriptionColor int `json:"description_color"` 27 | ExampleColor int `json:"example_color"` 28 | HypenColor int `json:"hypen_color"` 29 | SyntaxColor int `json:"syntax_color"` 30 | VariableColor int `json:"variable_color"` 31 | } 32 | 33 | // Load looks for the config.json file in $HOME/.tldr. If the configuration 34 | // file is not found it will create one with the default configuration options. 35 | func Load() { 36 | h, err := homedir.Dir() 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | f := h + "/" + ".tldr/config.json" 42 | _, err = os.Stat(f) 43 | if err != nil { 44 | create(f) 45 | } 46 | 47 | file, err := os.Open(f) 48 | defer file.Close() // nolint: errcheck, megacheck 49 | if err != nil { 50 | log.Println(err) 51 | return 52 | } 53 | 54 | b, err := ioutil.ReadAll(file) 55 | if err != nil { 56 | log.Println(err) 57 | return 58 | } 59 | 60 | if json.Unmarshal(b, &Config) != nil { 61 | log.Println(err) 62 | } 63 | } 64 | 65 | func create(f string) { 66 | vars := Options{ 67 | PagesURI: "", 68 | ZipURI: "", 69 | BannerColor1: color.Cyan, 70 | BannerColor2: color.Blue, 71 | TLDRColor: color.White, 72 | HeaderColor: color.Blue, 73 | HeaderDecorColor: color.White, 74 | PlatformColor: color.DarkGray, 75 | DescriptionColor: color.Normal, 76 | ExampleColor: color.Cyan, 77 | HypenColor: color.Normal, 78 | SyntaxColor: color.Red, 79 | VariableColor: color.Normal, 80 | } 81 | 82 | file, err := os.Create(f) 83 | defer file.Close() // nolint: errcheck, megacheck 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | j, err := json.MarshalIndent(vars, "", "") 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | _, err = file.Write(j) 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /dist/darwin-amd64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/dist/darwin-amd64.tar.bz2 -------------------------------------------------------------------------------- /dist/darwin-arm64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/dist/darwin-arm64.tar.bz2 -------------------------------------------------------------------------------- /dist/linux-amd64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/dist/linux-amd64.tar.bz2 -------------------------------------------------------------------------------- /dist/linux-arm64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/dist/linux-arm64.tar.bz2 -------------------------------------------------------------------------------- /dist/windows-amd64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/dist/windows-amd64.tar.bz2 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module bitbucket.org/djr2/tldr 2 | 3 | go 1.20 4 | 5 | require github.com/mitchellh/go-homedir v1.1.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 2 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 3 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | ) 8 | 9 | type logWriter struct{} 10 | 11 | func (writer logWriter) Write(bytes []byte) (int, error) { 12 | if flagSet.Lookup("debug").Value.String() != "disable" { 13 | return fmt.Print("["+time.Now().UTC().Format("2006-01-02 15:04:05")+"] ", string(bytes)) 14 | } 15 | return fmt.Print(string(bytes)) 16 | } 17 | 18 | func setLogDebug() { 19 | if flagSet.Lookup("debug").Value.String() != "disable" { 20 | log.SetFlags(log.Lshortfile) 21 | } else { 22 | log.SetFlags(0) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "bitbucket.org/djr2/tldr/cache" 10 | "bitbucket.org/djr2/tldr/config" 11 | "bitbucket.org/djr2/tldr/page" 12 | "bitbucket.org/djr2/tldr/platform" 13 | ) 14 | 15 | var flagSet *flag.FlagSet 16 | 17 | func init() { 18 | flagSet = flag.NewFlagSet("", flag.ContinueOnError) 19 | flagSet.String("p", "", "platform of the tldr page\n\t `platform` -- "+ 20 | strings.Join(platform.Platforms(), ", ")) 21 | flagSet.String("c", "", "clear cache for a tldr page\n\t `page` -- "+ 22 | "Use `clearall` to clear entire cache\n\t -p is required if clearing cache for a specific platform") 23 | flagSet.String("debug", "disable", "enables debug logging") 24 | log.SetOutput(new(logWriter)) 25 | } 26 | 27 | func main() { 28 | tldr() 29 | } 30 | 31 | func tldr() { 32 | if err := flagSet.Parse(os.Args[1:]); err != nil { 33 | return 34 | } 35 | 36 | setLogDebug() 37 | 38 | config.Load() 39 | plat := platform.ParseFlag(flagSet.Lookup("p")) 40 | 41 | if clear := flagSet.Lookup("c"); clear.Value.String() != "" { 42 | banner() 43 | cache.Remove(clear.Value.String(), plat) 44 | return 45 | } 46 | 47 | if len(flagSet.Args()) > 0 { 48 | page.New(cache.Find(strings.Join(flagSet.Args(), "-"), plat)).Print() 49 | } else if len(os.Args[1:]) == 0 { 50 | banner() 51 | flagSet.Usage() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /page/page.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "regexp" 9 | 10 | "bitbucket.org/djr2/tldr/color" 11 | "bitbucket.org/djr2/tldr/config" 12 | "bitbucket.org/djr2/tldr/platform" 13 | ) 14 | 15 | // Page provides the Print method for the final generated output of a TLDR page. 16 | type Page interface { 17 | Print() 18 | } 19 | 20 | // Parser provides the interface for parsing a TLDR page. 21 | type Parser interface { 22 | Write(p []byte) 23 | Lines() [][]byte 24 | Header() []byte 25 | Description(line []byte) []byte 26 | Example(line []byte) []byte 27 | Syntax(line []byte) []byte 28 | Variable(line []byte) []byte 29 | } 30 | 31 | var ( 32 | descRx = regexp.MustCompile(`^>\s`) 33 | varRx = regexp.MustCompile(`{{([\w\s\\/~!@#$%^&*()\[\]:;"'<,>?.]+)}}`) 34 | ) 35 | 36 | // New creates a parsed TLDR page. It parsers the provided file and returns the 37 | // parsed TLDR page. 38 | func New(file *os.File, plat platform.Platform) Page { // nolint: interfacer 39 | b, err := ioutil.ReadAll(file) 40 | defer file.Close() // nolint: errcheck 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | lines := bytes.Split(b, toB("\n")) 45 | if headerRxV2.Match(lines[1]) { 46 | p := &pagev2{lines, &bytes.Buffer{}} 47 | Parse(p, plat) 48 | return p 49 | } 50 | p := &pagev1{lines, &bytes.Buffer{}} 51 | Parse(p, plat) 52 | return p 53 | } 54 | 55 | // Parse takes a Parser interface and the current platform of the document 56 | // that is to be parsed and the parses the internal lines writing to the 57 | // internal buffer of the parser. 58 | func Parse(p Parser, plat platform.Platform) { 59 | cfg := config.Config 60 | p.Write(toB("\n")) 61 | for i, line := range p.Lines() { 62 | if i == 0 { 63 | p.Write(toB(" ")) 64 | p.Write(toB(color.ColorBold(cfg.HeaderDecorColor) + "[")) 65 | p.Write(p.Header()) 66 | p.Write(toB(color.ColorBold(cfg.HeaderDecorColor) + " - ")) 67 | p.Write(toB(color.Color(cfg.PlatformColor) + plat.String())) 68 | p.Write(toB(color.ColorBold(cfg.HeaderDecorColor) + "]" + "\n")) 69 | continue 70 | } 71 | 72 | if desc := p.Description(line); desc != nil { 73 | p.Write(toB(" ")) 74 | p.Write(desc) 75 | p.Write(toB("\n")) 76 | continue 77 | } 78 | 79 | if example := p.Example(line); example != nil { 80 | p.Write(toB("\n ")) 81 | p.Write(example) 82 | p.Write(toB("\n")) 83 | continue 84 | } 85 | 86 | if syntax := p.Syntax(line); syntax != nil { 87 | p.Write(toB(" ")) 88 | p.Write(p.Variable(syntax)) 89 | p.Write(toB("\n")) 90 | continue 91 | } 92 | } 93 | } 94 | 95 | func toB(str string) []byte { 96 | return []byte(str) 97 | } 98 | -------------------------------------------------------------------------------- /page/v1.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | 8 | "bitbucket.org/djr2/tldr/color" 9 | "bitbucket.org/djr2/tldr/config" 10 | ) 11 | 12 | var ( 13 | headerRxV1 = regexp.MustCompile(`^#\s`) 14 | exampleRxV1 = regexp.MustCompile(`^(-\s)`) 15 | syntaxRxV1 = regexp.MustCompile("^`(.+)`$") 16 | ) 17 | 18 | type pagev1 struct { 19 | lns [][]byte 20 | buf *bytes.Buffer 21 | } 22 | 23 | func (p *pagev1) Lines() [][]byte { 24 | return p.lns 25 | } 26 | 27 | func (p *pagev1) Write(b []byte) { 28 | p.buf.Write(b) 29 | } 30 | 31 | func (p *pagev1) Print() { 32 | fmt.Println(p.buf.String() + color.Reset) 33 | } 34 | 35 | func (p *pagev1) Header() []byte { 36 | cfg := config.Config 37 | return headerRxV1.ReplaceAll(p.lns[0], toB(color.ColorBold(cfg.HeaderColor))) 38 | } 39 | 40 | func (p *pagev1) Description(line []byte) []byte { 41 | cfg := config.Config 42 | if descRx.Match(line) { 43 | return descRx.ReplaceAll(line, toB(color.Color(cfg.DescriptionColor))) 44 | } 45 | return nil 46 | } 47 | 48 | func (p *pagev1) Example(line []byte) []byte { 49 | if exampleRxV1.Match(line) { 50 | cfg := config.Config 51 | return exampleRxV1.ReplaceAll(line, toB(color.Color(cfg.HypenColor)+"$1"+color.Color(cfg.ExampleColor))) 52 | } 53 | return nil 54 | } 55 | 56 | func (p *pagev1) Syntax(line []byte) []byte { 57 | if syntaxRxV1.Match(line) { 58 | cfg := config.Config 59 | return syntaxRxV1.ReplaceAll(line, toB(color.Color(cfg.SyntaxColor)+"$1")) 60 | } 61 | return nil 62 | } 63 | 64 | func (p *pagev1) Variable(line []byte) []byte { 65 | cfg := config.Config 66 | return varRx.ReplaceAll(line, toB(color.Color(cfg.VariableColor)+"$1"+color.Color(cfg.SyntaxColor))) 67 | } 68 | -------------------------------------------------------------------------------- /page/v2.go: -------------------------------------------------------------------------------- 1 | package page 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | 8 | "bitbucket.org/djr2/tldr/color" 9 | "bitbucket.org/djr2/tldr/config" 10 | ) 11 | 12 | var ( 13 | headerRxV2 = regexp.MustCompile(`^[=]+$`) 14 | exampleRxV2 = regexp.MustCompile(`^([\w]+)`) 15 | syntaxRxV2 = regexp.MustCompile(`^[\s]{4}`) 16 | ) 17 | 18 | type pagev2 struct { 19 | lns [][]byte 20 | buf *bytes.Buffer 21 | } 22 | 23 | func (p *pagev2) Lines() [][]byte { 24 | if headerRxV2.Match(p.lns[1]) { 25 | p.lns[1] = p.lns[0] 26 | p.lns = p.lns[1:] 27 | } 28 | return p.lns 29 | } 30 | 31 | func (p *pagev2) Write(b []byte) { 32 | p.buf.Write(b) 33 | } 34 | 35 | func (p *pagev2) Print() { 36 | fmt.Println(p.buf.String() + color.Reset) 37 | } 38 | 39 | func (p *pagev2) Header() []byte { 40 | cfg := config.Config 41 | return append(toB(color.ColorBold(cfg.HeaderColor)), p.lns[0]...) 42 | } 43 | 44 | func (p *pagev2) Description(line []byte) []byte { 45 | cfg := config.Config 46 | if descRx.Match(line) { 47 | return descRx.ReplaceAll(line, toB(color.Color(cfg.DescriptionColor))) 48 | } 49 | return nil 50 | } 51 | 52 | func (p *pagev2) Example(line []byte) []byte { 53 | if exampleRxV2.Match(line) { 54 | cfg := config.Config 55 | return exampleRxV2.ReplaceAll(line, toB(color.Color(cfg.HypenColor)+"- $1"+color.Color(cfg.ExampleColor))) 56 | } 57 | return nil 58 | } 59 | 60 | func (p *pagev2) Syntax(line []byte) []byte { 61 | if syntaxRxV2.Match(line) { 62 | cfg := config.Config 63 | return syntaxRxV2.ReplaceAll(line, toB(color.Color(cfg.SyntaxColor))) 64 | } 65 | return nil 66 | } 67 | 68 | func (p *pagev2) Variable(line []byte) []byte { 69 | cfg := config.Config 70 | return varRx.ReplaceAll(line, toB(color.Color(cfg.VariableColor)+"$1"+color.Color(cfg.SyntaxColor))) 71 | } 72 | -------------------------------------------------------------------------------- /pages/pages.go: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | 8 | "bitbucket.org/djr2/tldr/config" 9 | "bitbucket.org/djr2/tldr/platform" 10 | ) 11 | 12 | const ( 13 | zip = "https://tldr-pages.github.io/assets/tldr.zip" 14 | raw = "https://raw.githubusercontent.com/tldr-pages/tldr/main/pages/" 15 | ) 16 | 17 | // Pages provides the retrieval of the TLDR assets and repository pages. 18 | type Pages struct { 19 | Name string 20 | Platform platform.Platform 21 | cfg config.Options 22 | } 23 | 24 | // New creates a new Pages instance 25 | func New(name string, plat platform.Platform) *Pages { 26 | return &Pages{ 27 | Name: name, 28 | Platform: plat, 29 | cfg: config.Config, 30 | } 31 | } 32 | 33 | func (p *Pages) url() string { 34 | var uri string 35 | if p.cfg.PagesURI != "" { 36 | uri = p.cfg.PagesURI 37 | } else { 38 | uri = raw 39 | } 40 | return uri + p.Platform.String() + "/" + p.Name 41 | } 42 | 43 | // Zip returns the result of the downloaded TLDR pages zip file. 44 | func (p *Pages) Zip() io.ReadCloser { 45 | var uri string 46 | if p.cfg.ZipURI != "" { 47 | uri = p.cfg.ZipURI 48 | } else { 49 | uri = zip 50 | } 51 | zpr, err := http.Get(uri) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | if zpr.StatusCode != http.StatusOK { 56 | log.Fatal("Problem getting: ", zip, " Server Error: ", zpr.StatusCode) 57 | } 58 | return zpr.Body 59 | } 60 | 61 | // Body returns the result of a http lookup from the main TLDR repository for a 62 | // TLDR page that was not found in the local cache. 63 | func (p *Pages) Body() io.ReadCloser { 64 | log.Println("Retrieving:", p.url()) 65 | cnr, err := http.Get(p.url()) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | if cnr.StatusCode != http.StatusOK { 70 | log.Println("Problem getting:", p.Name, "Server Error:", cnr.StatusCode) 71 | return nil 72 | } 73 | return cnr.Body 74 | } 75 | -------------------------------------------------------------------------------- /platform/platform.go: -------------------------------------------------------------------------------- 1 | package platform 2 | 3 | import ( 4 | "flag" 5 | "runtime" 6 | ) 7 | 8 | // Platform holds the available TLDR page platforms. 9 | type Platform uint32 10 | 11 | const ( 12 | // UNKNOWN is for unknown platform type 13 | UNKNOWN Platform = iota 14 | // COMMON is a reference to the common directory of the TLDR page assets. 15 | COMMON 16 | // LINUX is a reference to the linux directory of the TLDR page assets. 17 | LINUX 18 | // OSX is a reference to the osx directory of the TLDR page assets. 19 | OSX 20 | // SUNOS is a reference to the sunos directory of the TLDR page assets. 21 | SUNOS 22 | // WINDOWS is a reference to the windows directory of the TLDR page assets. 23 | WINDOWS 24 | ) 25 | 26 | var platformMap = map[Platform]string{ 27 | UNKNOWN: `unknown`, 28 | COMMON: `common`, 29 | LINUX: `linux`, 30 | OSX: `osx`, 31 | SUNOS: `sunos`, 32 | WINDOWS: `windows`, 33 | } 34 | 35 | // Strings provides the string based representation of the Platform 36 | func (p Platform) String() string { 37 | if name, ok := platformMap[p]; ok { 38 | return name 39 | } 40 | return UNKNOWN.String() 41 | } 42 | 43 | // ParseFlag parses the provided command line platform Flag 44 | func ParseFlag(p *flag.Flag) Platform { 45 | return Parse(p.Value.String()) 46 | } 47 | 48 | // Parse returns the Platform if valid. If the provided platform is not valid 49 | // it returns an UNKNOWN platform. 50 | func Parse(p string) Platform { 51 | for plat, name := range platformMap { 52 | if p == name { 53 | return plat 54 | } 55 | } 56 | return UNKNOWN 57 | } 58 | 59 | // Platforms returns the string array of the available Platforms. 60 | func Platforms() []string { 61 | var platforms []string 62 | for _, name := range platformMap { 63 | if name == `unknown` { 64 | continue 65 | } 66 | platforms = append(platforms, name) 67 | } 68 | return platforms 69 | } 70 | 71 | // Actual returns the runtime platform. If a valid platform is not found it 72 | // it returns COMMON. 73 | func Actual() Platform { 74 | switch runtime.GOOS { 75 | case `freebsd`, `netbsd`, `openbsd`, `plan9`, `linux`: 76 | return LINUX 77 | case `darwin`: 78 | return OSX 79 | case `solaris`: 80 | return SUNOS 81 | case `windows`: 82 | return WINDOWS 83 | } 84 | return COMMON 85 | } 86 | -------------------------------------------------------------------------------- /terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k3mist/tldr/e4c3281b42ed2bbee33ebf6763cb2f35459c49f6/terminal.png --------------------------------------------------------------------------------