github.com/atrn/dcc@v0.0.0-20220806184050-4470d2553272/options.go (about)

     1  // dcc - dependency-driven C/C++ compiler front end
     2  //
     3  // Copyright © A.Newman 2015.
     4  //
     5  // This source code is released under version 2 of the  GNU Public License.
     6  // See the file LICENSE for details.
     7  //
     8  
     9  package main
    10  
    11  import (
    12  	"bufio"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  	"time"
    20  )
    21  
    22  const (
    23  	includeDirective = "!include"
    24  	inheritDirective = "!inherit"
    25  	ifdefDirective   = "!ifdef"
    26  	ifndefDirective  = "!ifndef"
    27  	elseDirective    = "!else"
    28  	endifDirective   = "!endif"
    29  	errorDirective   = "!error"
    30  )
    31  
    32  // Options represents a series of words and is used to represent
    33  // compiler and linker options. Options are intended to be read from a
    34  // file and act as a dependency to the build.
    35  //
    36  // An Options has a slice of strings, the option "values".
    37  //
    38  type Options struct {
    39  	Values   []string    // option values
    40  	Path     string      // associated file path
    41  	fileinfo os.FileInfo // actual options file info
    42  	mtime    time.Time   // options modtime, mutable
    43  }
    44  
    45  // NewOptions returns a new, empty, Options value
    46  //
    47  func NewOptions() *Options {
    48  	return &Options{
    49  		Values: make([]string, 0),
    50  	}
    51  }
    52  
    53  // FileInfo returns the receiver's os.FileInfo set when the
    54  // receiver was successfully read from a file.
    55  //
    56  func (o *Options) FileInfo() os.FileInfo {
    57  	return o.fileinfo
    58  }
    59  
    60  // Len returns the number of values defined by the receiver.
    61  //
    62  func (o *Options) Len() int {
    63  	return len(o.Values)
    64  }
    65  
    66  // Empty returns true if the Options has no values
    67  //
    68  func (o *Options) Empty() bool {
    69  	return len(o.Values) == 0
    70  }
    71  
    72  // String returns a string representation of the receiver.  This
    73  // returns a space separated list of the options' Values.
    74  //
    75  func (o *Options) String() string {
    76  	return strings.Join(o.Values, " ")
    77  }
    78  
    79  // Changed updates the "modtime" of the receiver, to "now".
    80  //
    81  func (o *Options) Changed() {
    82  	o.mtime = time.Now()
    83  }
    84  
    85  // Append appends an option to the set of options.  Note, Append does
    86  // NOT modify the mtime of the receiver.
    87  //
    88  func (o *Options) Append(option string) {
    89  	o.Values = append(o.Values, option)
    90  }
    91  
    92  // Prepend inserts an option at the start of the set of options.
    93  // Note, Prepend does NOT modify the mtime of the receiver.
    94  //
    95  func (o *Options) Prepend(option string) {
    96  	o.Values = append([]string{option}, o.Values...)
    97  }
    98  
    99  // SetModTime sets the receiver's modification time to the supplied
   100  // value.
   101  //
   102  func (o *Options) SetModTime(t time.Time) {
   103  	o.mtime = t
   104  }
   105  
   106  // Return the options modification time.
   107  //
   108  func (o *Options) ModTime() time.Time {
   109  	return o.mtime
   110  }
   111  
   112  // SetFrom copies options from another Options leaving
   113  // the receiver's Path unmodified.
   114  //
   115  func (o *Options) SetFrom(other *Options) {
   116  	o.Values = make([]string, len(other.Values))
   117  	copy(o.Values, other.Values)
   118  	o.mtime = other.mtime
   119  	o.fileinfo = other.fileinfo
   120  }
   121  
   122  // OptionIndex locates a specific option and returns its index within
   123  // the receiver's Values.  If no option is found -1 is returned.
   124  //
   125  func (o *Options) OptionIndex(s string) int {
   126  	for i := 0; i < len(o.Values); i++ {
   127  		if o.Values[i] == s {
   128  			return i
   129  		}
   130  	}
   131  	return -1
   132  }
   133  
   134  // ReadFromFile reads options from a text file.
   135  //
   136  // Options files are word-based with each non-blank, non-comment line
   137  // being split into space-separated fields.
   138  //
   139  // An optional 'filter' function can be supplied which is applied to
   140  // each option word before it is added to the receiver's Values slice.
   141  //
   142  // Returns true if everything worked, false if the file could not be
   143  // read for some reason.
   144  //
   145  func (o *Options) ReadFromFile(filename string, filter func(string) string) (bool, error) {
   146  	file, err := os.Open(filename)
   147  	if err != nil {
   148  		return false, err
   149  	}
   150  	defer file.Close()
   151  	o.Path = filename
   152  	info, err := file.Stat()
   153  	if err != nil {
   154  		return true, err
   155  	}
   156  	o.fileinfo = info
   157  	o.SetModTime(info.ModTime())
   158  	return o.ReadFromReader(file, filename, filter)
   159  }
   160  
   161  // Read options from the given io.Reader.
   162  //
   163  func (o *Options) ReadFromReader(r io.Reader, filename string, filter func(string) string) (bool, error) {
   164  	if filter == nil {
   165  		filter = func(s string) string { return s }
   166  	}
   167  	var conditional Conditional
   168  	input := bufio.NewScanner(r)
   169  	lineNumber := 0
   170  	for input.Scan() {
   171  		line := input.Text()
   172  		lineNumber++
   173  
   174  		if line == "" || line[0] == '#' {
   175  			continue
   176  		}
   177  
   178  		fields := strings.Fields(line)
   179  		if len(fields) == 0 {
   180  			continue
   181  		}
   182  
   183  		evalCondition := func(invert bool) error {
   184  			if len(fields) != 2 {
   185  				return reportErrorInFile(filename, lineNumber, fmt.Sprintf("%s requires a single parameter", fields[0]))
   186  			}
   187  			val := os.Getenv(fields[1])
   188  			state1, state2 := TrueConditionState, FalseConditionState
   189  			if invert {
   190  				state1, state2 = FalseConditionState, TrueConditionState
   191  			}
   192  			if val != "" {
   193  				conditional.PushState(state1)
   194  			} else {
   195  				conditional.PushState(state2)
   196  			}
   197  			return nil
   198  		}
   199  
   200  		if fields[0] == errorDirective {
   201  			if !conditional.IsSkippingLines() {
   202  				message := strings.Join(fields[1:], " ")
   203  				if message == "" {
   204  					message = "error"
   205  				}
   206  				return false, reportErrorInFile(filename, lineNumber, message)
   207  			}
   208  			continue
   209  		}
   210  
   211  		if fields[0] == ifdefDirective {
   212  			if conditional.IsSkippingLines() {
   213  				conditional.PushState(conditional.CurrentState())
   214  			} else if err := evalCondition(false); err != nil {
   215  				return false, err
   216  			} else {
   217  				continue
   218  			}
   219  		}
   220  
   221  		if fields[0] == ifndefDirective {
   222  			if conditional.IsSkippingLines() {
   223  				conditional.PushState(conditional.CurrentState())
   224  			} else if err := evalCondition(true); err != nil {
   225  				return false, err
   226  			} else {
   227  				continue
   228  			}
   229  		}
   230  
   231  		if fields[0] == elseDirective {
   232  			if !conditional.IsActive() {
   233  				return false, reportErrorInFile(filename, lineNumber, ErrNoCondition.Error())
   234  			}
   235  			conditional.ToggleState()
   236  			continue
   237  		}
   238  
   239  		if fields[0] == endifDirective {
   240  			if !conditional.IsActive() {
   241  				return false, reportErrorInFile(filename, lineNumber, ErrNoCondition.Error())
   242  			}
   243  			if err := conditional.PopState(); err != nil {
   244  				return false, err
   245  			}
   246  			continue
   247  		}
   248  
   249  		if conditional.IsSkippingLines() {
   250  			continue
   251  		}
   252  
   253  		// !include <filename>
   254  		//
   255  		if fields[0] == includeDirective {
   256  			if err := o.includeFile(filename, lineNumber, line, fields, filter); err != nil {
   257  				return false, err
   258  			}
   259  			continue
   260  		}
   261  
   262  		// !inherit [<filename>]
   263  		//
   264  		if fields[0] == inheritDirective {
   265  			if err := o.inheritFile(filename, lineNumber, line, fields, filter); err != nil {
   266  				return false, err
   267  			}
   268  			continue
   269  		}
   270  
   271  		// Otherwise, treat fields (tokens) as options to be included.
   272  		// Expand (interpolate) any variable references, filter and
   273  		// collect any non-empty strings.
   274  		for _, field := range fields {
   275  			field = os.ExpandEnv(field)
   276  			fields2 := strings.Fields(field)
   277  			for _, field2 := range fields2 {
   278  				if field2 = filter(field2); field2 != "" {
   279  					o.Values = append(o.Values, field2)
   280  				}
   281  			}
   282  		}
   283  	}
   284  	return true, nil
   285  }
   286  
   287  func extractFilename(filename string) string {
   288  	if len(filename) < 2 {
   289  		return filename
   290  	}
   291  	if filename[0] == '"' {
   292  		return RemoveDelimiters(filename, '"', '"')
   293  	}
   294  	if filename[0] == '<' {
   295  		return RemoveDelimiters(filename, '<', '>')
   296  	}
   297  	return filename
   298  }
   299  
   300  func reportErrorInFile(filename string, lineNumber int, what string) error {
   301  	return fmt.Errorf("error: %s:%d %s", filename, lineNumber, what)
   302  }
   303  
   304  func malformedLine(filename string, lineNumber int, what, line string) error {
   305  	return reportErrorInFile(filename, lineNumber, fmt.Sprintf("malformed %s - %s", what, line))
   306  }
   307  
   308  func (o *Options) includeFile(parentFilename string, lineNumber int, line string, fields []string, filter func(string) string) error {
   309  	if len(fields) != 2 {
   310  		return malformedLine(parentFilename, lineNumber, includeDirective, line)
   311  	}
   312  	Assert(fields[1] != "", "unexpected empty field returned from strings.Fields()")
   313  	filename := fields[1]
   314  	if filename[0] == '"' {
   315  		filename = RemoveDelimiters(filename, '"', '"')
   316  	} else if filename[0] == '<' {
   317  		filename = RemoveDelimiters(filename, '<', '>')
   318  	}
   319  	path := filepath.Join(filepath.Dir(parentFilename), filename)
   320  	if Debug {
   321  		log.Printf("DEBUG: %q including %q", parentFilename, path)
   322  	}
   323  	_, err := o.ReadFromFile(path, filter)
   324  	return err
   325  }
   326  
   327  func (o *Options) inheritFile(parentFilename string, lineNumber int, line string, fields []string, filter func(string) string) error {
   328  	if len(fields) > 2 {
   329  		return malformedLine(parentFilename, lineNumber, inheritDirective, line)
   330  	}
   331  	startingDir, inheritedFilename := ParentDir(parentFilename), Basename(parentFilename)
   332  	if len(fields) > 1 {
   333  		inheritedFilename = fields[1]
   334  	}
   335  	if Debug {
   336  		log.Printf("OPTIONS: %q !inherit (%q)", parentFilename, inheritedFilename)
   337  	}
   338  	path, _, found, err := FindFileFromDirectory(inheritedFilename, startingDir)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	if !found {
   343  		return reportErrorInFile(parentFilename, lineNumber, fmt.Sprintf("inherited file %q not found", inheritedFilename))
   344  	}
   345  
   346  	if Debug {
   347  		log.Printf("DEBUG: %q inheriting %q", parentFilename, path)
   348  	}
   349  
   350  	ok, err := o.ReadFromFile(path, filter)
   351  	if err != nil {
   352  		return err
   353  	}
   354  
   355  	if !ok {
   356  		return reportErrorInFile(parentFilename, lineNumber, fmt.Sprintf("error reading inherited file %q", path))
   357  	}
   358  
   359  	return nil
   360  }
   361  
   362  //
   363  // MostRecentModTime returns the modification time of the most recently
   364  // modified of the two Options.
   365  //
   366  func MostRecentModTime(a *Options, b *Options) time.Time {
   367  	at, bt := a.ModTime(), b.ModTime()
   368  	if at.After(bt) {
   369  		return at
   370  	}
   371  	return bt
   372  }