├── .gitignore ├── go.mod ├── go.sum ├── LICENSE ├── README.markdown └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | /info 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module arp242.net/info 2 | 3 | require github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035 h1:USWjF42jDCSEeikX/G1g40ZWnsPXN5WkZ4jMHZWyBK4= 2 | github.com/mattn/go-isatty v0.0.5-0.20180830101745-3fb116b82035/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2019 Martin Tournoij 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 7 | deal in the Software without restriction, including without limitation the 8 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 9 | sell 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 20 | from, out of or in connection with the software or the use or other dealings 21 | in the software. 22 | 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![This project is considered experimental](https://img.shields.io/badge/Status-experimental-red.svg)](https://arp242.net/status/experimental) 2 | 3 | A simple GNU `info` replacement which isn't terrible. 4 | 5 | This program displays the info pages as a single document in the standard pager 6 | (usually `less` or `more`) with various redundant formatting and free software 7 | propaganda removed (some pages contain more license text than actual useful 8 | content). 9 | 10 | You can install it with `go get arp242.net/info`, which will put the binary at 11 | `~/go/bin/info`. 12 | 13 | If the output is a terminal device the output will be piped to `MANPAGER` or 14 | `PAGER` if set. The default is to use `more -s`. 15 | 16 | `INFOPATH` is respected. 17 | 18 | There are no commandline options at this point; all arguments starting with `-` 19 | are ignored. 20 | 21 | --- 22 | 23 | Is this too complex? You can approximate it with something like: 24 | 25 | info() { 26 | zcat "/usr/share/info/$1.info.gz" | 27 | sed -Ee "/\x1f/d; /^File: $1.info,/d; /^(Node|Ref): .*/d" | 28 | cat -s | 29 | less 30 | } 31 | 32 | This was the original version; but it was too hard to handle stuff like info 33 | pages split over several files (which this snippet won't handle) so I ended up 34 | writing the Go tool. 35 | 36 | There's also [info2man](https://cskk.ezoshosting.com/cs/css/info2pod.html). I 37 | needed to make some changes to get it to work, and the output didn't look too 38 | great. This tool goes back to [at least 39 | 2004](https://web.archive.org/web/20040625210730/https://cskk.ezoshosting.com/cs/css/info2pod.html). 40 | These criticism of Texinfo are hardly new, and the GNU folk's imperviousness to 41 | it isn't, either. 42 | 43 | A Texinfo rant 44 | -------------- 45 | 46 | I have been using several different variants of Unix for almost 20 years. Yet I 47 | still cannot navigate GNU's Texinfo. 48 | 49 | - Maybe the key bindings make sense from an Emacs perspective, but most of us 50 | aren't Emacs users. There is no way to make it behave like the more standard 51 | "vi-ish" key bindings as far as I can find. I need to learn an entire new set 52 | of key bindings. From the perspective of the majority of users the key 53 | bindings are just nonsensical. 54 | 55 | If you're thinking "just read the manual you doofus": buzz off. Do you think I 56 | have nothing better to do than learn how to use some obscure piece of software 57 | I don't even like just to read some fecking plain text once a month? I've got 58 | about 80 years in my life, and this is *not* how I want to spend it. 59 | 60 | And should the entire industry deal with this crap? How many man-hours will be 61 | wasted? Far too many. The only reason I sat down and wrote this in a bout of 62 | anger-driven development is the hope that it'll save myself and a bunch of 63 | other people some time and frustration. 64 | 65 | - I don't like tech docs split over loads of very small pages; only makes it 66 | harder to read or search for things. 67 | 68 | I discovered that you can use `info --subnodes page | less` after I wrote this 69 | tool to output to a single page. I originally tried this, but it doesn't 70 | work unless you pipe it; just `info --subnodes` behaves as usual and the 71 | option is silently ignored 🤦 72 | 73 | It won't strip all the crap though (like now-useless navigation). 74 | 75 | - There is no longer a single source for documentation; do I look at the info 76 | page or man page? Some tools have both, but one is more comprehensive than the 77 | other (sometimes info, sometimes man). 78 | 79 | Fragmentation and inconsistency has long been considered one of Unix's weak 80 | points, and rightfully so. Texinfo only makes this worse. Then again, "GNU's 81 | not Unix" 🤷 82 | 83 | - Every page comes with an inordinate amount of free software advocacy. I want 84 | to read a bloody manual, not a political statement. I know where the websites 85 | of the FSF and GNU are if I'm interested. 86 | 87 | Example: tar page minus crap is reduced from 91k words to 73k, grep from 7.5k 88 | to 14.5k. That's right, over half of the grep info page is Free Software 89 | wankery ✊🍆 💦 90 | 91 | - Some pages include images, which don't work inside a terminal (all of the 92 | images in the info pages on my system could be easily represented with a 93 | simple ASCII diagram, so why use images? Because you can I guess.) 94 | 95 | There are exactly two good points: 96 | 97 | - It's not widely used outside of GNU. 98 | 99 | - The source format is fairly readable, so it's not too hard to write an 100 | alternative reader for it. 101 | 102 | The entire Texinfo project is a failure. A more reasonable person would have 103 | moved on from it a decade ago. 104 | 105 | None of this means that man pages couldn't do with some enhancement – better 106 | referencing probably being the most prominent – but GNU info isn't the answer. 107 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Command info displays texinfo pages. 2 | package main // import "arp242.net/info" 3 | 4 | import ( 5 | "bytes" 6 | "compress/gzip" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "os" 12 | "os/exec" 13 | "regexp" 14 | "strings" 15 | 16 | isatty "github.com/mattn/go-isatty" 17 | ) 18 | 19 | func main() { 20 | if len(os.Args) < 2 { 21 | fatal(errors.New("which page?")) 22 | } 23 | 24 | var page string 25 | for i := 1; i < len(os.Args); i++ { 26 | page = strings.TrimSpace(os.Args[i]) 27 | if len(page) > 0 && page[0] == '-' { 28 | _, _ = fmt.Fprintf(os.Stderr, "info: ignoring option %q\n", os.Args[i]) 29 | continue 30 | } 31 | 32 | break 33 | } 34 | 35 | fp := find(page) 36 | if fp == nil { 37 | fatal(fmt.Errorf("no page for %q", page)) 38 | } 39 | defer fp.Close() 40 | 41 | out := format(fp) 42 | if isatty.IsTerminal(os.Stdout.Fd()) { 43 | err := pager(out) 44 | fatal(err) 45 | return 46 | } 47 | fmt.Println(out) 48 | } 49 | 50 | func fatal(err error) { 51 | if err == nil { 52 | return 53 | } 54 | 55 | _, _ = fmt.Fprintf(os.Stderr, "info: %v\n", err) 56 | os.Exit(1) 57 | 58 | } 59 | 60 | func infopath() []string { 61 | var dirs []string 62 | if os.Getenv("INFOPATH") != "" { 63 | for _, p := range strings.Split(":", os.Getenv("INFOPATH")) { 64 | if p != "" { 65 | dirs = append(dirs, p) 66 | } 67 | } 68 | } 69 | if len(dirs) > 0 { 70 | return dirs 71 | } 72 | 73 | return []string{"/usr/share/info/"} 74 | } 75 | 76 | func find(page string) io.ReadCloser { 77 | // So that includes in the form "tar.info-1" work. 78 | addext := "" 79 | if !strings.Contains(page, ".info") { 80 | addext = ".info" 81 | } 82 | 83 | var ( 84 | fp io.ReadCloser 85 | err error 86 | ) 87 | for _, d := range infopath() { 88 | fp, err = os.Open(fmt.Sprintf("%s/%s%s", d, page, addext)) 89 | if os.IsNotExist(err) { 90 | fp, err = os.Open(fmt.Sprintf("%s/%s%s.gz", d, page, addext)) 91 | if err == nil { 92 | fp, err = gzip.NewReader(fp) 93 | } 94 | } 95 | if os.IsNotExist(err) { 96 | continue 97 | } 98 | 99 | if err != nil { 100 | fatal(err) 101 | } 102 | return fp 103 | } 104 | 105 | return nil 106 | } 107 | 108 | var ( 109 | sub = []*regexp.Regexp{ 110 | regexp.MustCompile(`(^|\n)File: [\w\-]+.info, Node: .+($|\n)`), // Navigation 111 | regexp.MustCompile(`\n\* Menu:\n\n(\* .+?::( .+?)?\n){1,}`), // Menu for subtree 112 | regexp.MustCompile(`\n{3,}`), 113 | } 114 | repl = []string{"", "", "\n\n"} 115 | 116 | nuke = []*regexp.Regexp{ 117 | // Often longer than manpage 🤦 118 | regexp.MustCompile(`^\s*\d+ Copying\n\*{9,}\n\n`), 119 | regexp.MustCompile(`^\s*[\d.]+ GNU Free Documentation License\n={32,}\n\n`), 120 | regexp.MustCompile(`Appendix \w Free Software Needs Free Documentation\n\*{49,}\n\n`), 121 | regexp.MustCompile(`Appendix \w GNU Free Documentation License\n\*{41,}\n\n`), 122 | regexp.MustCompile(`GNU General Public License\n\*{26}`), 123 | 124 | // Free software bruhaha repeated ad nauseam. 125 | regexp.MustCompile(`Permission is granted to copy, distribute and/or modify this`), 126 | 127 | regexp.MustCompile(`\x00\x08\[index\x00\x08]\n`), 128 | regexp.MustCompile(`^\s*Tag Table:\n`), 129 | regexp.MustCompile(`^\s*End Tag Table\n`), 130 | regexp.MustCompile(`^\s*Local Variables:\n`), 131 | 132 | regexp.MustCompile(`\nIndirect:\n(.+?: \d+\n)+?`), 133 | } 134 | 135 | // ^_ 136 | // Indirect: 137 | // tar.info-1: 1139 138 | // tar.info-2: 303202 139 | // ^_ 140 | reInclude = regexp.MustCompile(`\x1f\nIndirect:\n(.+?: \d+\n)+?\x1f`) 141 | ) 142 | 143 | func format(fp io.Reader) string { 144 | d, err := ioutil.ReadAll(fp) 145 | fatal(err) 146 | 147 | // Load subpages. 148 | var subpages string 149 | if m := reInclude.Find(d); m != nil { 150 | for _, f := range bytes.Split(m, []byte("\n"))[2:] { 151 | path := bytes.Split(f, []byte(":")) 152 | if len(path) != 2 { 153 | continue 154 | } 155 | 156 | subfp := find(string(path[0])) 157 | if subfp == nil { 158 | fatal(fmt.Errorf("could not find included page %q", path[0])) 159 | } 160 | 161 | subpages += format(subfp) + "\n\n" 162 | _ = subfp.Close() 163 | } 164 | } 165 | 166 | pages := strings.Split(string(d), "\x1f") 167 | for i := range pages { 168 | for j, re := range sub { 169 | pages[i] = re.ReplaceAllString(pages[i], repl[j]) 170 | } 171 | 172 | for _, re := range nuke { 173 | if re.MatchString(pages[i]) { 174 | pages[i] = "" 175 | } 176 | } 177 | 178 | pages[i] = strings.TrimSpace(pages[i]) 179 | } 180 | 181 | return strings.TrimSpace(strings.Join(pages, "\n\n")+subpages) + "\n" 182 | } 183 | 184 | func pager(page string) error { 185 | tmp, err := ioutil.TempFile(os.TempDir(), "info.") 186 | if err != nil { 187 | return err 188 | } 189 | 190 | _, err = tmp.WriteString(page) 191 | if err != nil { 192 | _ = tmp.Close() 193 | return err 194 | } 195 | err = tmp.Close() 196 | if err != nil { 197 | return err 198 | } 199 | 200 | defer os.Remove(tmp.Name()) 201 | 202 | p := os.Getenv("MANPAGER") 203 | if p == "" { 204 | p = os.Getenv("PAGER") 205 | } 206 | if p == "" { 207 | p = "more -s" 208 | } 209 | 210 | cmd := exec.Command("/bin/sh", "-c", p+" "+tmp.Name()) 211 | cmd.Stdin = os.Stdin 212 | cmd.Stdout = os.Stdout 213 | cmd.Stderr = os.Stderr 214 | return cmd.Run() 215 | } 216 | --------------------------------------------------------------------------------