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 }