File: squeeze.go
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright (c) 2026 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 squeeze.go
  30 */
  31 
  32 package main
  33 
  34 import (
  35     "bufio"
  36     "bytes"
  37     "errors"
  38     "io"
  39     "os"
  40 )
  41 
  42 const info = `
  43 squeeze [filenames...]
  44 
  45 Ignore leading/trailing spaces (and carriage-returns) on lines, also turning
  46 all runs of multiple consecutive spaces into single spaces. Spaces around
  47 tabs are ignored as well.
  48 `
  49 
  50 func main() {
  51     buffered := false
  52     args := os.Args[1:]
  53 
  54     if len(args) > 0 {
  55         switch args[0] {
  56         case `-b`, `--b`, `-buffered`, `--buffered`:
  57             buffered = true
  58             args = args[1:]
  59 
  60         case `-h`, `--h`, `-help`, `--help`:
  61             os.Stdout.WriteString(info[1:])
  62             return
  63         }
  64     }
  65 
  66     if len(args) > 0 && args[0] == `--` {
  67         args = args[1:]
  68     }
  69 
  70     liveLines := !buffered
  71     if !buffered {
  72         if _, err := os.Stdout.Seek(0, io.SeekCurrent); err == nil {
  73             liveLines = false
  74         }
  75     }
  76 
  77     if err := run(os.Stdout, args, liveLines); err != nil && err != io.EOF {
  78         os.Stderr.WriteString(err.Error())
  79         os.Stderr.WriteString("\n")
  80         os.Exit(1)
  81     }
  82 }
  83 
  84 func run(w io.Writer, args []string, live bool) error {
  85     bw := bufio.NewWriter(w)
  86     defer bw.Flush()
  87 
  88     if len(args) == 0 {
  89         return squeeze(bw, os.Stdin, live)
  90     }
  91 
  92     for _, name := range args {
  93         if err := handleFile(bw, name, live); err != nil {
  94             return err
  95         }
  96     }
  97     return nil
  98 }
  99 
 100 func handleFile(w *bufio.Writer, name string, live bool) error {
 101     if name == `` || name == `-` {
 102         return squeeze(w, os.Stdin, live)
 103     }
 104 
 105     f, err := os.Open(name)
 106     if err != nil {
 107         return errors.New(`can't read from file named "` + name + `"`)
 108     }
 109     defer f.Close()
 110 
 111     return squeeze(w, f, live)
 112 }
 113 
 114 func squeeze(w *bufio.Writer, r io.Reader, live bool) error {
 115     const gb = 1024 * 1024 * 1024
 116     sc := bufio.NewScanner(r)
 117     sc.Buffer(nil, 8*gb)
 118 
 119     for i := 0; sc.Scan(); i++ {
 120         s := sc.Bytes()
 121         if i == 0 && bytes.HasPrefix(s, []byte{0xef, 0xbb, 0xbf}) {
 122             s = s[3:]
 123         }
 124 
 125         writeSqueezed(w, s)
 126         if w.WriteByte('\n') != nil {
 127             return io.EOF
 128         }
 129 
 130         if !live {
 131             continue
 132         }
 133 
 134         if err := w.Flush(); err != nil {
 135             return io.EOF
 136         }
 137     }
 138 
 139     return sc.Err()
 140 }
 141 
 142 func writeSqueezed(w *bufio.Writer, s []byte) {
 143     // ignore leading spaces
 144     for len(s) > 0 && s[0] == ' ' {
 145         s = s[1:]
 146     }
 147 
 148     // ignore trailing spaces
 149     for len(s) > 0 && s[len(s)-1] == ' ' {
 150         s = s[:len(s)-1]
 151     }
 152 
 153     space := false
 154 
 155     for len(s) > 0 {
 156         switch s[0] {
 157         case ' ':
 158             s = s[1:]
 159             space = true
 160 
 161         case '\t':
 162             s = s[1:]
 163             space = false
 164             for len(s) > 0 && s[0] == ' ' {
 165                 s = s[1:]
 166             }
 167             w.WriteByte('\t')
 168 
 169         default:
 170             if space {
 171                 w.WriteByte(' ')
 172                 space = false
 173             }
 174             w.WriteByte(s[0])
 175             s = s[1:]
 176         }
 177     }
 178 }