File: get.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 Single-file source-code for get.
  27 
  28 To compile a smaller-sized command-line app, you can use the `go` command as
  29 follows:
  30 
  31 go build -ldflags "-s -w" -trimpath get.go
  32 */
  33 
  34 package main
  35 
  36 import (
  37     "bufio"
  38     "encoding/base64"
  39     "errors"
  40     "fmt"
  41     "io"
  42     "net/http"
  43     "os"
  44     "strings"
  45 )
  46 
  47 const info = `
  48 get [files/URIs...]
  49 
  50 Load all files and/or web-resources given.
  51 `
  52 
  53 // errNoMoreOutput is a dummy error whose message is ignored, and which
  54 // causes the app to quit immediately and successfully
  55 var errNoMoreOutput = errors.New(`no more output`)
  56 
  57 func main() {
  58     if len(os.Args) > 1 {
  59         switch os.Args[1] {
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stderr.WriteString(info[1:])
  62             return
  63         }
  64     }
  65 
  66     if err := run(os.Args[1:]); err != nil && err != errNoMoreOutput {
  67         fmt.Fprintf(os.Stderr, "\x1b[31m%s\x1b[0m\n", err.Error())
  68         os.Exit(1)
  69     }
  70 }
  71 
  72 func run(names []string) error {
  73     w := bufio.NewWriter(os.Stdout)
  74     defer w.Flush()
  75 
  76     dashes := 0
  77     for _, s := range names {
  78         if s == `-` {
  79             dashes++
  80         }
  81     }
  82 
  83     var stdin []byte
  84     gotStdin := false
  85 
  86     for _, name := range names {
  87         w.Flush()
  88 
  89         // handle reading from stdin, keeping its data for reuse if needed
  90         if name == `-` {
  91             if dashes > 1 {
  92                 if !gotStdin {
  93                     stdin, _ = io.ReadAll(os.Stdin)
  94                     gotStdin = true
  95                 }
  96 
  97                 n, err := w.Write(stdin)
  98                 if n < len(stdin) || err != nil {
  99                     return nil
 100                 }
 101                 continue
 102             }
 103 
 104             _, err := io.Copy(os.Stdout, os.Stdin)
 105             if err != nil {
 106                 return nil
 107             }
 108             continue
 109         }
 110 
 111         // handle data-URIs
 112         if seemsDataURI(name) {
 113             if err := handleDataURI(w, name); err != nil {
 114                 return err
 115             }
 116             continue
 117         }
 118 
 119         // handle web-requests and actual files
 120         handle := handleFile
 121         if seemsURI(name) {
 122             handle = handleURI
 123         }
 124         if err := handle(w, name); err != nil {
 125             return err
 126         }
 127     }
 128 
 129     // use stdin when no filenames were given
 130     if len(names) == 0 {
 131         io.Copy(os.Stdout, os.Stdin)
 132     }
 133     return nil
 134 }
 135 
 136 // handleDataURI decodes base64 explicitly, so decoding errors can be told
 137 // apart from output-writing ones
 138 func handleDataURI(w *bufio.Writer, data string) error {
 139     i := strings.Index(data, `;base64,`)
 140     if i < 0 {
 141         return errors.New(`invalid data URI`)
 142     }
 143     r := strings.NewReader(data[i+len(`;base64,`):])
 144     dec := base64.NewDecoder(base64.StdEncoding, r)
 145 
 146     var buf [16 * 1024]byte
 147 
 148     for {
 149         n, err := dec.Read(buf[:])
 150         if n < 1 && err == io.EOF {
 151             return nil
 152         }
 153         if err != nil {
 154             return err
 155         }
 156 
 157         _, err = w.Write(buf[:n])
 158         if err != nil {
 159             return errNoMoreOutput
 160         }
 161     }
 162 }
 163 
 164 func handleFile(w *bufio.Writer, path string) error {
 165     f, err := os.Open(path)
 166     if err != nil {
 167         return err
 168     }
 169     defer f.Close()
 170 
 171     _, err = io.Copy(w, f)
 172     if err != nil {
 173         return errNoMoreOutput
 174     }
 175     return nil
 176 }
 177 
 178 func handleURI(w *bufio.Writer, uri string) error {
 179     r, err := http.Get(uri)
 180     if err != nil {
 181         return err
 182     }
 183     defer r.Body.Close()
 184 
 185     _, err = io.Copy(w, r.Body)
 186     if err != nil {
 187         return errNoMoreOutput
 188     }
 189     return nil
 190 }
 191 
 192 func seemsDataURI(s string) bool {
 193     start := s
 194     if len(s) > 64 {
 195         start = s[:64]
 196     }
 197     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 198 }
 199 
 200 func seemsURI(s string) bool {
 201     return false ||
 202         strings.HasPrefix(s, `https://`) ||
 203         strings.HasPrefix(s, `http://`)
 204 }