commit 025ec775ca9aaea2c8d60c108c3463f36a4dd6e4 Author: Rockingcool Date: Fri Aug 9 19:20:42 2024 -0500 First commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a8baebc --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +build: fmt vet + go build +fmt: + go fmt +vet: + go vet diff --git a/ccat b/ccat new file mode 100755 index 0000000..259077d Binary files /dev/null and b/ccat differ diff --git a/color.go b/color.go new file mode 100644 index 0000000..3a80129 --- /dev/null +++ b/color.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + + colorData "github.com/fatih/color" +) + +// A color represents a possible color, which text can be printed out in. +// Each color has a name and an object (from fatih/color). This object is used +// to print text in that color. +type color struct { + name string + colorObj *colorData.Color +} + +// The following is a list of all possible colors, stored in a map. +var possibleColors map[string]color = map[string]color{ + "BLACK": {"BLACK", colorData.New(colorData.FgBlack)}, + "RED": {"RED", colorData.New(colorData.FgRed)}, + "GREEN": {"GREEN", colorData.New(colorData.FgGreen)}, + "YELLOW": {"YELLOW", colorData.New(colorData.FgYellow)}, + "BLUE": {"BLUE", colorData.New(colorData.FgBlue)}, + "MAGENTA": {"MAGENTA", colorData.New(38, 2, 254, 141, 255)}, + "CYAN": {"CYAN", colorData.New(colorData.FgCyan)}, + "WHITE": {"WHITE", colorData.New(colorData.FgWhite)}, + "GRAY": {"GRAY", colorData.New(colorData.FgWhite, colorData.Faint)}, + // Last three numbers are RGB. Reference https://en.wikipedia.org/wiki/ANSI_escape_code for what the first two numbers mean. + "ORANGE": {"ORANGE", colorData.New(38, 2, 255, 153, 28)}, + "NONE": {"NONE", colorData.New()}, +} + +// Apply the given color 'clr' to all units in 'units', within the indices +// marked by 'start' and 'end' +func applyColor(units []colorunit, start int, end int, clr color) []colorunit { + for i := start; i < end; i++ { + units[i].clr = clr + } + return units +} + +// newColor takes a string, and if it represents one of the colors in the dictionary, +// it returns the appropriate color. If it doesn't, the function returns an error. +func newColor(colorString string) (color, error) { + clr, ok := possibleColors[colorString] + if ok != true { + return color{}, fmt.Errorf("Invalid color: %s", colorString) + } + return clr, nil +} + +// newColorMust is similar to newColor, but panics if the given color isn't valid. +func newColorMust(colorString string) color { + if clr, err := newColor(colorString); err != nil { + panic(err) + } else { + return clr + } +} diff --git a/colorunit.go b/colorunit.go new file mode 100644 index 0000000..6ec5134 --- /dev/null +++ b/colorunit.go @@ -0,0 +1,36 @@ +package main + +import ( + "os" +) + +// A colorunit represents a unit in a file. It consists of the character, +// and the color that the character should be printed out in. +type colorunit struct { + ch byte + clr color +} + +// loadInputFile loads the given file and returns a slice of colorunits, +// and a slice of bytes (which just contains all the text in the file). +// The slice of colorunits is used to fill in the color for each character. +// The slice of bytes is used to perform the regex matching. +// The color will be set to the current terminal foreground color. +func loadInputFile(fileName string) ([]colorunit, []byte) { + data, err := os.ReadFile(fileName) + if err != nil { + panic(err) + } + units := make([]colorunit, len(data)) + for idx, c := range data { + units[idx] = colorunit{byte(c), newColorMust("NONE")} + } + return units, data +} + +// print is used to print out the character in the given colorunit, according to +// its color. +func (unit colorunit) print() { + unit.clr.colorObj.Printf("%c", unit.ch) + return +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..1e24aeb --- /dev/null +++ b/config.go @@ -0,0 +1,47 @@ +package main + +import ( + "ccat/stack" + "os" + "regexp" + "strings" + + "gopkg.in/yaml.v2" +) + +// loadConfig takes in the filename of a config file. It reads the file, +// and returns a stack of RegColors, with the item at the bottom being the one that +// was read first. This ensures that, _when accessing the RegColors in the stack, the last +// one (ie. the one that was read first) has highest precedence_. +// If there is an error compiling the regular expressions, the error is returned. +func loadConfig(configFilename string) (stack.Stack[regColor], error) { + configFile, err := os.ReadFile(configFilename) + if err != nil { + panic(err) + } + + // Here, I create a MapSlice. This is a slice of key-value pairs, and will + // store the results of unmarshalling the YAML file. + tempMapSlice := yaml.MapSlice{} + if err := yaml.Unmarshal(configFile, &tempMapSlice); err != nil { + panic(err) + } + + // Here, I create the stack which will eventually be returned. + // Each element of the MapSlice (created above) stores the key and value of a line + // in the file. + // Each regex string is compiled, and if there is an error, that error is + // returned. + regColorStack := stack.NewStack[regColor](len(strings.Split(string(configFile), "\n"))) // The stack will have the same size as the number of lines in the file + for _, item := range tempMapSlice { + re := regexp.MustCompile(item.Key.(string)) + clr, err := newColor(item.Value.(string)) + if err != nil { + panic(err) + } + // If we got past the panic, then the color _must_ be valid. + regColorStack.Push(regColor{re, clr}) + } + + return *regColorStack, nil +} diff --git a/config/c.conf b/config/c.conf new file mode 100644 index 0000000..81e6e12 --- /dev/null +++ b/config/c.conf @@ -0,0 +1,20 @@ +# Priority decreases going downward ie. If two regexes match the same piece of +# text, the one defined earlier will take precedence over the one defined later. +# Comments +'//.*': GRAY +'/\*[^*]*\*+(?:[^/*][^*]*\*+)*/': GRAY +# Constants +'\b[A-Z0-9_]*\b': MAGENTA +# Numbers +'\b\-?[0-9]*\b': MAGENTA +# Strings in double quotes and single quotes +'"(.*?)"': BLUE +"'(.)'": BLUE +# Assignments and comparisons +# TODO: Add less than, greater than, not equal to, and struct pointer member access +'(?:\s|\b)==?(\s|\b)' : CYAN +# Keywords +'\b(if|else|while|do|for|return)\b': CYAN +'^(#ifdef|#ifndef|#define|#include)\b': CYAN +# Data Types +'\b(int|char|float|double|void|long|short|unsigned|signed|bool)\b': YELLOW diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fdb2c9d --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module ccat + +go 1.22.5 + +require ( + github.com/fatih/color v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7aaf4bf --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..8b77e9c --- /dev/null +++ b/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +// fileExists returns true if the given file exists, and false if it +// doesn't. It panics if an error is encountered. +func fileExists(filename string) bool { + if _, err := os.Stat(filename); err == nil { + return true + } else if errors.Is(err, os.ErrNotExist) { + return false + } else { + panic(err) + } +} + +// mustExist can be called to ensure that a file exists; it panics if +// the file doesn't exist. +func mustExist(filename string) { + if fileExists(filename) != true { + panic(os.ErrNotExist) + } +} + +// 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(extension string) (bool, string) { + if extension == "" { + return false, "" + } + // Assuming the file has an extension + fileName := "config/" + 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 { + panic(err) + } + fmt.Print(string(data)) + return +} + +func main() { + + // Check if user has provided a file name + if len(os.Args) != 2 { + panic("ERROR: Invalid number of arguments") + } + fileName := os.Args[1] + mustExist(fileName) + + extension := filepath.Ext(fileName) + configExists, configFilename := getConfig(extension) + // If the given file has no corresponding config, print it out + // and exit. + if configExists == false { + printFile(fileName) + return + } + // If the given file has a config, load the config into a stack of regColors. + regColorStack, err := loadConfig(configFilename) + if err != nil { + panic(err) + } + + // Load the input file into a colorunit slice (units) and a byte slice (data) + units, data := loadInputFile(fileName) + + // 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. + for { + 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'. + for _, unit := range units { + unit.print() + } +} diff --git a/regcolor.go b/regcolor.go new file mode 100644 index 0000000..e938cdf --- /dev/null +++ b/regcolor.go @@ -0,0 +1,10 @@ +package main + +import "regexp" + +// A regColor is a regex-color pair. The config file is read +// into a stack of this data type. +type regColor struct { + re *regexp.Regexp + clr color +} diff --git a/stack/stack.go b/stack/stack.go new file mode 100644 index 0000000..c09a0c1 --- /dev/null +++ b/stack/stack.go @@ -0,0 +1,33 @@ +// Copied from https://gist.github.com/hedhyw/d52bfdc27befe56ffc59b948086fcd9e + +package stack + +type Stack[T any] struct { + elements []T +} + +func NewStack[T any](capacity int) *Stack[T] { + return &Stack[T]{ + elements: make([]T, 0, capacity), + } +} + +func (s *Stack[T]) Push(el T) { + s.elements = append(s.elements, el) +} + +func (s *Stack[T]) Len() int { + return len(s.elements) +} + +func (s *Stack[T]) Pop() (el T, ok bool) { + if len(s.elements) == 0 { + return el, false + } + + end := len(s.elements) - 1 + el = s.elements[end] + s.elements = s.elements[:end] + + return el, true +} diff --git a/test.c b/test.c new file mode 100644 index 0000000..66158a3 --- /dev/null +++ b/test.c @@ -0,0 +1,173 @@ +//go:build exclude +#include "easysock.h" + + +int create_socket(int network, char transport) { + int domain; + int type; + + if (network == 4) { + domain = AF_INET; + } else if (network == 6) { + domain = AF_INET6; + } else { + return -1; + } + + if (transport == 'T') { + type = SOCK_STREAM; + } else if (transport == 'U') { + type = SOCK_DGRAM; + } else { + return -1; + } + + int newSock = socket(domain,type,0); + return newSock; +} + + +int create_addr(int network, char* address, int port,struct sockaddr* dest) { + if (network == 4) { + struct sockaddr_in listen_address; + + listen_address.sin_family = AF_INET; + listen_address.sin_port = htons(port); + inet_pton(AF_INET,address,&listen_address.sin_addr); + memcpy(dest,&listen_address,sizeof(listen_address)); + return 0; + + } else if (network == 6) { + struct sockaddr_in6 listen_ipv6; + listen_ipv6.sin6_family = AF_INET6; + listen_ipv6.sin6_port = htons(port); + inet_pton(AF_INET6,address,&listen_ipv6.sin6_addr); + memcpy(dest,&listen_ipv6,sizeof(listen_ipv6)); + return 0; + + } else { + return -202; + } + + + +} + +int create_local (int network, char transport, char* address, int port,struct sockaddr* addr_struct) { + int socket = create_socket(network,transport); + if (socket < 0) { + return (-1 * errno); + } + create_addr(network,address,port,addr_struct); + int addrlen; + if (network == 4) { + addrlen = sizeof(struct sockaddr_in); + } else if (network == 6) { + addrlen = sizeof(struct sockaddr_in6); + } else { + return -202; + } + + /* The value of addrlen should be the size of the 'sockaddr'. + This should be set to the size of 'sockaddr_in' for IPv4, and 'sockaddr_in6' for IPv6. + See https://stackoverflow.com/questions/73707162/socket-bind-failed-with-invalid-argument-error-for-program-running-on-macos */ + + int i = bind (socket,addr_struct,(socklen_t)addrlen); + if (i < 0) { + return (-1 * errno); + } + return socket; +} + +int create_remote (int network,char transport,char* address,int port,struct sockaddr* remote_addr_struct) { + + struct addrinfo hints; /* Used to tell getaddrinfo what kind of address we want */ + struct addrinfo* results; /* Used by getaddrinfo to store the addresses */ + + + if (check_ip_ver(address) < 0) { /* If the address is a domain name */ + int err_code; + char* port_str = malloc(10 * sizeof(char)); + + sprintf(port_str,"%d",port); /* getaddrinfo expects a string for its port */ + + + memset(&hints,'\0',sizeof(hints)); + hints.ai_socktype = char_to_socktype(transport); + + err_code = getaddrinfo(address,port_str,&hints,&results); + if (err_code != 0) { + return (-1 * err_code); + } + remote_addr_struct = results->ai_addr; + network = inet_to_int(results->ai_family); + } else { + create_addr(network,address,port,remote_addr_struct); + } + + int socket = create_socket(network,transport); + if (socket < 0) { + return (-1 * errno); + } + + int addrlen; + if (network == 4) { + addrlen = sizeof(struct sockaddr_in); + } else if (network == 6) { + addrlen = sizeof(struct sockaddr_in6); + } else { + return (-202); + } + + /* The value of addrlen should be the size of the 'sockaddr'. + This should be set to the size of 'sockaddr_in' for IPv4, and 'sockaddr_in6' for IPv6. + See https://stackoverflow.com/questions/73707162/socket-bind-failed-with-invalid-argument-error-for-program-running-on-macos */ + + int i = connect(socket,remote_addr_struct,(socklen_t)addrlen); + if (i < 0) { + return (-1 * errno); + } + return socket; +} + + +int check_ip_ver(char* address) { + char buffer[16]; /* 16 chars - 128 bits - is enough to hold an ipv6 address */ + if (inet_pton(AF_INET,address,buffer) == 1) { + return 4; + } else if (inet_pton(AF_INET6,address,buffer) == 1) { + return 6; + } else { + return -1; + } +} + +int int_to_inet(int network) { + if (network == 4) { + return AF_INET; + } else if (network == 6) { + return AF_INET6; + } else { + return -202; + } +} + +int inet_to_int(int af_type) { + if (af_type == AF_INET) { + return 4; + } else if (af_type == AF_INET6) { + return 6; + } else { + return -207; + } +} + +int char_to_socktype(char transport) { + if (transport == 'T') { + return SOCK_STREAM; + } else if (transport == 'U') { + return SOCK_DGRAM; + } else { + return -250; + } +} diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..d23fb8c --- /dev/null +++ b/todo.txt @@ -0,0 +1,5 @@ +1. Man page +2. Logging +3. Coloring functions +4. Flag to list all available colors for your terminal - some terminals will only support the predefined colors. + a. Maybe, I could add a field to the 'color' type, which indicates if it is predefined, or defined by me. diff --git a/vision.txt b/vision.txt new file mode 100644 index 0000000..494ac81 --- /dev/null +++ b/vision.txt @@ -0,0 +1,6 @@ +1. Take in a filename as input +2. Find the file's extension +3. Look at the corresponding config file, and load the regex-color mappings +4. Load the file, with each character loaded as an object - character, color +5. Apply the regexes, one by one, to the file. if a regex matches a certain group of characters, those characters have the corresponding color. +6. Print the file, char-by-char.