dotfiles

regexghost dotfiles and scripts
git clone https://git.regexghost.com/dotfiles.git
Log | Files | Refs | README

sub.go (6974B)


      1 package main
      2 
      3 import  (
      4 	"fmt"
      5 	"os"
      6 	"regexp"
      7 	"bufio"
      8 	"strings"
      9 	"io"
     10 	"path/filepath"
     11 	"crypto/sha256"
     12 	"encoding/hex"
     13 	"path"
     14 )
     15 
     16 type Paths struct {
     17 	InputFilepath string
     18 	TrueName string
     19 	SubsDir string
     20 }
     21 
     22 type SubZone struct {
     23 	StartLine int
     24 	EndLine int
     25 	ZoneName string
     26 	Hash string
     27 	CurrentInstance SubInstance
     28 }
     29 
     30 type SubInstance struct {
     31 	Filename string
     32 	Hash string
     33 	SubName string
     34 	Content []string
     35 }
     36 
     37 // Helpers
     38 
     39 func panicIfErr(err error) {
     40 	if err != nil {
     41 		panic(err)
     42 	}
     43 }
     44 
     45 func hashLines(lines []string) string {
     46 	hash := sha256.New()
     47 	asString := joinLines(lines)
     48 	hash.Write([]byte(asString))
     49 	return hex.EncodeToString(hash.Sum(nil))
     50 }
     51 
     52 func hashFile(filepath string) string {
     53 	hash := sha256.New()
     54 			
     55 	file, err := os.Open(filepath)
     56 	panicIfErr(err)
     57 			
     58 	_, err = io.Copy(hash, file)
     59 	panicIfErr(err)
     60 			
     61 	return hex.EncodeToString(hash.Sum(nil))
     62 }
     63 
     64 // Get Zone and Sub names
     65 // Zone = general name of area, Sub = specific instance of that area
     66 
     67 func extractZoneName(line string) string {
     68 	r, _ := regexp.Compile("Start Substitute - ([^ ]*)")
     69 	return r.FindStringSubmatch(line)[1]
     70 }
     71 
     72 func extractZoneAndSubName(filename string) (string, string) {
     73 	r, _ := regexp.Compile("([^-]*)-Substitution-([^-]*)")
     74 	x := r.FindStringSubmatch(filename)
     75 	return x[1], x[2]
     76 }
     77 
     78 // File handling
     79 
     80 func readFileIntoLines(filename string) (lines []string) {
     81 	file, err := os.Open(filename)
     82 	panicIfErr(err)
     83 	scanner := bufio.NewScanner(file)
     84 	for scanner.Scan() {
     85 		lines = append(lines, scanner.Text())
     86 	}
     87 	return lines
     88 }
     89 
     90 func fileParse(inputFile string) []string {
     91 	lines := readFileIntoLines(inputFile)
     92 	return lines
     93 }
     94 
     95 func joinLines(lines []string) string {
     96 	return strings.Join(lines, "\n") + "\n"
     97 }
     98 
     99 func writeLinesToFile(lines []string, outputFile string) {
    100 	file, err := os.Create(outputFile)
    101 	panicIfErr(err)
    102 	defer file.Close()
    103 	_, err = file.Write([]byte(joinLines(lines)))
    104 	panicIfErr(err)
    105 }
    106 
    107 // Input new name
    108 
    109 func userInputName(prompt string) (name string) {
    110 	fmt.Print("Enter name for sub (" + prompt + "): ")
    111 	fmt.Scanln(&name)
    112 	return name
    113 }
    114 
    115 func nameAlreadyTaken(name string, otherInstances []SubInstance) bool {
    116 	for _, inst := range otherInstances {
    117 		if name == inst.SubName {
    118 			return true
    119 		}
    120 	}
    121 	return false
    122 }
    123 
    124 // Get relevant subs for a specific file, and zone
    125 func getRelevantInstances(paths Paths, zoneName string) (relevantInstances []SubInstance) {
    126 	entries, err := os.ReadDir(paths.SubsDir)
    127 	panicIfErr(err)
    128 	for _, e := range entries {
    129 		if strings.HasPrefix(e.Name(), zoneName + "-Substitution-") && strings.HasSuffix(e.Name(), paths.TrueName) {
    130 			_, name := extractZoneAndSubName(e.Name())
    131 			newInstance := SubInstance {
    132 				Filename: e.Name(),
    133 				Content: fileParse(filepath.Join(paths.SubsDir, e.Name())),
    134 				SubName: name,
    135 			}
    136 			newInstance.Hash = hashLines(newInstance.Content)
    137 
    138 			relevantInstances = append(relevantInstances, newInstance)
    139 		}
    140 	}
    141 
    142 	return relevantInstances
    143 }
    144 
    145 // Extract zones from file
    146 func extractZones(lines []string) ([]SubZone) {
    147 	var curZone SubZone
    148 	var zones []SubZone
    149 	for i, line := range lines {
    150 		if strings.Contains(line, "Start Substitute") {
    151 			curZone = SubZone{}
    152 			curZone.StartLine = i
    153 			curZone.ZoneName = extractZoneName(line)
    154 		}
    155 		if strings.Contains(line, "End Substitute") {
    156 			curZone.EndLine = i
    157 			instance := SubInstance {
    158 				Content: lines[curZone.StartLine:i+1],
    159 			}
    160 			instance.Hash = hashLines(instance.Content)
    161 			curZone.CurrentInstance = instance
    162 			zones = append(zones, curZone)
    163 		}
    164 	}
    165 
    166 	return zones
    167 }
    168 
    169 // Check for any new subsitutions and handle them
    170 func newSubs(paths Paths) {
    171 	lines := fileParse(paths.InputFilepath)
    172 	zones := extractZones(lines)
    173 	
    174 	for _, zone := range zones {
    175 		match := false
    176 		hash := zone.CurrentInstance.Hash
    177 		relevantInstances := getRelevantInstances(paths, zone.ZoneName)
    178 		// If there are existing instances for this zone, check for a match
    179 		for _, inst := range relevantInstances {
    180 			if inst.Hash == hash {
    181 				match = true
    182 				break
    183 			}
    184 		}
    185 		
    186 		// If we get a match don't save again
    187 		if !match {
    188 			newSub(paths, zone, relevantInstances)
    189 		}
    190 	}
    191 }
    192 
    193 // Get name for new substitution and save
    194 func newSub(paths Paths, zone SubZone, otherInstances []SubInstance) {
    195 	name := userInputName(zone.ZoneName)
    196 	for nameAlreadyTaken(name, otherInstances) {
    197 		name = userInputName(zone.ZoneName)
    198 	}
    199 	saveNewSub(paths, zone, name)
    200 }
    201 
    202 // Write a new sub to file
    203 func saveNewSub(paths Paths, zone SubZone, name string) {
    204 	filename := paths.SubsDir + "/" + zone.ZoneName + "-Substitution-" + name + "-" + paths.TrueName
    205 	writeLinesToFile(zone.CurrentInstance.Content, filename)
    206 }
    207 
    208 // Prompt the user to pick between substitutions
    209 func userPickSub(paths Paths, line string) []string {
    210 	zoneName := extractZoneName(line)
    211 	relevantInstances := getRelevantInstances(paths, zoneName)
    212 	for i, inst := range relevantInstances {
    213 		fmt.Printf("%d: %s\n", i+1, inst.SubName)
    214 	}
    215 	var choice int
    216 	fmt.Print("Pick sub to use: ")
    217 	fmt.Scanln(&choice)
    218 	chosenInst := relevantInstances[choice-1]
    219 	return readFileIntoLines(path.Join(paths.SubsDir, chosenInst.Filename))
    220 }
    221 
    222 // Assemble a final config file from base file and user chosen substitutions
    223 func makeFinalFile(paths Paths, outputFile string) {
    224 	lines := fileParse(paths.InputFilepath)
    225 	var finalLines []string
    226 	for i, line := range lines {
    227 		_ = i
    228 		if strings.Contains(line, "Start Substitute") {
    229 			newLines := userPickSub(paths, line)
    230 			finalLines = append(finalLines, newLines...)
    231 		} else {
    232 			finalLines = append(finalLines, line)
    233 		}
    234 	}
    235 
    236 	writeLinesToFile(finalLines, outputFile)
    237 }
    238 
    239 // Remove all substitutions from a file, leaving only the header
    240 func cleanFile(paths Paths, outputFile string) {
    241 	lines := fileParse(paths.InputFilepath)
    242 	zones := extractZones(lines)
    243 
    244 	var finalLines []string
    245 	previous := -1
    246 	for _, zone := range zones {
    247 		finalLines = append(finalLines, lines[previous+1:zone.StartLine+1]...)
    248 		previous = zone.EndLine
    249 	}
    250 	finalLines = append(finalLines, lines[previous+1:]...)
    251 
    252 	writeLinesToFile(finalLines, outputFile)
    253 }
    254 
    255 func printHelp() {
    256 	fmt.Println("Usage:")
    257 	fmt.Println("  sub save proper_filename /path/to/subs/ path/to/input_file")
    258 	fmt.Println("  sub make proper_filename /path/to/subs/ path/to/clean_file path/to/output_file")
    259 	fmt.Println("    e.g. sub make tmux.conf /path/to/subs/ path/to/tmux.conf final/tmux.conf")
    260 	fmt.Println("  sub clean proper_filename /path/to/subs path/to/input_file path/to/output_file")
    261 }
    262 
    263 func main() {
    264 	if len(os.Args) < 5 {
    265 		printHelp()
    266 		os.Exit(1)
    267 	}
    268 	command := os.Args[1]
    269 	paths := Paths{
    270 		InputFilepath: os.Args[4],
    271 		SubsDir: os.Args[3],
    272 		TrueName: os.Args[2],
    273 	}
    274 	
    275 	if command == "save" {
    276 		newSubs(paths)
    277 	} else if command == "clean" {
    278 		if len(os.Args) != 6 {
    279 			printHelp()
    280 			os.Exit(1)
    281 		}
    282 		outputFile := os.Args[5]
    283 		cleanFile(paths, outputFile)
    284 	} else if command == "make" {
    285 		if len(os.Args) != 6 {
    286 			printHelp()
    287 			os.Exit(1)
    288 		}
    289 		outputFile := os.Args[5]
    290 		makeFinalFile(paths, outputFile)
    291 	}
    292 }