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 }