github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/printf/printf.go (about)

     1  // Package printf implements a parser for fmt.Printf-style format
     2  // strings.
     3  //
     4  // It parses verbs according to the following syntax:
     5  //
     6  //	Numeric -> '0'-'9'
     7  //	Letter -> 'a'-'z' | 'A'-'Z'
     8  //	Index -> '[' Numeric+ ']'
     9  //	Star -> '*'
    10  //	Star -> Index '*'
    11  //
    12  //	Precision -> Numeric+ | Star
    13  //	Width -> Numeric+ | Star
    14  //
    15  //	WidthAndPrecision -> Width '.' Precision
    16  //	WidthAndPrecision -> Width '.'
    17  //	WidthAndPrecision -> Width
    18  //	WidthAndPrecision -> '.' Precision
    19  //	WidthAndPrecision -> '.'
    20  //
    21  //	Flag -> '+' | '-' | '#' | ' ' | '0'
    22  //	Verb -> Letter | '%'
    23  //
    24  //	Input -> '%' [ Flag+ ] [ WidthAndPrecision ] [ Index ] Verb
    25  package printf
    26  
    27  import (
    28  	"errors"
    29  	"regexp"
    30  	"strconv"
    31  	"strings"
    32  )
    33  
    34  // ErrInvalid is returned for invalid format strings or verbs.
    35  var ErrInvalid = errors.New("invalid format string")
    36  
    37  type Verb struct {
    38  	Letter rune
    39  	Flags  string
    40  
    41  	Width     Argument
    42  	Precision Argument
    43  	// Which value in the argument list the verb uses.
    44  	// -1 denotes the next argument,
    45  	// values > 0 denote explicit arguments.
    46  	// The value 0 denotes that no argument is consumed. This is the case for %%.
    47  	Value int
    48  
    49  	Raw string
    50  }
    51  
    52  // Argument is an implicit or explicit width or precision.
    53  type Argument interface {
    54  	isArgument()
    55  }
    56  
    57  // The Default value, when no width or precision is provided.
    58  type Default struct{}
    59  
    60  // Zero is the implicit zero value.
    61  // This value may only appear for precisions in format strings like %6.f
    62  type Zero struct{}
    63  
    64  // Star is a * value, which may either refer to the next argument (Index == -1) or an explicit argument.
    65  type Star struct{ Index int }
    66  
    67  // A Literal value, such as 6 in %6d.
    68  type Literal int
    69  
    70  func (Default) isArgument() {}
    71  func (Zero) isArgument()    {}
    72  func (Star) isArgument()    {}
    73  func (Literal) isArgument() {}
    74  
    75  // Parse parses f and returns a list of actions.
    76  // An action may either be a literal string, or a Verb.
    77  func Parse(f string) ([]interface{}, error) {
    78  	var out []interface{}
    79  	for len(f) > 0 {
    80  		if f[0] == '%' {
    81  			v, n, err := ParseVerb(f)
    82  			if err != nil {
    83  				return nil, err
    84  			}
    85  			f = f[n:]
    86  			out = append(out, v)
    87  		} else {
    88  			n := strings.IndexByte(f, '%')
    89  			if n > -1 {
    90  				out = append(out, f[:n])
    91  				f = f[n:]
    92  			} else {
    93  				out = append(out, f)
    94  				f = ""
    95  			}
    96  		}
    97  	}
    98  
    99  	return out, nil
   100  }
   101  
   102  func atoi(s string) int {
   103  	n, _ := strconv.Atoi(s)
   104  	return n
   105  }
   106  
   107  // ParseVerb parses the verb at the beginning of f.
   108  // It returns the verb, how much of the input was consumed, and an error, if any.
   109  func ParseVerb(f string) (Verb, int, error) {
   110  	if len(f) < 2 {
   111  		return Verb{}, 0, ErrInvalid
   112  	}
   113  	const (
   114  		flags = 1
   115  
   116  		width      = 2
   117  		widthStar  = 3
   118  		widthIndex = 5
   119  
   120  		dot       = 6
   121  		prec      = 7
   122  		precStar  = 8
   123  		precIndex = 10
   124  
   125  		verbIndex = 11
   126  		verb      = 12
   127  	)
   128  
   129  	m := re.FindStringSubmatch(f)
   130  	if m == nil {
   131  		return Verb{}, 0, ErrInvalid
   132  	}
   133  
   134  	v := Verb{
   135  		Letter: []rune(m[verb])[0],
   136  		Flags:  m[flags],
   137  		Raw:    m[0],
   138  	}
   139  
   140  	if m[width] != "" {
   141  		// Literal width
   142  		v.Width = Literal(atoi(m[width]))
   143  	} else if m[widthStar] != "" {
   144  		// Star width
   145  		if m[widthIndex] != "" {
   146  			v.Width = Star{atoi(m[widthIndex])}
   147  		} else {
   148  			v.Width = Star{-1}
   149  		}
   150  	} else {
   151  		// Default width
   152  		v.Width = Default{}
   153  	}
   154  
   155  	if m[dot] == "" {
   156  		// default precision
   157  		v.Precision = Default{}
   158  	} else {
   159  		if m[prec] != "" {
   160  			// Literal precision
   161  			v.Precision = Literal(atoi(m[prec]))
   162  		} else if m[precStar] != "" {
   163  			// Star precision
   164  			if m[precIndex] != "" {
   165  				v.Precision = Star{atoi(m[precIndex])}
   166  			} else {
   167  				v.Precision = Star{-1}
   168  			}
   169  		} else {
   170  			// Zero precision
   171  			v.Precision = Zero{}
   172  		}
   173  	}
   174  
   175  	if m[verb] == "%" {
   176  		v.Value = 0
   177  	} else if m[verbIndex] != "" {
   178  		v.Value = atoi(m[verbIndex])
   179  	} else {
   180  		v.Value = -1
   181  	}
   182  
   183  	return v, len(m[0]), nil
   184  }
   185  
   186  const (
   187  	flags             = `([+#0 -]*)`
   188  	verb              = `([a-zA-Z%])`
   189  	index             = `(?:\[([0-9]+)\])`
   190  	star              = `((` + index + `)?\*)`
   191  	width1            = `([0-9]+)`
   192  	width2            = star
   193  	width             = `(?:` + width1 + `|` + width2 + `)`
   194  	precision         = width
   195  	widthAndPrecision = `(?:(?:` + width + `)?(?:(\.)(?:` + precision + `)?)?)`
   196  )
   197  
   198  var re = regexp.MustCompile(`^%` + flags + widthAndPrecision + `?` + index + `?` + verb)