package main import ( "bytes" "errors" "flag" "fmt" "math" "os" "os/user" "path/filepath" ) // fileExists returns true if the given file exists, and false if it // doesn't. If it encounters an error, it prints the error and exits. func fileExists(filename string) bool { if _, err := os.Stat(filename); err == nil { return true } else if errors.Is(err, os.ErrNotExist) { return false } else { printErrAndExit(err.Error()) return false // NEVER REACHED } } // mustExist can be called to ensure that a file exists; it errors and exits if // the file doesn't exist. func mustExist(filename string) { if fileExists(filename) != true { printErrAndExit(os.ErrNotExist.Error()) } } // getConfig fetches the config file name for the given file extension. // It returns two values: the first is true if the config file exists. // If it does, the second value is the config filename. // If it doesn't, the second value is blank and can be ignored. func getConfig(configPath, extension string) (bool, string) { if extension == "" { return false, "" } // Assuming the file has an extension fileName := filepath.Join(configPath, extension[1:]+".conf") if exists := fileExists(fileName); exists == false { return false, "" } else { return true, fileName } } // printFile is used when no config file can be found for the file extension // It prints out the file as it reads it, with no modifications applied. Essentially // works like 'cat'. func printFile(fileName string) { mustExist(fileName) data, err := os.ReadFile(fileName) if err != nil { printErrAndExit(err.Error()) } fmt.Print(string(data)) return } // computeLineNumDigits computes the number of digits in the number of lines // in the given byte array. func computeLineNumDigits(data []byte) int { numLines := bytes.Count(data, []byte{'\n'}) + 1 return int(math.Round(math.Log10(float64(numLines)))) } func main() { disableColorFlag := flag.Bool("d", false, "Disable color") lineNumberFlag := flag.Bool("n", false, "Print line numbers") // Used only if lineNumberFlag is true var lineNumDigits int var lineNum int flag.Parse() // Check if config exists. If it doesn't, generate the config files. var configPath string // Location of config files, depends on OS if runningOnWindows() { configPath = "%APPDATA%\\ccat" } else { currentUser, err := user.Current() if err != nil { panic(err) } configPath = filepath.Join("/home/" + currentUser.Username + "/.config/ccat/") } if _, err := os.Stat(configPath); os.IsNotExist(err) { generateDefaultConfigs() } // Check if user has provided a file name if len(flag.Args()) < 1 { printErrAndExit("No File specified") } fileName := flag.Args()[0] // Check if file exists. mustExist(fileName) extension := filepath.Ext(fileName) configExists, configFilename := getConfig(configPath, extension) // If the given file has no corresponding config, print the file out and exit. if configExists == false { printFile(fileName) return } // To save computing time, determine here if the file is empty. If it is, exit // the program. finfo, err := os.Stat(fileName) if err != nil { panic(err) } if finfo.Size() == 0 { os.Exit(0) } // Assuming the file is not empty... // If the given file has a config, load the config into a stack of regColors. regColorStack, err := loadConfig(configFilename) if err != nil { printErrAndExit(err.Error()) } // Load the input file into a colorunit slice (units) and a byte slice (data) units, data, err := loadInputFile(fileName) if err != nil { printErrAndExit(err.Error()) } // If the '-n' flag is set, compute the number of digits in the number of lines // in the file, to determine the padding for the line numbers. if *lineNumberFlag { lineNumDigits = computeLineNumDigits(data) } // For each regular expression in the stack, apply it to the byte slice. Find // the first and last index of all matches of the regex. Then apply the corresponding color // to every character within these indices. // // The infinite for loop exists, because I couldn't figure out a way to pop an element from // the stack inside the 'for' statement. The loop exits when the 'pop' call returns 'false', // indicating that the stack is empty. // // The loop is also only run if the 'disable color' flag is not set. for *disableColorFlag == false { regclr, ok := regColorStack.Pop() // regColorStack.Pop() returns false when there are no more elements to pop if ok != true { break } re := regclr.re clr := regclr.clr // Returns an int double-slice, where each slice contains the start and end indices // of the match. In this case, I am finding all the matches of 're' in 'data'. matches := re.FindAllSubmatchIndex(data, -1) if matches == nil { continue } // For each match, apply the corresponding color to all characters in the match. for _, match := range matches { units = applyColor(units, match[0], match[1], clr) } } // After all possible regexes have been matched, print out the contents of 'units'. // If the line number flag is set, initialize the lineNum variable and print the first line number // with the appropriate padding. if *lineNumberFlag { lineNum = 1 fmt.Printf(" %*d ", lineNumDigits, lineNum) lineNum++ } for idx, unit := range units { unit.print() // If the flag is set and we encounter a newline (and the newline isn't a trailing newline), // then print the next line number. if *lineNumberFlag && unit.ch == '\n' && idx != len(units)-1 { fmt.Printf(" %*d ", lineNumDigits, lineNum) lineNum++ } } }