github.com/argots/stencil@v0.0.2/pkg/stencil/stencil.go (about)

     1  // Package stencil implements a template package manager.
     2  package stencil
     3  
     4  import (
     5  	"bytes"
     6  	"errors"
     7  	"flag"
     8  	"os"
     9  	"text/template"
    10  )
    11  
    12  // Logger is the interface used by stencil to log messages.
    13  //
    14  // Use log.New(...) to create an appropriate logger.
    15  type Logger interface {
    16  	Printf(format string, v ...interface{})
    17  }
    18  
    19  // FileSystem is the generic file system used by stencil.
    20  // Use FS{} or a custom implementation.
    21  type FileSystem interface {
    22  	Read(path string) ([]byte, error)
    23  	Write(path string, data []byte, mode os.FileMode) error
    24  	Remove(path string) error
    25  	RemoveAll(path string) error
    26  }
    27  
    28  // Prompter is the generic interface to prompt and fetch info
    29  // interactively.
    30  type Prompter interface {
    31  	PromptBool(prompt string) (bool, error)
    32  	PromptString(prompt string) (string, error)
    33  }
    34  
    35  // New creates a new stencil manager.
    36  func New(verbose, errorl Logger, p Prompter, fs FileSystem) *Stencil {
    37  	s := &Stencil{
    38  		State:  map[string]interface{}{},
    39  		Funcs:  map[string]interface{}{},
    40  		Printf: verbose.Printf,
    41  		Errorf: func(fmt string, v ...interface{}) error {
    42  			errorl.Printf(fmt, v...)
    43  			err, _ := v[len(v)-1].(error)
    44  			return err
    45  		},
    46  		FileSystem: fs,
    47  		Prompter:   p,
    48  		Binary:     Binary{},
    49  		Objects: Objects{
    50  			Before:       &Objects{},
    51  			Pulls:        map[string]bool{},
    52  			Files:        map[string]*FileObj{},
    53  			FileArchives: map[string]*FileArchiveObj{},
    54  			Bools:        map[string]bool{},
    55  			Strings:      map[string]string{},
    56  		},
    57  		Vars: Vars{
    58  			BoolDefs:   map[string]string{},
    59  			StringDefs: map[string]string{},
    60  		},
    61  		Markdown: Markdown{},
    62  	}
    63  	s.Binary.Stencil = s
    64  	s.Objects.Stencil = s
    65  	s.Vars.Stencil = s
    66  	s.Markdown.Stencil = s
    67  	s.Funcs["stencil"] = func() interface{} {
    68  		return s
    69  	}
    70  	return s
    71  }
    72  
    73  // Stencil maintains all the state for managing a single directory.
    74  type Stencil struct {
    75  	State  map[string]interface{}
    76  	Funcs  map[string]interface{}
    77  	Printf func(format string, v ...interface{})
    78  	Errorf func(format string, v ...interface{}) error
    79  	FileSystem
    80  	Prompter
    81  	Env
    82  	Binary
    83  	Objects
    84  	Vars
    85  	Markdown
    86  }
    87  
    88  // Main implements the main program.
    89  func (s *Stencil) Main(f *flag.FlagSet, args []string) error {
    90  	errMissingArg := errors.New("missing arg")
    91  	f.Usage = func() {
    92  		s.Printf(`Usage: stencil [options] commands
    93    commands:
    94      pull url_or_file -- add url to pulls and sync
    95      rm url_or_fil    -- remove url from pulls and sync
    96      sync             -- update all existing pulls
    97  `)
    98  		f.PrintDefaults()
    99  	}
   100  	s.Vars.Init(f)
   101  	if err := f.Parse(args[1:]); err != nil {
   102  		return s.Errorf("flagset parse", err)
   103  	}
   104  
   105  	switch f.Arg(0) {
   106  	case "pull":
   107  		if f.Arg(1) != "" {
   108  			s.Printf("Adding %s\n", f.Arg(1))
   109  			return s.run(f.Arg(1), "")
   110  		}
   111  		return s.Errorf("pull requires a url or path to a recipe %v\n", errMissingArg)
   112  	case "sync":
   113  		s.Printf("Updating all pulled recipes\n")
   114  		return s.run("", "")
   115  	case "rm":
   116  		if f.Arg(1) != "" {
   117  			s.Printf("Removing %s\n", f.Arg(1))
   118  			return s.run("", f.Arg(1))
   119  		}
   120  		return s.Errorf("rm requires a url or path to a recipe %v\n", errMissingArg)
   121  	case "":
   122  		f.Usage()
   123  		return nil
   124  	}
   125  	return s.Errorf("%v", errors.New("unknown command: "+f.Arg(0)))
   126  }
   127  
   128  func (s *Stencil) run(add, rm string) error {
   129  	if err := s.Objects.LoadObjects(); err != nil {
   130  		return s.Errorf("LoadObjects %v\n", err)
   131  	}
   132  	for pull := range s.Before.Pulls {
   133  		if pull != rm {
   134  			s.Objects.addPull(pull)
   135  		}
   136  	}
   137  	if add != "" {
   138  		s.Objects.addPull(add)
   139  	}
   140  	for pull := range s.Pulls {
   141  		s.Printf("Pulling %s\n", pull)
   142  		if err := s.Run(pull); err != nil {
   143  			return s.Errorf("Run: %v\n", err)
   144  		}
   145  	}
   146  	if err := s.GC(); err != nil {
   147  		return s.Errorf("GC %v\n", err)
   148  	}
   149  
   150  	return s.SaveObjects()
   151  }
   152  
   153  // CopyFile copies a url to a local file.
   154  func (s *Stencil) CopyFile(key, localPath, url string) error {
   155  	s.Printf("copying %s to %s, key (%s)\n", url, localPath, key)
   156  	s.Objects.addFile(key, localPath, url)
   157  
   158  	data, err := s.Execute(url)
   159  	if err != nil {
   160  		return s.Errorf("Error reading %s %v\n", url, err)
   161  	}
   162  	return s.Write(localPath, []byte(data), 0666)
   163  }
   164  
   165  // Run runs a template discarding the output.
   166  func (s *Stencil) Run(source string) error {
   167  	_, err := s.Import(source)
   168  	return err
   169  }
   170  
   171  // Import imports a template after applying it.
   172  func (s *Stencil) Import(source string) (string, error) {
   173  	s.Printf("Running import %s\n", source)
   174  	return s.Execute(source)
   175  }
   176  
   177  // Execute is like Run but it returns the output.
   178  func (s *Stencil) Execute(source string) (string, error) {
   179  	return s.executeFilter(source, nil)
   180  }
   181  
   182  func (s *Stencil) executeFilter(source string, filter func(string) (string, error)) (string, error) {
   183  	s.Printf("Executing %s\n", source)
   184  
   185  	data, err := s.Read(source)
   186  	if err != nil {
   187  		return "", s.Errorf("Error reading %s: %v\n", source, err)
   188  	}
   189  
   190  	if filter != nil {
   191  		str, err := filter(string(data))
   192  		if err != nil {
   193  			return "", s.Errorf("Error filtering %s: %v", source, err)
   194  		}
   195  		data = []byte(str)
   196  	}
   197  
   198  	t, err := template.New(source).Funcs(s.Funcs).Parse(string(data))
   199  	if err != nil {
   200  		return "", s.Errorf("Error parsing %s: %v\n", source, err)
   201  	}
   202  
   203  	var buf bytes.Buffer
   204  	err = t.Execute(&buf, s.State)
   205  	if err != nil {
   206  		return "", s.Errorf("Error executing %s: %v\n", source, err)
   207  	}
   208  
   209  	return buf.String(), nil
   210  }