6 Commits

Author SHA1 Message Date
6e63af80a6 better suffix check 2025-05-07 14:30:59 +02:00
8eebdc0d38 ignore ttf 2025-05-07 11:30:01 +02:00
454554f8e0 add typst export 2025-05-07 11:00:32 +02:00
c0fe9a4a46 fix heading problem 2025-05-02 13:21:27 +02:00
214bf9acf5 added markdown support 2025-05-02 13:18:47 +02:00
158e963c7b version2 (#1)
Co-authored-by: u80864958 <niklas.breitenstein@bit.admin.ch>
Reviewed-on: #1
2025-04-02 17:29:26 +02:00
10 changed files with 273 additions and 89 deletions

2
.vscode/launch.json vendored
View File

@ -13,7 +13,7 @@
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "${workspaceFolder}/cmd/main.go", "program": "${workspaceFolder}/cmd/main.go",
"args": [ ".."] "args": [ "."]
} }
] ]
} }

View File

@ -1,18 +1,20 @@
# PAT # PAT
## What it Does ## What it Does
`pat` is a command-line tool for concatenating and displaying the contents of files and directories. It: `pat` is a command-line tool for concatenating and displaying the contents of files and directories. It:
1. Processes the specified files and directories recursively. 1. Processes the specified files and directories recursively.
2. Appends content to a structured output with file paths and a delimiter for clarity. 2. Appends content to a structured output with file paths and a delimiter for clarity.
3. Copies the resulting output to the system clipboard for convenient sharing or reuse. 3. Copies the resulting output to the system clipboard for convenient sharing or reuse.
Example use case: Example use case:
- Aggregate and view the contents of multiple files or directories in one command. - Aggregate and view the contents of multiple files or directories in one command.
- Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms. - Automatically copy the aggregated result to the clipboard for seamless integration with other tools or platforms.
## Dependencies ## Dependencies
1. **Golang** (only to install / build): 1. **Golang** (only to install / build):
- The application requires the Go programming language (`>= 1.18`) to compile and run. - The application requires the Go programming language (`>= 1.18`) to compile and run.
- Dependency: `golang.design/x/clipboard` for clipboard interaction. - Dependency: `golang.design/x/clipboard` for clipboard interaction.
@ -21,18 +23,27 @@ Example use case:
## Installation ## Installation
Install go-cat directly using go install: Install pat:
``` sh ```sh
go install git.schreifuchs.ch/schreifuchs/pat@latest git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git
cd pat
go build -o=pat cmd/main.go
mv pat $GOPATH/bin/
``` ```
In one go:
```sh
git clone --depth 1 https://git.schreifuchs.ch/schreifuchs/pat.git && cd pat && go build -o=pat cmd/main.go && mv pat $GOPATH/bin/
```
The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly. The binary will be placed in your $GOPATH/bin directory. Ensure $GOPATH/bin is in your system's PATH to run it directly.
## Example Usage ## Example Usage
Concatenate files and directories: Concatenate files and directories:
```bash ```bash
./pat file1.txt folder/ ./pat file1.txt folder/
``` ```
@ -42,10 +53,6 @@ Output is printed to the terminal and copied to the clipboard, allowing you to p
--- ---
#### Notes #### Notes
- The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability. - The tool uses `---------------------------------------------------------------------------` as a delimiter to separate file contents for readability.
- If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal. - If clipboard functionality fails (e.g., unsupported environment), the application will still display the result in the terminal.

View File

@ -13,9 +13,11 @@ import (
const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n" const DELEMITTER = "-(%s)--------------------------------------------------------------------------\n"
func main() { func main() {
ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched") ignorePath := flag.String("i", "", "set path to gitignore, if no gitignore parent dirs will be searched")
hiddenFiles := flag.Bool("h", false, "show hidden files") hiddenFiles := flag.Bool("h", false, "show hidden files")
delemitter := flag.String("d", DELEMITTER, "delemitter to use to split files when not in markdown mode must contain %s for filename")
markdown := flag.Bool("m", false, "markdown mode, outputs files in markdown")
typst := flag.Bool("t", false, "typst mode, outputs files in typst")
flag.Parse() flag.Parse()
cats, err := cat.Path(flag.Args()...) cats, err := cat.Path(flag.Args()...)
@ -33,11 +35,19 @@ func main() {
cats = cats.Ignored(i) cats = cats.Ignored(i)
} }
if *hiddenFiles == false {
if !*hiddenFiles {
cats = cats.Ignored(ignore.Filesystem{}) cats = cats.Ignored(ignore.Filesystem{})
} }
out := cats.ToString(DELEMITTER) var out string
if *markdown {
out = cats.ToMarkdown()
} else if *typst {
out = cats.ToTypst()
} else {
out = cats.ToString(*delemitter)
}
fmt.Print(out) fmt.Print(out)
if err = clip.Copy(out); err != nil { if err = clip.Copy(out); err != nil {

View File

@ -6,10 +6,10 @@ import (
"strings" "strings"
) )
type Cater map[string]string type Cater []entry
func Path(paths ...string) (c Cater, err error) { func Path(paths ...string) (c Cater, err error) {
c = make(Cater) c = make(Cater, 0, 10)
var p os.FileInfo var p os.FileInfo
for _, path := range paths { for _, path := range paths {
@ -17,28 +17,29 @@ func Path(paths ...string) (c Cater, err error) {
if err != nil { if err != nil {
return return
} }
var e entry
if p.IsDir() { if p.IsDir() {
err = c.dir(path) e, err = c.dir(path)
} else { } else {
err = c.file(path) e, err = c.file(path)
} }
if err != nil { if err != nil {
return return
} }
c = append(c, e)
} }
return return
} }
func (c Cater) Ignored(ignore ignorer) Cater { func (c Cater) Ignored(ignore ignorer) Cater {
cat := make(Cater) cat := make(Cater, 0, len(c))
ok := func(e entry) bool {
return !ignore.Ignore(e.fqname)
}
for name, content := range c { for _, entry := range c {
if ignore.Ignore(name) { cat = append(cat, entry.filter(ok))
continue
}
cat[name] = content
} }
return cat return cat
@ -47,9 +48,20 @@ func (c Cater) Ignored(ignore ignorer) Cater {
func (c Cater) ToString(delemiter string) string { func (c Cater) ToString(delemiter string) string {
var sb strings.Builder var sb strings.Builder
for name, content := range c { var entries []entry
sb.WriteString(fmt.Sprintf(delemiter, name)) entries = c
sb.WriteString(content)
for len(entries) > 0 {
n := make([]entry, 0, len(entries))
for _, e := range entries {
if len(e.children) > 0 {
n = append(n, e.children...)
continue
}
sb.WriteString(fmt.Sprintf(delemiter, e.fqname))
sb.WriteString(e.content)
}
entries = n
} }
return sb.String() return sb.String()

32
pkg/cat/entry.go Normal file
View File

@ -0,0 +1,32 @@
package cat
type entry struct {
name string
fqname string
content string
children []entry
}
func (e entry) filter(ok func(e entry) bool) entry {
children := make([]entry, 0, len(e.children))
for _, entry := range e.children {
if !ok(entry) {
continue
}
children = append(children, entry.filter(ok))
}
return entry{
name: e.name,
fqname: e.fqname,
content: e.content,
children: children,
}
}
func (e entry) traverse(lvl int, do func(e entry, lvl int)) {
do(e, lvl)
for _, entry := range e.children {
entry.traverse(lvl+1, do)
}
}

View File

@ -1,58 +0,0 @@
package cat
import (
"bufio"
"io"
"os"
"path"
"strings"
)
func (c Cater) dir(dir string) error {
files, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, file := range files {
i, err := file.Info()
if err != nil {
continue
}
path := path.Join(dir, i.Name())
if !file.IsDir() {
c.file(path)
} else {
c.dir(path)
}
}
return nil
}
func (c Cater) file(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// read file into strings.Builder
var sb strings.Builder
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
sb.WriteString(line)
if err == io.EOF {
break
}
}
c[filePath] = sb.String()
return nil
}

39
pkg/cat/markdown.go Normal file
View File

@ -0,0 +1,39 @@
package cat
import (
"fmt"
"strings"
)
func (c Cater) ToMarkdown() string {
var sb strings.Builder
write := func(e entry, lvl int) {
for range lvl {
sb.WriteString("#")
}
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
if len(e.content) > 0 {
prts := strings.Split(e.name, ".")
sb.WriteString(
fmt.Sprintf(
"```%s\n%s\n```\n\n",
prts[len(prts)-1],
strings.ReplaceAll(
e.content,
"```",
"\\`\\`\\`",
),
),
)
}
}
for _, e := range c {
e.traverse(1, write)
}
return sb.String()
}

97
pkg/cat/read.go Normal file
View File

@ -0,0 +1,97 @@
package cat
import (
"bufio"
"io"
"os"
"path"
"strings"
)
var INVALID_SUFFIXES = []string{
"png",
"jpg",
"jpeg",
"webp",
"ico",
"ttf",
}
func (c Cater) dir(dir string) (e entry, err error) {
files, err := os.ReadDir(dir)
if err != nil {
return
}
e.fqname = dir
e.name = name(dir)
e.children = []entry{}
for _, file := range files {
i, err := file.Info()
if err != nil {
return e, err
}
path := path.Join(dir, i.Name())
var ent entry
if !file.IsDir() {
if !validSuffix(file.Name()) {
continue
}
ent, err = c.file(path)
} else {
ent, err = c.dir(path)
}
if err != nil {
return e, err
}
e.children = append(e.children, ent)
}
return
}
func (c Cater) file(filePath string) (e entry, err error) {
file, err := os.Open(filePath)
if err != nil {
return
}
defer file.Close()
// read file into strings.Builder
var sb strings.Builder
reader := bufio.NewReader(file)
for {
line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
return e, err
}
sb.WriteString(line)
if err == io.EOF {
break
}
}
e.fqname = filePath
e.name = name(filePath)
e.content = sb.String()
e.children = []entry{}
return
}
func name(name string) string {
ps := strings.Split(name, "/")
return ps[len(ps)-1]
}
func validSuffix(name string) bool {
for _, s := range INVALID_SUFFIXES {
if strings.HasSuffix(name, s) {
return false
}
}
return true
}

42
pkg/cat/typst.go Normal file
View File

@ -0,0 +1,42 @@
package cat
import (
"fmt"
"strings"
)
func (c Cater) ToTypst() string {
var sb strings.Builder
write := func(e entry, lvl int) {
for range lvl {
sb.WriteString("=")
}
sb.WriteString(fmt.Sprintf(" %s (`%s`)\n", e.name, e.fqname))
if len(e.content) > 0 {
prts := strings.Split(e.name, ".")
sb.WriteString(
fmt.Sprintf(
"```%s\n%s\n```\n\n",
prts[len(prts)-1],
strings.ReplaceAll(
e.content,
"```",
"\\`\\`\\`",
),
),
)
}
}
for _, e := range c {
sb.WriteString("= Export\n")
sb.WriteString("#outline()\n")
e.traverse(1, write)
}
return sb.String()
}

View File

@ -1,9 +1,12 @@
package ignore package ignore
import "strings" import (
"strings"
)
type Filesystem struct{} type Filesystem struct{}
func (f Filesystem) Ignore(name string) bool { func (f Filesystem) Ignore(name string) bool {
return strings.Contains(name, "/.") || strings.HasPrefix(name, ".") parts := strings.Split(name, "/")
return strings.HasPrefix(parts[len(parts)-1], ".")
} }