general.go (13120B)
1 // Copyright 2019 The Hugo Authors. All rights reserved.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13
14 package helpers
15
16 import (
17 "bytes"
18 "crypto/md5"
19 "encoding/hex"
20 "fmt"
21 "io"
22 "net"
23 "os"
24 "path/filepath"
25 "sort"
26 "strconv"
27 "strings"
28 "sync"
29 "unicode"
30 "unicode/utf8"
31
32 "github.com/gohugoio/hugo/common/loggers"
33
34 "github.com/mitchellh/hashstructure"
35
36 "github.com/gohugoio/hugo/common/hugo"
37
38 "github.com/spf13/afero"
39
40 "github.com/jdkato/prose/transform"
41
42 bp "github.com/gohugoio/hugo/bufferpool"
43 "github.com/spf13/pflag"
44 )
45
46 // FilePathSeparator as defined by os.Separator.
47 const FilePathSeparator = string(filepath.Separator)
48
49 // FindAvailablePort returns an available and valid TCP port.
50 func FindAvailablePort() (*net.TCPAddr, error) {
51 l, err := net.Listen("tcp", ":0")
52 if err == nil {
53 defer l.Close()
54 addr := l.Addr()
55 if a, ok := addr.(*net.TCPAddr); ok {
56 return a, nil
57 }
58 return nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
59 }
60 return nil, err
61 }
62
63 // TCPListen starts listening on a valid TCP port.
64 func TCPListen() (net.Listener, *net.TCPAddr, error) {
65 l, err := net.Listen("tcp", ":0")
66 if err != nil {
67 return nil, nil, err
68 }
69 addr := l.Addr()
70 if a, ok := addr.(*net.TCPAddr); ok {
71 return l, a, nil
72 }
73 l.Close()
74 return nil, nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
75
76 }
77
78 // InStringArray checks if a string is an element of a slice of strings
79 // and returns a boolean value.
80 func InStringArray(arr []string, el string) bool {
81 for _, v := range arr {
82 if v == el {
83 return true
84 }
85 }
86 return false
87 }
88
89 // FirstUpper returns a string with the first character as upper case.
90 func FirstUpper(s string) string {
91 if s == "" {
92 return ""
93 }
94 r, n := utf8.DecodeRuneInString(s)
95 return string(unicode.ToUpper(r)) + s[n:]
96 }
97
98 // UniqueStrings returns a new slice with any duplicates removed.
99 func UniqueStrings(s []string) []string {
100 unique := make([]string, 0, len(s))
101 for i, val := range s {
102 var seen bool
103 for j := 0; j < i; j++ {
104 if s[j] == val {
105 seen = true
106 break
107 }
108 }
109 if !seen {
110 unique = append(unique, val)
111 }
112 }
113 return unique
114 }
115
116 // UniqueStringsReuse returns a slice with any duplicates removed.
117 // It will modify the input slice.
118 func UniqueStringsReuse(s []string) []string {
119 result := s[:0]
120 for i, val := range s {
121 var seen bool
122
123 for j := 0; j < i; j++ {
124 if s[j] == val {
125 seen = true
126 break
127 }
128 }
129
130 if !seen {
131 result = append(result, val)
132 }
133 }
134 return result
135 }
136
137 // UniqueStringsReuse returns a sorted slice with any duplicates removed.
138 // It will modify the input slice.
139 func UniqueStringsSorted(s []string) []string {
140 if len(s) == 0 {
141 return nil
142 }
143 ss := sort.StringSlice(s)
144 ss.Sort()
145 i := 0
146 for j := 1; j < len(s); j++ {
147 if !ss.Less(i, j) {
148 continue
149 }
150 i++
151 s[i] = s[j]
152 }
153
154 return s[:i+1]
155 }
156
157 // ReaderToBytes takes an io.Reader argument, reads from it
158 // and returns bytes.
159 func ReaderToBytes(lines io.Reader) []byte {
160 if lines == nil {
161 return []byte{}
162 }
163 b := bp.GetBuffer()
164 defer bp.PutBuffer(b)
165
166 b.ReadFrom(lines)
167
168 bc := make([]byte, b.Len())
169 copy(bc, b.Bytes())
170 return bc
171 }
172
173 // ReaderToString is the same as ReaderToBytes, but returns a string.
174 func ReaderToString(lines io.Reader) string {
175 if lines == nil {
176 return ""
177 }
178 b := bp.GetBuffer()
179 defer bp.PutBuffer(b)
180 b.ReadFrom(lines)
181 return b.String()
182 }
183
184 // ReaderContains reports whether subslice is within r.
185 func ReaderContains(r io.Reader, subslice []byte) bool {
186 if r == nil || len(subslice) == 0 {
187 return false
188 }
189
190 bufflen := len(subslice) * 4
191 halflen := bufflen / 2
192 buff := make([]byte, bufflen)
193 var err error
194 var n, i int
195
196 for {
197 i++
198 if i == 1 {
199 n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
200 } else {
201 if i != 2 {
202 // shift left to catch overlapping matches
203 copy(buff[:], buff[halflen:])
204 }
205 n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
206 }
207
208 if n > 0 && bytes.Contains(buff, subslice) {
209 return true
210 }
211
212 if err != nil {
213 break
214 }
215 }
216 return false
217 }
218
219 // GetTitleFunc returns a func that can be used to transform a string to
220 // title case.
221 //
222 // The supported styles are
223 //
224 // - "Go" (strings.Title)
225 // - "AP" (see https://www.apstylebook.com/)
226 // - "Chicago" (see http://www.chicagomanualofstyle.org/home.html)
227 //
228 // If an unknown or empty style is provided, AP style is what you get.
229 func GetTitleFunc(style string) func(s string) string {
230 switch strings.ToLower(style) {
231 case "go":
232 return strings.Title
233 case "chicago":
234 tc := transform.NewTitleConverter(transform.ChicagoStyle)
235 return tc.Title
236 default:
237 tc := transform.NewTitleConverter(transform.APStyle)
238 return tc.Title
239 }
240 }
241
242 // HasStringsPrefix tests whether the string slice s begins with prefix slice s.
243 func HasStringsPrefix(s, prefix []string) bool {
244 return len(s) >= len(prefix) && compareStringSlices(s[0:len(prefix)], prefix)
245 }
246
247 // HasStringsSuffix tests whether the string slice s ends with suffix slice s.
248 func HasStringsSuffix(s, suffix []string) bool {
249 return len(s) >= len(suffix) && compareStringSlices(s[len(s)-len(suffix):], suffix)
250 }
251
252 func compareStringSlices(a, b []string) bool {
253 if a == nil && b == nil {
254 return true
255 }
256
257 if a == nil || b == nil {
258 return false
259 }
260
261 if len(a) != len(b) {
262 return false
263 }
264
265 for i := range a {
266 if a[i] != b[i] {
267 return false
268 }
269 }
270
271 return true
272 }
273
274 // DistinctLogger ignores duplicate log statements.
275 type DistinctLogger struct {
276 loggers.Logger
277 sync.RWMutex
278 m map[string]bool
279 }
280
281 func (l *DistinctLogger) Reset() {
282 l.Lock()
283 defer l.Unlock()
284
285 l.m = make(map[string]bool)
286 }
287
288 // Println will log the string returned from fmt.Sprintln given the arguments,
289 // but not if it has been logged before.
290 func (l *DistinctLogger) Println(v ...any) {
291 // fmt.Sprint doesn't add space between string arguments
292 logStatement := strings.TrimSpace(fmt.Sprintln(v...))
293 l.printIfNotPrinted("println", logStatement, func() {
294 l.Logger.Println(logStatement)
295 })
296 }
297
298 // Printf will log the string returned from fmt.Sprintf given the arguments,
299 // but not if it has been logged before.
300 func (l *DistinctLogger) Printf(format string, v ...any) {
301 logStatement := fmt.Sprintf(format, v...)
302 l.printIfNotPrinted("printf", logStatement, func() {
303 l.Logger.Printf(format, v...)
304 })
305 }
306
307 func (l *DistinctLogger) Debugf(format string, v ...any) {
308 logStatement := fmt.Sprintf(format, v...)
309 l.printIfNotPrinted("debugf", logStatement, func() {
310 l.Logger.Debugf(format, v...)
311 })
312 }
313
314 func (l *DistinctLogger) Debugln(v ...any) {
315 logStatement := fmt.Sprint(v...)
316 l.printIfNotPrinted("debugln", logStatement, func() {
317 l.Logger.Debugln(v...)
318 })
319 }
320
321 func (l *DistinctLogger) Infof(format string, v ...any) {
322 logStatement := fmt.Sprintf(format, v...)
323 l.printIfNotPrinted("info", logStatement, func() {
324 l.Logger.Infof(format, v...)
325 })
326 }
327
328 func (l *DistinctLogger) Infoln(v ...any) {
329 logStatement := fmt.Sprint(v...)
330 l.printIfNotPrinted("infoln", logStatement, func() {
331 l.Logger.Infoln(v...)
332 })
333 }
334
335 func (l *DistinctLogger) Warnf(format string, v ...any) {
336 logStatement := fmt.Sprintf(format, v...)
337 l.printIfNotPrinted("warnf", logStatement, func() {
338 l.Logger.Warnf(format, v...)
339 })
340 }
341
342 func (l *DistinctLogger) Warnln(v ...any) {
343 logStatement := fmt.Sprint(v...)
344 l.printIfNotPrinted("warnln", logStatement, func() {
345 l.Logger.Warnln(v...)
346 })
347 }
348
349 func (l *DistinctLogger) Errorf(format string, v ...any) {
350 logStatement := fmt.Sprint(v...)
351 l.printIfNotPrinted("errorf", logStatement, func() {
352 l.Logger.Errorf(format, v...)
353 })
354 }
355
356 func (l *DistinctLogger) Errorln(v ...any) {
357 logStatement := fmt.Sprint(v...)
358 l.printIfNotPrinted("errorln", logStatement, func() {
359 l.Logger.Errorln(v...)
360 })
361 }
362
363 func (l *DistinctLogger) hasPrinted(key string) bool {
364 l.RLock()
365 defer l.RUnlock()
366 _, found := l.m[key]
367 return found
368 }
369
370 func (l *DistinctLogger) printIfNotPrinted(level, logStatement string, print func()) {
371 key := level + logStatement
372 if l.hasPrinted(key) {
373 return
374 }
375 l.Lock()
376 defer l.Unlock()
377 l.m[key] = true // Placing this after print() can cause duplicate warning entries to be logged when --panicOnWarning is true.
378 print()
379
380 }
381
382 // NewDistinctErrorLogger creates a new DistinctLogger that logs ERRORs
383 func NewDistinctErrorLogger() loggers.Logger {
384 return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewErrorLogger()}
385 }
386
387 // NewDistinctLogger creates a new DistinctLogger that logs to the provided logger.
388 func NewDistinctLogger(logger loggers.Logger) loggers.Logger {
389 return &DistinctLogger{m: make(map[string]bool), Logger: logger}
390 }
391
392 // NewDistinctWarnLogger creates a new DistinctLogger that logs WARNs
393 func NewDistinctWarnLogger() loggers.Logger {
394 return &DistinctLogger{m: make(map[string]bool), Logger: loggers.NewWarningLogger()}
395 }
396
397 var (
398 // DistinctErrorLog can be used to avoid spamming the logs with errors.
399 DistinctErrorLog = NewDistinctErrorLogger()
400
401 // DistinctWarnLog can be used to avoid spamming the logs with warnings.
402 DistinctWarnLog = NewDistinctWarnLogger()
403 )
404
405 // InitLoggers resets the global distinct loggers.
406 func InitLoggers() {
407 DistinctErrorLog.Reset()
408 DistinctWarnLog.Reset()
409 }
410
411 // Deprecated informs about a deprecation, but only once for a given set of arguments' values.
412 // If the err flag is enabled, it logs as an ERROR (will exit with -1) and the text will
413 // point at the next Hugo release.
414 // The idea is two remove an item in two Hugo releases to give users and theme authors
415 // plenty of time to fix their templates.
416 func Deprecated(item, alternative string, err bool) {
417 if err {
418 DistinctErrorLog.Errorf("%s is deprecated and will be removed in Hugo %s. %s", item, hugo.CurrentVersion.Next().ReleaseVersion(), alternative)
419 } else {
420 var warnPanicMessage string
421 if !loggers.PanicOnWarning {
422 warnPanicMessage = "\n\nRe-run Hugo with the flag --panicOnWarning to get a better error message."
423 }
424 DistinctWarnLog.Warnf("%s is deprecated and will be removed in a future release. %s%s", item, alternative, warnPanicMessage)
425 }
426 }
427
428 // SliceToLower goes through the source slice and lowers all values.
429 func SliceToLower(s []string) []string {
430 if s == nil {
431 return nil
432 }
433
434 l := make([]string, len(s))
435 for i, v := range s {
436 l[i] = strings.ToLower(v)
437 }
438
439 return l
440 }
441
442 // MD5String takes a string and returns its MD5 hash.
443 func MD5String(f string) string {
444 h := md5.New()
445 h.Write([]byte(f))
446 return hex.EncodeToString(h.Sum([]byte{}))
447 }
448
449 // MD5FromFileFast creates a MD5 hash from the given file. It only reads parts of
450 // the file for speed, so don't use it if the files are very subtly different.
451 // It will not close the file.
452 func MD5FromFileFast(r io.ReadSeeker) (string, error) {
453 const (
454 // Do not change once set in stone!
455 maxChunks = 8
456 peekSize = 64
457 seek = 2048
458 )
459
460 h := md5.New()
461 buff := make([]byte, peekSize)
462
463 for i := 0; i < maxChunks; i++ {
464 if i > 0 {
465 _, err := r.Seek(seek, 0)
466 if err != nil {
467 if err == io.EOF {
468 break
469 }
470 return "", err
471 }
472 }
473
474 _, err := io.ReadAtLeast(r, buff, peekSize)
475 if err != nil {
476 if err == io.EOF || err == io.ErrUnexpectedEOF {
477 h.Write(buff)
478 break
479 }
480 return "", err
481 }
482 h.Write(buff)
483 }
484
485 return hex.EncodeToString(h.Sum(nil)), nil
486 }
487
488 // MD5FromReader creates a MD5 hash from the given reader.
489 func MD5FromReader(r io.Reader) (string, error) {
490 h := md5.New()
491 if _, err := io.Copy(h, r); err != nil {
492 return "", nil
493 }
494 return hex.EncodeToString(h.Sum(nil)), nil
495 }
496
497 // IsWhitespace determines if the given rune is whitespace.
498 func IsWhitespace(r rune) bool {
499 return r == ' ' || r == '\t' || r == '\n' || r == '\r'
500 }
501
502 // NormalizeHugoFlags facilitates transitions of Hugo command-line flags,
503 // e.g. --baseUrl to --baseURL, --uglyUrls to --uglyURLs
504 func NormalizeHugoFlags(f *pflag.FlagSet, name string) pflag.NormalizedName {
505 switch name {
506 case "baseUrl":
507 name = "baseURL"
508 case "uglyUrls":
509 name = "uglyURLs"
510 }
511 return pflag.NormalizedName(name)
512 }
513
514 // PrintFs prints the given filesystem to the given writer starting from the given path.
515 // This is useful for debugging.
516 func PrintFs(fs afero.Fs, path string, w io.Writer) {
517 if fs == nil {
518 return
519 }
520
521 afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
522 fmt.Println(path)
523 return nil
524 })
525 }
526
527 // HashString returns a hash from the given elements.
528 // It will panic if the hash cannot be calculated.
529 func HashString(elements ...any) string {
530 var o any
531 if len(elements) == 1 {
532 o = elements[0]
533 } else {
534 o = elements
535 }
536
537 hash, err := hashstructure.Hash(o, nil)
538 if err != nil {
539 panic(err)
540 }
541 return strconv.FormatUint(hash, 10)
542 }