You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ccat/main.go

188 lines
5.5 KiB
Go

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++
}
}
}