File: get.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 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 To compile a smaller-sized command-line app, you can use the `go` command as
  27 follows:
  28 
  29 go build -ldflags "-s -w" -trimpath get.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "encoding/base64"
  37     "errors"
  38     "fmt"
  39     "io"
  40     "net/http"
  41     "os"
  42     "strings"
  43 )
  44 
  45 const info = `
  46 get [files/URIs...]
  47 
  48 Load all files and/or web-resources given.
  49 `
  50 
  51 // errNoMoreOutput is a dummy error whose message is ignored, and which
  52 // causes the app to quit immediately and successfully
  53 var errNoMoreOutput = errors.New(`no more output`)
  54 
  55 func main() {
  56     if len(os.Args) > 1 {
  57         switch os.Args[1] {
  58         case `-h`, `--h`, `-help`, `--help`:
  59             os.Stderr.WriteString(info[1:])
  60             return
  61         }
  62     }
  63 
  64     if err := run(os.Args[1:]); err != nil && err != errNoMoreOutput {
  65         fmt.Fprintf(os.Stderr, "\x1b[31m%s\x1b[0m\n", err.Error())
  66         os.Exit(1)
  67     }
  68 }
  69 
  70 func run(names []string) error {
  71     w := bufio.NewWriter(os.Stdout)
  72     defer w.Flush()
  73 
  74     dashes := 0
  75     for _, s := range names {
  76         if s == `-` {
  77             dashes++
  78         }
  79     }
  80 
  81     var stdin []byte
  82     gotStdin := false
  83 
  84     for _, name := range names {
  85         w.Flush()
  86 
  87         // handle reading from stdin, keeping its data for reuse if needed
  88         if name == `-` {
  89             if dashes > 1 {
  90                 if !gotStdin {
  91                     stdin, _ = io.ReadAll(os.Stdin)
  92                     gotStdin = true
  93                 }
  94 
  95                 n, err := w.Write(stdin)
  96                 if n < len(stdin) || err != nil {
  97                     return nil
  98                 }
  99                 continue
 100             }
 101 
 102             _, err := io.Copy(os.Stdout, os.Stdin)
 103             if err != nil {
 104                 return nil
 105             }
 106             continue
 107         }
 108 
 109         // handle data-URIs
 110         if seemsDataURI(name) {
 111             if err := handleDataURI(w, name); err != nil {
 112                 return err
 113             }
 114             continue
 115         }
 116 
 117         // handle web-requests and actual files
 118         handle := handleFile
 119         if seemsURI(name) {
 120             handle = handleURI
 121         }
 122         if err := handle(w, name); err != nil {
 123             return err
 124         }
 125     }
 126 
 127     // use stdin when no filenames were given
 128     if len(names) == 0 {
 129         io.Copy(os.Stdout, os.Stdin)
 130     }
 131     return nil
 132 }
 133 
 134 // handleDataURI decodes base64 explicitly, so decoding errors can be told
 135 // apart from output-writing ones
 136 func handleDataURI(w *bufio.Writer, data string) error {
 137     i := strings.Index(data, `;base64,`)
 138     if i < 0 {
 139         return errors.New(`invalid data URI`)
 140     }
 141     r := strings.NewReader(data[i+len(`;base64,`):])
 142     dec := base64.NewDecoder(base64.StdEncoding, r)
 143 
 144     var buf [32 * 1024]byte
 145 
 146     for {
 147         n, err := dec.Read(buf[:])
 148         if n < 1 && err == io.EOF {
 149             return nil
 150         }
 151         if err != nil {
 152             return err
 153         }
 154 
 155         _, err = w.Write(buf[:n])
 156         if err != nil {
 157             return errNoMoreOutput
 158         }
 159     }
 160 }
 161 
 162 func handleFile(w *bufio.Writer, path string) error {
 163     f, err := os.Open(path)
 164     if err != nil {
 165         return err
 166     }
 167     defer f.Close()
 168 
 169     _, err = io.Copy(w, f)
 170     if err != nil {
 171         return errNoMoreOutput
 172     }
 173     return nil
 174 }
 175 
 176 func handleURI(w *bufio.Writer, uri string) error {
 177     r, err := http.Get(uri)
 178     if err != nil {
 179         return err
 180     }
 181     defer r.Body.Close()
 182 
 183     _, err = io.Copy(w, r.Body)
 184     if err != nil {
 185         return errNoMoreOutput
 186     }
 187     return nil
 188 }
 189 
 190 func seemsDataURI(s string) bool {
 191     start := s
 192     if len(s) > 64 {
 193         start = s[:64]
 194     }
 195     return strings.HasPrefix(s, `data:`) && strings.Contains(start, `;base64,`)
 196 }
 197 
 198 func seemsURI(s string) bool {
 199     return false ||
 200         strings.HasPrefix(s, `https://`) ||
 201         strings.HasPrefix(s, `http://`)
 202 }