50 Commits

Author SHA1 Message Date
720a01140c Updated some regexes for markdown coloring 2025-04-24 08:58:46 -04:00
9ced9ab5cb Added underline 'color' 2025-04-24 08:58:24 -04:00
8ae28cb359 Don't define any custom colors 2025-04-24 08:47:20 -04:00
d1a9f3f726 Updated coloring for Golang 2025-04-24 08:46:50 -04:00
a1309af696 Updated some regexes
One of the expressions used .* to mean 'any character except newline', but since I've
enabled 'RE_SINGLE_LINE' mode, a dot matches a newline as well.
2025-04-23 16:25:48 -04:00
06fab2292c Enable RE_SINGLE_LINE flag when compiling regex, to allow newline to be treated like any other character 2025-04-22 15:55:30 -04:00
19be04fd66 Enable multiline mode when compiling regex, because I read the whole file at once, rather than lline-by-line 2025-04-22 15:38:17 -04:00
40858a673a Use my regex engine instead of the stdlib one 2025-04-21 23:02:20 -04:00
23e9c5d58d Added SGR fields to the RGB struct for bold and italic;
allow RGB values to be -1 (default color);
allow underscore in color names
2025-04-21 11:48:56 -04:00
fd256bc7c7 Read the file from the embedded FS instead of the real FS 2025-04-21 11:48:24 -04:00
565205fb5e Added some extended colors along with the config files 2025-04-21 11:48:07 -04:00
33a3f7c937 Added a Markdown config file 2025-04-21 11:47:43 -04:00
9fce9cf2a1 Construct config path using user's home directory, instead of trying to guess what the home directory is 2025-01-27 09:22:25 -05:00
434c804b08 Added license 2025-01-11 13:11:52 -06:00
50d9126f2b Updated config file to recognize include headers 2024-09-02 23:18:56 -04:00
6b4d131f4f Updated README and TODO 2024-08-25 15:35:48 -04:00
51b4029a79 Changed project name to include repo path 2024-08-25 15:30:40 -04:00
a1f804ac38 Used absolute import path 2024-08-25 15:30:23 -04:00
eb32ec1027 Added link to releases 2024-08-15 12:55:32 -05:00
2625239dba Added note describing where to obtain releases 2024-08-15 12:53:24 -05:00
44de668546 Updated README 2024-08-15 12:46:01 -05:00
20cb665b33 Updated README 2024-08-15 12:45:43 -05:00
9a6fc3475a Added comment to function 2024-08-15 12:42:50 -05:00
d15a771e89 Removed references to Windows support
The program doesn't seem to work on Windows, and I don't have the time
right now to debug it. So, at the moment, the program isn't supported on Windows.
2024-08-15 12:37:01 -05:00
f8cb03bf88 Wrote script to create release builds 2024-08-15 12:28:12 -05:00
b65cef96c3 Replaced the relative path on Windows with an absolute path 2024-08-15 12:27:48 -05:00
b511c14cc3 Updated gitignore 2024-08-15 12:27:33 -05:00
122cd5ed04 Fixed typo 2024-08-15 11:38:00 -05:00
3b8bcb4c8a Updated README 2024-08-15 11:37:13 -05:00
e7e7a247d8 Updated README 2024-08-15 10:19:52 -05:00
eb2a0a9122 If the color config file exists, load colors from it 2024-08-15 10:19:43 -05:00
46e3e9da85 Added function to load colors from a config file 2024-08-15 10:19:21 -05:00
5bb51fb90c Updated README 2024-08-14 12:21:58 -05:00
5da734e06d Fixed typo 2024-08-14 12:21:21 -05:00
925ef4df4b Updated README 2024-08-14 12:20:30 -05:00
79cd6dab8d Cosmetic changes 2024-08-14 12:16:41 -05:00
28ee686295 Fixed typo 2024-08-14 12:15:57 -05:00
5e0bbbec4f Added README 2024-08-14 12:15:19 -05:00
05f3ebc178 Updated TODO 2024-08-14 10:05:11 -05:00
a34f2309a8 Removed C file which was used for testing 2024-08-14 10:03:33 -05:00
66b227fb63 Defined color mappings for Go 2024-08-14 09:42:07 -05:00
373c3c3385 Added Dark Blue as a color 2024-08-14 09:41:55 -05:00
624ea575cc Changed behavior of program when 'disable color' flag is passed.
Instead of printing out the contents early on (which wouldn't allow it to work
with '-n'), I skip the loop that applies colors based on the regular expressions.
2024-08-13 00:55:55 -05:00
ffae02b62b Fixed bug where line number wasn't updated 2024-08-13 00:51:53 -05:00
81b976bcc7 Added 'line number' flag, and added check for empty file.
I added a 'line number' flag ('-n') which will print the line number
with padding before every line. To ensure that a line number isn't printed when
trying to 'ccat' an empty file, I added a check to see if the file is empty.
If it is, the program exits.
2024-08-13 00:49:02 -05:00
0b67cb317e Added 'disable color' flag ('-d') to disable color printing. Also replaced os.Args with flag.Args, to retrieve command-line arguments after stripping out flags 2024-08-13 00:21:26 -05:00
098f7fe01b Instead of using command-line flags, I decided to generate the configs if they don't exist 2024-08-13 00:07:46 -05:00
76fc61a19d Wrote function to copy embedded config files to a config directory 2024-08-13 00:01:00 -05:00
94d1d99af2 Added a flag called 'generate-config' which will generate default config files. 2024-08-12 11:27:07 -05:00
7feac0b5f7 Renamed 'printAndExit' to 'printErrAndExit' 2024-08-10 11:29:18 -05:00
16 changed files with 470 additions and 207 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
ccat
*.zip

8
LICENSE.md Normal file
View File

@@ -0,0 +1,8 @@
Copyright 2025 Aadhavan Srinivasan
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
## ccat
ccat is a file printing tool (like 'cat') which uses Regular Expressions to enable syntax highlighting.
---
### Features
- 11 colors are defined out-of-the-box: RED, BLUE, GREEN, MAGENTA, CYAN, BLACK, WHITE, YELLOW, GRAY, ORANGE and DARKBLUE.
- Support for defining custom colors via the `ccat.colors` file.
- Regex-color mappings are stored in configuration files.
- Uses the file extension to determine which configuration file to use.
- Highly extensible - to add a config file for an specific file type, name the file `<extension>.conf`.
- Support for printing line numbers with the `-n` flag.
- Statically linked Go binary - no runtime dependencies, config files are distributed along with the binary.
- Linux and MacOS supported.
---
### Installing
Download the appropriate zip-file from the 'Releases' section. Place the executable in your PATH.
NOTE: The releases are not available on the GitHub repo (which is a mirror of https://gitea.twomorecents.org/Rockingcool/ccat). Obtain the [releases](https://gitea.twomorecents.org/Rockingcool/ccat/releases) from there instead.
---
### Building from source
If you have the `go` command installed, run `make` after cloning the repository.
---
### Supported Languages
The following languages have config files included by default:
- C
- Go
---
### Getting Started
The config files are embedded within the binary. They will automatically be installed to the correct location (`~/.config/ccat` on UNIX) when the program is first run.
As written above, if provided a file with extension `.example`, the program will look for the config file named `example.conf`. If such a file doesn't exist, the file is printed out without any highlighting.
For example, if you want to create syntax highlighting for Java, create a file named `java.conf` in your config directory. In this file, include regular-expressions for each of the langauges's keywords, and provide a corresponding color. Use the provided `c.conf` and `go.conf` files as a starting point.
---
### Config Files
The config files are written in YAML. Each line has the following syntax:
`"<regex>": COLOR`
Note that the regex must be enclosed in double quotes, and the color must be capitalized.
---
### Custom Colors
To define a color of your own, create a file named `ccat.colors` in the config directory (mentioned above). The syntax of this file is the following:
`COLOR: <red> <green> <blue>`
Note that the color name must be capitalized (and shouldn't contain spaces). The RGB values must each be from 0 to 255.
---
### TODO:
- Windows support.
- Allow users to provide a config file in the command-line, overriding the extension-based config file.

152
color.go
View File

@@ -2,8 +2,12 @@ package main
import (
"fmt"
"os"
"strconv"
"strings"
colorData "github.com/fatih/color"
"gopkg.in/yaml.v2"
)
// A color represents a possible color, which text can be printed out in.
@@ -14,6 +18,19 @@ type color struct {
colorObj *colorData.Color
}
// A RGB represents a Red, Blue, Green trio of values, along with SGR parameters.
// Each value is represented as an int. For info on SGR parameters, see:
// https://en.wikipedia.org/wiki/ANSI_escape_code#Select_Graphic_Rendition_parameters
// If 'red', 'green' and 'blue' are all -1, then the default terminal color is used.
// If some (but not all) of them are -1, an error is thrown.
type RGB struct {
sgr1 int
red int
blue int
green int
sgr2 int
}
// 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)},
@@ -21,13 +38,14 @@ var possibleColors map[string]color = map[string]color{
"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)},
"MAGENTA": {"MAGENTA", colorData.New(colorData.FgMagenta)},
"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()},
// "ORANGE": {"ORANGE", colorData.New(38, 2, 255, 153, 28)},
// "DARKBLUE": {"DARKBLUE", colorData.New(38, 2, 0, 112, 255)},
"NONE": {"NONE", colorData.New()},
}
// Apply the given color 'clr' to all units in 'units', within the indices
@@ -52,9 +70,135 @@ func newColor(colorString string) (color, error) {
// newColorMust is similar to newColor, but prints an error and exits if the given color isn't valid.
func newColorMust(colorString string) color {
if clr, err := newColor(colorString); err != nil {
printAndExit(err.Error())
printErrAndExit(err.Error())
panic(err) // NEVER REACHED
} else {
return clr
}
}
// isValidColorName returns true if the given string only contains uppercase alphabetic
// characters.
func isValidColorName(colorName string) bool {
for _, ch := range colorName {
if (ch > 'Z' || ch < 'A') && (ch != '_') {
return false
}
}
return true
}
// stringToRGB takes a string representing an RGB five-tuple. It constructs and RGB type and
// returns it. Any errors encountered are returned. If an error is returned, it is safe to
// assume that the string doesn't represent an RGB five-tuple.
func stringToRGB(rgbString string) (*RGB, error) {
values := strings.Split(rgbString, " ")
// There must be three space-separated strings.
if len(values) != 5 {
// TODO: Instead of ignoring these errors and returning a generic error (as I do in the
// callee), wrap the error returned from this function, inside the error returned by the callee.
return nil, fmt.Errorf("Error parsing RGB five-tuple.")
}
// If any of the strings doesn't represent an integer (or is out of bounds), return an error.
// WARNING: LAZY CODE INCOMING
var toReturn RGB
var err error
toReturn.sgr1, err = strconv.Atoi(values[0])
if err != nil {
return nil, fmt.Errorf("Error parsing SGR1 integer: Invalid value.")
}
if toReturn.sgr1 < 0 || toReturn.sgr1 > 107 { // Maximum value for SGR values
return nil, fmt.Errorf("Error parsing SGR1 integer: Out-of-bounds.")
}
toReturn.red, err = strconv.Atoi(values[1])
if err != nil {
return nil, fmt.Errorf("Error parsing RED integer: Invalid value.")
}
if toReturn.red < -1 || toReturn.red > 255 {
return nil, fmt.Errorf("Error parsing RED integer: Out-of-bounds.")
}
toReturn.blue, err = strconv.Atoi(values[2])
if err != nil {
return nil, fmt.Errorf("Error parsing BLUE integer: Invalid value.")
}
if toReturn.blue < -1 || toReturn.blue > 255 {
return nil, fmt.Errorf("Error parsing BLUE integer: Out-of-bounds.")
}
toReturn.green, err = strconv.Atoi(values[3])
if err != nil {
return nil, fmt.Errorf("Error parsing GREEN integer: Invalid value.")
}
if toReturn.green < -1 || toReturn.green > 255 {
return nil, fmt.Errorf("Error parsing GREEN integer: Out-of-bounds.")
}
toReturn.sgr2, err = strconv.Atoi(values[4])
if err != nil {
return nil, fmt.Errorf("Error parsing SGR2 integer: Invalid value.")
}
if toReturn.sgr2 < 0 || toReturn.sgr2 > 107 {
return nil, fmt.Errorf("Error parsing SGR2 integer: Out-of-bounds.")
}
if !(toReturn.red > 0 && toReturn.blue > 0 && toReturn.green > 0) &&
!(toReturn.red == -1 && toReturn.green == -1 && toReturn.blue == -1) {
return nil, fmt.Errorf("Error parsing color: All values must be positive or -1 for default terminal color.")
}
return &toReturn, nil
}
// loadColorsFromFile loads the colors defined in the given config file, and adds them to
// the possibleColors map. This allows the user to define custom colors at run-time.
// The colors config file has the following syntax:
// COLOR: <SGR1> <RED> <GREEN> <BLUE> <SGR2>
//
// Note that the color must be capitalized (and not contain spaces), and the R, G and B
// values must be from -1 to 255 (-1 refers to the default terminal color, and all three values
// must be -1 for this to work).
func loadColorsFromFile(filepath string) error {
data, err := os.ReadFile(filepath)
if err != nil {
panic(err)
}
// Read color config file into a MapSlice
tempMapSlice := yaml.MapSlice{}
if err := yaml.Unmarshal(data, &tempMapSlice); err != nil {
return fmt.Errorf("Unable to read color config file: %s", filepath)
}
for _, item := range tempMapSlice {
if !(isValidColorName(item.Key.(string))) {
return fmt.Errorf("Invalid color name: %s", item.Key.(string))
}
var rgb *RGB
if rgb, err = stringToRGB(item.Value.(string)); err != nil {
return fmt.Errorf("Invalid RGB trio: %s", item.Value.(string))
}
// If we haven't returned an error yet, the color must be valid.
// Add it to the map. colorData.New() expects values of type colorData.Attribute,
// so we must cast our RGB values accordingly.
// First, check if one of the color values is -1. If it is, they must all be negative (based
// on the check in 'stringToRGB()'). If this is the case, don't put the color values.
if rgb.red == -1 {
possibleColors[item.Key.(string)] = color{
item.Key.(string),
colorData.New(
colorData.Attribute(rgb.sgr2),
),
}
} else {
possibleColors[item.Key.(string)] = color{
item.Key.(string),
colorData.New(
colorData.Attribute(rgb.sgr1),
2,
colorData.Attribute(rgb.red),
colorData.Attribute(rgb.blue),
colorData.Attribute(rgb.green),
colorData.Attribute(rgb.sgr2),
),
}
}
}
return nil
}

View File

@@ -1,14 +1,68 @@
package main
import (
"ccat/stack"
"embed"
"errors"
"io/fs"
"os"
"regexp"
"path/filepath"
"runtime"
"strings"
"gitea.twomorecents.org/Rockingcool/ccat/stack"
"gitea.twomorecents.org/Rockingcool/kleingrep/regex"
"gopkg.in/yaml.v2"
)
//go:embed config
var storedConfigs embed.FS // Embed the folder containing config files
// runningOnWindows: At the moment this function isn't used. When Window support is added,
// it will be used to determine if the program is being run on Windows.
func runningOnWindows() bool {
return runtime.GOOS == "windows"
}
// generateDefaultConfigs is used to generate a folder of default config files
// for common languages. These default config files are embedded into the program, and will
// be outputted into the given directory.
//
// If there is an error encountered, the error is returned.
func generateDefaultConfigs(configOutputPath string) error {
err := os.MkdirAll(configOutputPath, 0755)
if err != nil {
if os.IsExist(err) {
return errors.New("Directory already exists.")
} else {
return errors.New("Unable to create directory.")
}
}
// Copy each folder from the embedded filesystem, into the destination path
err = fs.WalkDir(storedConfigs, "config", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() { // Skip directories
return nil
}
relPath, _ := filepath.Rel("config", path)
dstPath := filepath.Join(configOutputPath, relPath) // Destination path
data, err := storedConfigs.ReadFile(path)
if err != nil {
return err
}
if err := os.WriteFile(dstPath, data, 0644); err != nil {
return err
}
return nil
})
return nil
}
// 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
@@ -34,13 +88,13 @@ func loadConfig(configFilename string) (stack.Stack[regColor], error) {
// 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))
re := regex.MustCompile(item.Key.(string), regex.RE_MULTILINE, regex.RE_SINGLE_LINE)
clr, err := newColor(item.Value.(string))
if err != nil {
return *stack.NewStack[regColor](0), err
}
// If we got past the errors, then the color _must_ be valid.
regColorStack.Push(regColor{re, clr})
regColorStack.Push(regColor{&re, clr})
}
return *regColorStack, nil

View File

@@ -10,11 +10,17 @@
# Strings in double quotes and single quotes
'"(.*?)"': BLUE
"'(.)'": BLUE
# Text inside angle-brackets (used in 'include' statements)
'\<(.*?)\>': BLUE
# Assignments and comparisons
# TODO: Add less than, greater than, not equal to, and struct pointer member access
'(?:\s|\b)(=|==|!=|<=|>=|\->)(\s|\b)' : CYAN
'(?:\s|\b)(=|==|!=|\<=|\>=|\->)(\s|\b)' : CYAN
# Built-in boolean values, modifiers
'\b(static|const|true|false)\b': DARKBLUE
# Keywords
'\b(if|else|while|do|for|return)\b': CYAN
'^(#ifdef|#ifndef|#define|#include)\b': CYAN
'(\n|^)(#ifdef|#ifndef|#define|#include)\b': CYAN
# Data Types
'\b(int|char|float|double|void|long|short|unsigned|signed|bool)\b': YELLOW

8
config/ccat.colors Normal file
View File

@@ -0,0 +1,8 @@
# The first and last fields are SGR values, used to control things
# like bold and italic text. If you're unsure what to put, use '38'
# for the first one and '22' for the last one, for normal text.
# The last three values are R G B values.
PINK: 38 244 211 244 22
BOLD_WHITE: 38 -1 -1 -1 1
ITALIC_WHITE: 38 -1 -1 -1 3
UNDERLINE_WHITE: 38 -1 -1 -1 4

28
config/go.conf Normal file
View File

@@ -0,0 +1,28 @@
# 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
'//[^\n]*': GRAY
'/\*[^*]*\*+(?:[^/*][^*]*\*+)*/': GRAY
# Numbers and special values
'\b\-?[0-9]*\b': MAGENTA
'\b(true|false)\b': MAGENTA
'\b(nil)\b': MAGENTA
# Strings in double quotes and backticks
'"(.*?)"': BLUE
'`(.*?)`': BLUE
# Bytes / Runes
"'(.)'": BLUE
"'\\\\(.)'": BLUE # The escape backslash needs to be escaped as well
# Assignments and comparisons
'(?:\s|\b)(=|==|!=|\<=|\>=)(\s|\b)' : CYAN
'(&&)|(\|\|)': CYAN
# Keywords
'\b(if|else|for|range|go|func|return|break|continue)\b': CYAN
'\b(package|import|var|const|type|struct)\b': CYAN
# Built-in Functions
'\b(panic|len)\b': GREEN
# Data Types
'\b(bool|byte|rune|string|interface|map|chan)\b': YELLOW
'\b(u?int)(8|16|32|64)?\b': YELLOW
'\b(float)(32|64)\b': YELLOW
'\b(complex)(64|128)\b': YELLOW

19
config/md.conf Normal file
View File

@@ -0,0 +1,19 @@
# 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.
# Headings
'^#{1,6}.*?$': MAGENTA
# Link text
'\[.*?\](?=\(.*?\))': UNDERLINE_WHITE
# Link URL
'https?://\w+\.\w+.*?(?=\))': RED
# Code blocks
'```(.|\n)+?```': YELLOW
# Bold text
'\b__[^_]+?__\b': BOLD_WHITE
# Italic text
'\b_[^_]+?_\b': ITALIC_WHITE

17
create_release_builds.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
set -euo pipefail
POSSIBLE_GOOS=( "linux" "darwin" )
POSSIBLE_GOARCH=( "amd64" "arm64" )
for OS in "${POSSIBLE_GOOS[@]}"; do
for ARCH in "${POSSIBLE_GOARCH[@]}"; do
FOLDER_NAME="ccat-$OS-$ARCH"
mkdir "${FOLDER_NAME}"
GOOS=$OS GOARCH=$ARCH go build -o "${FOLDER_NAME}/"
zip -r "${FOLDER_NAME}" "${FOLDER_NAME}"
rm -r "${FOLDER_NAME}"
done
done

View File

@@ -5,7 +5,7 @@ import (
"os"
)
func printAndExit(errorStr string) {
func printErrAndExit(errorStr string) {
fmt.Printf("ERROR: %s\n", errorStr)
os.Exit(1)
}

11
go.mod
View File

@@ -1,11 +1,14 @@
module ccat
module gitea.twomorecents.org/Rockingcool/ccat
go 1.22.5
go 1.23.1
toolchain go1.24.2
require (
github.com/fatih/color v1.17.0 // indirect
gitea.twomorecents.org/Rockingcool/kleingrep v0.7.0 // indirect
github.com/fatih/color v1.18.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
golang.org/x/sys v0.25.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

8
go.sum
View File

@@ -1,7 +1,13 @@
gitea.twomorecents.org/Rockingcool/kleingrep v0.6.1 h1:eeryIhh2lDMXsu3D1i95IgW2SPte1DaJxlfmiQjrpsE=
gitea.twomorecents.org/Rockingcool/kleingrep v0.6.1/go.mod h1:8bcYe2hyjNIDM9J2xnyH5veMCAMzVJQR3c0OkatcEPg=
gitea.twomorecents.org/Rockingcool/kleingrep v0.7.0 h1:owDJjgulFmg9DmgKBtwZMxdf19wM9VbGchMXq7ZlhIM=
gitea.twomorecents.org/Rockingcool/kleingrep v0.7.0/go.mod h1:8bcYe2hyjNIDM9J2xnyH5veMCAMzVJQR3c0OkatcEPg=
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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
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=
@@ -13,6 +19,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
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=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.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=

101
main.go
View File

@@ -1,8 +1,11 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"math"
"os"
"path/filepath"
)
@@ -15,7 +18,7 @@ func fileExists(filename string) bool {
} else if errors.Is(err, os.ErrNotExist) {
return false
} else {
printAndExit(err.Error())
printErrAndExit(err.Error())
return false // NEVER REACHED
}
}
@@ -24,7 +27,7 @@ func fileExists(filename string) bool {
// the file doesn't exist.
func mustExist(filename string) {
if fileExists(filename) != true {
printAndExit(os.ErrNotExist.Error())
printErrAndExit(os.ErrNotExist.Error())
}
}
@@ -32,12 +35,12 @@ func mustExist(filename string) {
// 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) {
func getConfig(configPath, extension string) (bool, string) {
if extension == "" {
return false, ""
}
// Assuming the file has an extension
fileName := "config/" + extension[1:] + ".conf"
fileName := filepath.Join(configPath, extension[1:]+".conf")
if exists := fileExists(fileName); exists == false {
return false, ""
} else {
@@ -52,41 +55,89 @@ func printFile(fileName string) {
mustExist(fileName)
data, err := os.ReadFile(fileName)
if err != nil {
printAndExit(err.Error())
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.
userHomeDir, err := os.UserHomeDir() // Get current user's home directory, to construct config path
if err != nil {
panic(err)
}
configPath := filepath.Join(userHomeDir + "/.config/ccat/")
if _, err := os.Stat(configPath); os.IsNotExist(err) {
generateDefaultConfigs(configPath)
}
// Check if user has provided a file name
if len(os.Args) != 2 {
printAndExit("Invalid number of arguments")
if len(flag.Args()) < 1 {
printErrAndExit("No File specified")
}
fileName := os.Args[1]
fileName := flag.Args()[0]
// Check if file exists.
mustExist(fileName)
extension := filepath.Ext(fileName)
configExists, configFilename := getConfig(extension)
// If the given file has no corresponding config, print it out
// and exit.
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 a ccat.colors file exists in the config directory, load all the colors in it
if fileExists(filepath.Join(configPath, "ccat.colors")) {
err := loadColorsFromFile(filepath.Join(configPath, "ccat.colors"))
if err != nil {
printErrAndExit(err.Error())
}
}
// If the given file has a config, load the config into a stack of regColors.
regColorStack, err := loadConfig(configFilename)
if err != nil {
printAndExit(err.Error())
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 {
printAndExit(err.Error())
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.
@@ -94,7 +145,9 @@ func main() {
// 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 {
//
// 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 {
@@ -104,18 +157,32 @@ func main() {
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)
matches := re.FindAllSubmatch(string(data))
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)
units = applyColor(units, match[0].StartIdx, match[0].EndIdx, clr)
}
}
// After all possible regexes have been matched, print out the contents of 'units'.
for _, unit := range 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++
}
}
}

View File

@@ -1,10 +1,10 @@
package main
import "regexp"
import "gitea.twomorecents.org/Rockingcool/kleingrep/regex"
// 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
re *regex.Reg
clr color
}

173
test.c
View File

@@ -1,173 +0,0 @@
//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;
}
}