chore: cleanup — untrack binaries, consolidate Go dirs, dedupe tools
- Untrack and delete compiled binaries (tarsum, gosoft.exe, rust uniq/uniq2);
ignore build outputs (dist/, bin/, *.exe, *.test, .ruff_cache/)
- Merge tools/go/ and projects/go-tools/go/ into projects/go-tools/<name>/
- Fix goipgrep .gitignore: bare 'ipgrep' pattern was ignoring cmd/ipgrep/,
so the main entrypoint was never tracked; now anchored to /ipgrep
- Archive duplicate implementations to archive/experimental/{rust,go}/
(uniq, between, tarsum rewrites); canonical versions stay in tools/
- Update README tool catalog to match new layout
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
401b3e1781
commit
1cbf8afb4a
39 files changed
+449
-27
No files matched your search
@@ -0,0 +1,5 @@
|
||||
module gobincmp
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require github.com/glaslos/ssdeep v0.4.0
|
||||
@@ -0,0 +1,10 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/glaslos/ssdeep v0.4.0 h1:w9PtY1HpXbWLYgrL/rvAVkj2ZAMOtDxoGKcBHcUFCLs=
|
||||
github.com/glaslos/ssdeep v0.4.0/go.mod h1:il4NniltMO8eBtU7dqoN+HVJ02gXxbpbUfkcyUvNtG0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,216 @@
|
||||
// main.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/glaslos/ssdeep"
|
||||
)
|
||||
|
||||
const blockSize = 1024 * 1024
|
||||
|
||||
// main is the entry point of the program.
|
||||
func main() {
|
||||
// 1. Get and validate command-line arguments
|
||||
if len(os.Args) != 3 {
|
||||
usage := `
|
||||
Binwally (Go Version): Binary and Directory tree comparison tool
|
||||
using the Fuzzy Hashing concept (ssdeep)
|
||||
|
||||
Usage: go run . <dir1/file1> <dir2/file2>
|
||||
`
|
||||
fmt.Printf(usage)
|
||||
os.Exit(1)
|
||||
}
|
||||
path1 := os.Args[1]
|
||||
path2 := os.Args[2]
|
||||
|
||||
info1, err1 := os.Stat(path1)
|
||||
if err1 != nil {
|
||||
log.Fatalf("Invalid path: %s", path1)
|
||||
}
|
||||
info2, err2 := os.Stat(path2)
|
||||
if err2 != nil {
|
||||
log.Fatalf("Invalid path: %s", path2)
|
||||
}
|
||||
|
||||
// 2. Route to appropriate comparison logic
|
||||
if info1.IsDir() && info2.IsDir() {
|
||||
// Both are directories
|
||||
var diffs []int
|
||||
fmt.Println("\nSCORE RESULT PATH")
|
||||
compareTrees(path1, path2, &diffs)
|
||||
|
||||
if len(diffs) == 0 {
|
||||
fmt.Println("No diffs found\n")
|
||||
} else {
|
||||
totalScore := 0
|
||||
for _, score := range diffs {
|
||||
totalScore += score
|
||||
}
|
||||
averageScore := totalScore / len(diffs)
|
||||
fmt.Println("\nTotal files compared:", len(diffs))
|
||||
fmt.Printf("Overall match score: %d%%\n\n", averageScore)
|
||||
}
|
||||
} else if !info1.IsDir() && !info2.IsDir() {
|
||||
// Both are files
|
||||
hash1, err := ssdeep.HashFromFile(path1) // CORRECTED
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to hash file %s: %v", path1, err)
|
||||
}
|
||||
hash2, err := ssdeep.HashFromFile(path2) // CORRECTED
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to hash file %s: %v", path2, err)
|
||||
}
|
||||
score, err := ssdeep.Distance(hash1, hash2) // CORRECTED
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to compare hashes: %v", err)
|
||||
}
|
||||
fmt.Printf("Overall match score: %d%%\n\n", score)
|
||||
} else {
|
||||
log.Fatal("Error: Both arguments must be files or both must be directories.")
|
||||
}
|
||||
}
|
||||
|
||||
// compareTrees recursively compares two directory trees.
|
||||
func compareTrees(dir1, dir2 string, diffs *[]int) {
|
||||
// 1. Read contents of both directories
|
||||
entries1, err := os.ReadDir(dir1)
|
||||
if err != nil {
|
||||
log.Printf("Error reading directory %s: %v", dir1, err)
|
||||
return
|
||||
}
|
||||
entries2, err := os.ReadDir(dir2)
|
||||
if err != nil {
|
||||
log.Printf("Error reading directory %s: %v", dir2, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create maps for quick lookup by name
|
||||
map1 := make(map[string]os.DirEntry)
|
||||
for _, e := range entries1 {
|
||||
map1[e.Name()] = e
|
||||
}
|
||||
map2 := make(map[string]os.DirEntry)
|
||||
for _, e := range entries2 {
|
||||
map2[e.Name()] = e
|
||||
}
|
||||
|
||||
// 2. Find and report unique files/dirs
|
||||
reportUniques(map1, map2, dir1, " <<< unique ", diffs) // In dir1 only
|
||||
reportUniques(map2, map1, dir2, " >>> unique ", diffs) // In dir2 only
|
||||
|
||||
// 3. Process common items
|
||||
for name, e1 := range map1 {
|
||||
if e2, ok := map2[name]; ok {
|
||||
// Name exists in both directories
|
||||
path1 := filepath.Join(dir1, name)
|
||||
path2 := filepath.Join(dir2, name)
|
||||
|
||||
isDir1 := e1.IsDir()
|
||||
isDir2 := e2.IsDir()
|
||||
|
||||
if isDir1 && isDir2 {
|
||||
// Both are directories, recurse
|
||||
compareTrees(path1, path2, diffs)
|
||||
} else if !isDir1 && !isDir2 {
|
||||
// Both are files, compare them
|
||||
compareFiles(path1, path2, diffs)
|
||||
} else {
|
||||
// Mismatched types (file vs dir) or symlinks
|
||||
fmt.Printf(" - ignored %s (symlink or type mismatch)\n", path1)
|
||||
*diffs = append(*diffs, 100) // Original script counts this as 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reportUniques finds items in map1 not in map2 and reports them.
|
||||
func reportUniques(map1, map2 map[string]os.DirEntry, baseDir, prefix string, diffs *[]int) {
|
||||
for name, entry := range map1 {
|
||||
if _, exists := map2[name]; !exists {
|
||||
fullPath := filepath.Join(baseDir, name)
|
||||
if !entry.IsDir() {
|
||||
// It's a unique file
|
||||
fmt.Println(prefix, fullPath)
|
||||
*diffs = append(*diffs, 0)
|
||||
} else {
|
||||
// It's a unique directory, walk it and report all files within
|
||||
filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
fmt.Println(prefix, path)
|
||||
*diffs = append(*diffs, 0)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compareFiles compares two files, first byte-by-byte, then with ssdeep if different.
|
||||
func compareFiles(path1, path2 string, diffs *[]int) {
|
||||
f1, err := os.Open(path1)
|
||||
if err != nil {
|
||||
log.Printf("Error opening file %s: %v", path1, err)
|
||||
return
|
||||
}
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.Open(path2)
|
||||
if err != nil {
|
||||
log.Printf("Error opening file %s: %v", path2, err)
|
||||
return
|
||||
}
|
||||
defer f2.Close()
|
||||
|
||||
// Compare files chunk by chunk
|
||||
for {
|
||||
b1 := make([]byte, blockSize)
|
||||
_, err1 := f1.Read(b1)
|
||||
|
||||
b2 := make([]byte, blockSize)
|
||||
_, err2 := f2.Read(b2)
|
||||
|
||||
if err1 == io.EOF && err2 == io.EOF {
|
||||
// Files are identical
|
||||
fmt.Printf("%5s matches %s\n", "100", getRelativePath(path1))
|
||||
*diffs = append(*diffs, 100)
|
||||
return
|
||||
}
|
||||
|
||||
if err1 != nil && err1 != io.EOF || err2 != nil && err2 != io.EOF {
|
||||
log.Printf("Error reading from files: %v, %v", err1, err2)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(b1, b2) {
|
||||
// Files differ, use ssdeep
|
||||
hash1, _ := ssdeep.HashFromFile(path1) // CORRECTED
|
||||
hash2, _ := ssdeep.HashFromFile(path2) // CORRECTED
|
||||
score, _ := ssdeep.Distance(hash1, hash2) // CORRECTED
|
||||
fmt.Printf("%5s differs %s\n", strconv.Itoa(score), getRelativePath(path1))
|
||||
*diffs = append(*diffs, score)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getRelativePath tries to mimic the Python script's path trimming.
|
||||
func getRelativePath(path string) string {
|
||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||
if len(parts) > 1 {
|
||||
return strings.Join(parts[1:], "/")
|
||||
}
|
||||
return path
|
||||
}
|
||||
File renamed without changes.
@@ -1,140 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func printUsage() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <range specification> [file]\n", os.Args[0])
|
||||
fmt.Fprintln(os.Stderr, "Options:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintln(os.Stderr, "\nRange specification examples:")
|
||||
fmt.Fprintln(os.Stderr, "10,20 Reads lines 10 to 20 from the input")
|
||||
fmt.Fprintln(os.Stderr, "15:+5 Reads 5 lines starting from line 15")
|
||||
fmt.Fprintln(os.Stderr, "3 Reads from line 3 to the end of the file")
|
||||
fmt.Fprintln(os.Stderr, "+2 Reads the first 2 lines")
|
||||
fmt.Fprintln(os.Stderr, "Please ensure you input valid range specifications and file paths.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func parseArgs(args []string) (int, int, string, error) {
|
||||
var filename string
|
||||
var start, length, end int
|
||||
found := false
|
||||
|
||||
// Concatenate all arguments to a single string for easier parsing
|
||||
joinedArgs := strings.Join(args, " ")
|
||||
|
||||
// Regular expression to match ranges and numbers
|
||||
rangeRegex := regexp.MustCompile(`((\d+)?([ :,;-]))(\+)?(\d+)`)
|
||||
range2Regex := regexp.MustCompile(`((\d+)([ :,;-]))`)
|
||||
range3Regex := regexp.MustCompile(`(\+)?(\d+)`)
|
||||
|
||||
matches := rangeRegex.FindStringSubmatch(joinedArgs)
|
||||
|
||||
if matches != nil {
|
||||
//check if start was defined
|
||||
if matches[2] != "" {
|
||||
start, _ = strconv.Atoi(matches[2]) // Convert start line to integer
|
||||
} else {
|
||||
start = 1
|
||||
}
|
||||
if matches[4] == "+" { // Check if it's a relative length
|
||||
length, _ = strconv.Atoi(matches[5]) // Convert length to integer
|
||||
end = start + length - 1
|
||||
} else {
|
||||
end, _ = strconv.Atoi(matches[5]) // Convert end line to integer
|
||||
}
|
||||
// Remove the matched part from the arguments
|
||||
joinedArgs = strings.Replace(joinedArgs, matches[0], "", 1)
|
||||
found = true
|
||||
} else {
|
||||
matches = range2Regex.FindStringSubmatch(joinedArgs)
|
||||
if matches != nil {
|
||||
start, _ = strconv.Atoi(matches[2]) // Convert start line to integer
|
||||
end = -1
|
||||
// Remove the matched part from the arguments
|
||||
joinedArgs = strings.Replace(joinedArgs, matches[0], "", 1)
|
||||
found = true
|
||||
} else {
|
||||
matches = range3Regex.FindStringSubmatch(joinedArgs)
|
||||
if matches != nil {
|
||||
if matches[1] == "+" { // Check if it's a relative length
|
||||
length, _ := strconv.Atoi(matches[2]) // Convert length to integer
|
||||
start = 1
|
||||
end = start + length - 1
|
||||
} else { // Otherwise convert it to an absolute length
|
||||
start, _ = strconv.Atoi(matches[2]) // Convert start line to integer
|
||||
end = -1
|
||||
// Remove the matched part from the arguments
|
||||
}
|
||||
joinedArgs = strings.Replace(joinedArgs, matches[0], "", 1)
|
||||
found = true
|
||||
} else {
|
||||
found = false
|
||||
printUsage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up and identify the filename, if present
|
||||
joinedArgs = strings.TrimSpace(joinedArgs)
|
||||
if joinedArgs != "" && !found {
|
||||
// If we didn't find numbers, interpret the remaining as filename
|
||||
filename = joinedArgs
|
||||
} else if joinedArgs != "" {
|
||||
// Otherwise, interpret any non-empty remaining part as filename
|
||||
filename = joinedArgs
|
||||
}
|
||||
|
||||
if !found {
|
||||
return 0, 0, "", fmt.Errorf("no valid range or line number found")
|
||||
}
|
||||
|
||||
return start, end, filename, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
startIndex, endIndex, filename, err := parseArgs(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error parsing arguments: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Detemine the input source (file or stdin)
|
||||
inputSource := os.Stdin
|
||||
if filename != "" {
|
||||
inputSource, err = os.Open(filename)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file: %v\n", err)
|
||||
}
|
||||
defer inputSource.Close()
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(inputSource)
|
||||
//print all lines from line buner start to linenumber end unless linenumber end is -1
|
||||
for i := 1; scanner.Scan(); i++ {
|
||||
if startIndex <= i && (i <= endIndex || endIndex == -1) {
|
||||
fmt.Println(scanner.Text())
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func hashLine(s string) uint32 {
|
||||
hasher := fnv.New32a()
|
||||
hasher.Write([]byte(s))
|
||||
return hasher.Sum32()
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Define command line flags
|
||||
reverse := flag.Bool("d", false, "Print only lines that appear more than once.")
|
||||
help := flag.Bool("h", false, "Display help and usage information.")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s:\n", os.Args[0])
|
||||
fmt.Println("This program reads from a file or standard input, deduplicates lines, and outputs the results.")
|
||||
fmt.Println("Options:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("Example usage:")
|
||||
fmt.Println("\t", os.Args[0], "[options] [filename]")
|
||||
fmt.Println("\t", os.Args[0], "-d filename # Only print duplicates")
|
||||
fmt.Println("\t", "cat /some/text/file |", os.Args[0], "# Read from standard input")
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
// Check for help flag
|
||||
if *help {
|
||||
flag.Usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Detemine the input source (file or stdin)
|
||||
inputSource := os.Stdin
|
||||
var err error
|
||||
if flag.NArg() > 0 {
|
||||
inputSource, err = os.Open(flag.Args()[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to open file: %v\n", err)
|
||||
}
|
||||
defer inputSource.Close()
|
||||
}
|
||||
seenLines := make(map[uint32]int)
|
||||
scanner := bufio.NewScanner(inputSource)
|
||||
|
||||
//Readin lines
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
hash := hashLine(line)
|
||||
seenLines[hash]++
|
||||
|
||||
if *reverse {
|
||||
// Print only lines that appear more than once
|
||||
if seenLines[hash] > 1 {
|
||||
fmt.Println(line)
|
||||
}
|
||||
} else {
|
||||
// Normal mode, print only unique lines
|
||||
if seenLines[hash] == 1 {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
//Check for errors during scanning
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatalf("Failed to read input: %v\n", err)
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,4 @@
|
||||
dist/
|
||||
ipgrep
|
||||
/ipgrep
|
||||
*.test
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,418 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/cache"
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/extract"
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/ipinfo"
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/normalize"
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/ping"
|
||||
"git.ktf.ninja/tabledevil/gists/projects/go-tools/go/goipgrep/internal/resolve"
|
||||
)
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
commit = "unknown"
|
||||
date = "unknown"
|
||||
)
|
||||
|
||||
type stringSlice []string
|
||||
|
||||
func (s *stringSlice) String() string { return strings.Join(*s, ",") }
|
||||
func (s *stringSlice) Set(v string) error {
|
||||
*s = append(*s, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
var (
|
||||
// legacy short flags (kept for compatibility)
|
||||
shortUniq bool
|
||||
shortSort bool
|
||||
shortMAC bool
|
||||
shortPing bool
|
||||
shortResolve bool
|
||||
shortLookup bool
|
||||
shortFile string
|
||||
|
||||
// long flags
|
||||
longUniq bool
|
||||
longSort bool
|
||||
longMAC bool
|
||||
longPing bool
|
||||
longResolve bool
|
||||
longLookup bool
|
||||
longFiles stringSlice
|
||||
|
||||
format string
|
||||
jobs int
|
||||
timeout time.Duration
|
||||
pingMode string
|
||||
ipv6 bool
|
||||
cachePath string
|
||||
noCache bool
|
||||
clearCache bool
|
||||
cacheTTL time.Duration
|
||||
cacheMax int
|
||||
ipinfoToken string
|
||||
showVersion bool
|
||||
)
|
||||
|
||||
flag.BoolVar(&shortUniq, "u", false, "only show uniq IPs/MACs (implies -s)")
|
||||
flag.BoolVar(&shortSort, "s", false, "sort output")
|
||||
flag.BoolVar(&shortMAC, "m", false, "grep MAC addresses instead of IP addresses")
|
||||
flag.BoolVar(&shortPing, "p", false, "only show entries considered reachable (see --ping-mode)")
|
||||
flag.BoolVar(&shortResolve, "r", false, "resolve: IP reverse DNS; MAC best-effort via neighbor/ARP table (Linux only)")
|
||||
flag.BoolVar(&shortLookup, "l", false, "lookup ip info using ipinfo.io (IP only)")
|
||||
flag.StringVar(&shortFile, "f", "", "input file (legacy; prefer --file)")
|
||||
|
||||
flag.BoolVar(&longUniq, "uniq", false, "only show uniq IPs/MACs (implies --sort)")
|
||||
flag.BoolVar(&longSort, "sort", false, "sort output")
|
||||
flag.BoolVar(&longMAC, "mac", false, "grep MAC addresses instead of IP addresses")
|
||||
flag.BoolVar(&longPing, "pingable", false, "only show entries considered reachable (see --ping-mode)")
|
||||
flag.BoolVar(&longResolve, "resolve", false, "resolve: IP reverse DNS; MAC best-effort via neighbor/ARP table (Linux only)")
|
||||
flag.BoolVar(&longLookup, "lookup", false, "lookup ip info using ipinfo.io (IP only)")
|
||||
flag.Var(&longFiles, "file", "input file (repeatable)")
|
||||
|
||||
flag.StringVar(&format, "format", "text", "output format: text|csv|json")
|
||||
flag.IntVar(&jobs, "jobs", runtime.GOMAXPROCS(0), "max concurrent workers for network operations")
|
||||
flag.DurationVar(&timeout, "timeout", 2*time.Second, "per-operation timeout for -p/-r/-l")
|
||||
flag.StringVar(&pingMode, "ping-mode", "auto", "reachability mode for -p: auto|icmp|tcp")
|
||||
flag.BoolVar(&ipv6, "ipv6", false, "also match IPv6 addresses (default: IPv4 only)")
|
||||
|
||||
flag.StringVar(&cachePath, "cache-path", "", "cache path for ipinfo lookups (default: OS cache dir)")
|
||||
flag.BoolVar(&noCache, "no-cache", false, "disable reading/writing the ipinfo cache")
|
||||
flag.BoolVar(&clearCache, "clear-cache", false, "clear the ipinfo cache then exit")
|
||||
flag.DurationVar(&cacheTTL, "cache-ttl", 30*24*time.Hour, "ipinfo cache TTL (0 disables expiry)")
|
||||
flag.IntVar(&cacheMax, "cache-max-entries", 50000, "max cached IPs for ipinfo lookups (0 disables pruning)")
|
||||
flag.StringVar(&ipinfoToken, "ipinfo-token", "", "ipinfo.io token (also read from IPINFO_TOKEN)")
|
||||
|
||||
flag.BoolVar(&showVersion, "version", false, "print version and exit")
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [flags] [file...]\n\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if showVersion {
|
||||
fmt.Printf("ipgrep %s (%s) %s\n", version, commit, date)
|
||||
return
|
||||
}
|
||||
|
||||
uniqFlag := shortUniq || longUniq
|
||||
sortFlag := shortSort || longSort
|
||||
macFlag := shortMAC || longMAC
|
||||
pingable := shortPing || longPing
|
||||
resolveFlag := shortResolve || longResolve
|
||||
lookupFlag := shortLookup || longLookup
|
||||
|
||||
// keep old behavior: pingable/lookup implies uniq+sort
|
||||
if pingable || lookupFlag {
|
||||
sortFlag = true
|
||||
uniqFlag = true
|
||||
}
|
||||
if uniqFlag {
|
||||
sortFlag = true
|
||||
}
|
||||
|
||||
if lookupFlag && macFlag {
|
||||
fatalf("lookup mode (-l/--lookup) only works for IP addresses, not MAC addresses")
|
||||
}
|
||||
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
switch format {
|
||||
case "text", "csv", "json":
|
||||
default:
|
||||
fatalf("invalid --format %q (want text|csv|json)", format)
|
||||
}
|
||||
pingMode = strings.ToLower(strings.TrimSpace(pingMode))
|
||||
switch pingMode {
|
||||
case "auto", "icmp", "tcp":
|
||||
default:
|
||||
fatalf("invalid --ping-mode %q (want auto|icmp|tcp)", pingMode)
|
||||
}
|
||||
if jobs < 1 {
|
||||
fatalf("--jobs must be >= 1")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
fatalf("--timeout must be > 0")
|
||||
}
|
||||
|
||||
var files []string
|
||||
if shortFile != "" {
|
||||
files = append(files, shortFile)
|
||||
}
|
||||
files = append(files, longFiles...)
|
||||
files = append(files, flag.Args()...)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
// cache management for lookup mode
|
||||
if lookupFlag && ipinfoToken == "" {
|
||||
ipinfoToken = os.Getenv("IPINFO_TOKEN")
|
||||
}
|
||||
|
||||
// Resolve cache default path.
|
||||
if cachePath == "" {
|
||||
cachePath = cache.DefaultPath()
|
||||
}
|
||||
|
||||
if clearCache {
|
||||
if err := cache.Clear(cachePath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
fatalf("clear cache: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mode := extract.ModeIP
|
||||
if macFlag {
|
||||
mode = extract.ModeMAC
|
||||
}
|
||||
|
||||
// Extraction stage (stream input, collect matches as needed).
|
||||
var matches []string
|
||||
seen := map[string]struct{}{}
|
||||
emit := func(s string) {
|
||||
if uniqFlag {
|
||||
if _, ok := seen[s]; ok {
|
||||
return
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
}
|
||||
matches = append(matches, s)
|
||||
}
|
||||
|
||||
exOpts := extract.Options{Mode: mode, IPv6: ipv6}
|
||||
|
||||
if len(files) == 0 {
|
||||
if err := extract.Grep(ctx, os.Stdin, exOpts, func(raw string) {
|
||||
if mode == extract.ModeMAC {
|
||||
if norm, ok := normalize.NormalizeMAC(raw); ok {
|
||||
emit(norm)
|
||||
}
|
||||
return
|
||||
}
|
||||
if norm, ok := normalize.NormalizeIP(raw, ipv6); ok {
|
||||
emit(norm)
|
||||
}
|
||||
}); err != nil {
|
||||
fatalf("read stdin: %v", err)
|
||||
}
|
||||
} else {
|
||||
for _, f := range files {
|
||||
r, err := os.Open(f)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: open %s: %v\n", f, err)
|
||||
continue
|
||||
}
|
||||
err = extract.Grep(ctx, r, exOpts, func(raw string) {
|
||||
if mode == extract.ModeMAC {
|
||||
if norm, ok := normalize.NormalizeMAC(raw); ok {
|
||||
emit(norm)
|
||||
}
|
||||
return
|
||||
}
|
||||
if norm, ok := normalize.NormalizeIP(raw, ipv6); ok {
|
||||
emit(norm)
|
||||
}
|
||||
})
|
||||
_ = r.Close()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: read %s: %v\n", f, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if sortFlag {
|
||||
sort.Strings(matches)
|
||||
}
|
||||
|
||||
// -p reachability filter
|
||||
if pingable {
|
||||
popts := ping.Options{
|
||||
Mode: pingMode,
|
||||
Timeout: timeout,
|
||||
Jobs: jobs,
|
||||
}
|
||||
if macFlag {
|
||||
var err error
|
||||
matches, err = ping.FilterMACs(ctx, matches, popts)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
matches, err = ping.FilterIPs(ctx, matches, popts)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
if sortFlag {
|
||||
sort.Strings(matches)
|
||||
}
|
||||
}
|
||||
|
||||
// -l ipinfo lookup
|
||||
if lookupFlag {
|
||||
if format == "csv" {
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
must(w.Write([]string{"ip", "country", "region", "city", "org", "hostname"}))
|
||||
client := ipinfo.Client{
|
||||
Token: ipinfoToken,
|
||||
Timeout: timeout,
|
||||
}
|
||||
var store *cache.Store
|
||||
if !noCache {
|
||||
s, err := cache.Load(cachePath, cacheTTL, cacheMax)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: cache load: %v\n", err)
|
||||
} else {
|
||||
store = s
|
||||
}
|
||||
}
|
||||
results := ipinfo.LookupAll(ctx, matches, client, store, jobs)
|
||||
sort.Slice(results, func(i, j int) bool { return results[i].IP < results[j].IP })
|
||||
for _, info := range results {
|
||||
must(w.Write([]string{info.IP, info.Country, info.Region, info.City, info.Org, info.Hostname}))
|
||||
}
|
||||
w.Flush()
|
||||
if err := w.Error(); err != nil {
|
||||
fatalf("csv: %v", err)
|
||||
}
|
||||
if store != nil && store.Changed() {
|
||||
if err := store.Save(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: cache save: %v\n", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
client := ipinfo.Client{
|
||||
Token: ipinfoToken,
|
||||
Timeout: timeout,
|
||||
}
|
||||
var store *cache.Store
|
||||
if !noCache {
|
||||
s, err := cache.Load(cachePath, cacheTTL, cacheMax)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: cache load: %v\n", err)
|
||||
} else {
|
||||
store = s
|
||||
}
|
||||
}
|
||||
results := ipinfo.LookupAll(ctx, matches, client, store, jobs)
|
||||
sort.Slice(results, func(i, j int) bool { return results[i].IP < results[j].IP })
|
||||
|
||||
if format == "json" {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
must(enc.Encode(results))
|
||||
} else {
|
||||
for _, info := range results {
|
||||
fmt.Printf("%s\t%s\t%s\t%s\t%s\t%s\n", info.IP, info.Country, info.Region, info.City, info.Org, info.Hostname)
|
||||
}
|
||||
}
|
||||
if store != nil && store.Changed() {
|
||||
if err := store.Save(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: cache save: %v\n", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// -r resolution
|
||||
if resolveFlag {
|
||||
if macFlag {
|
||||
res, err := resolve.ResolveMACs(matches)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
if format == "json" {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
must(enc.Encode(res))
|
||||
return
|
||||
}
|
||||
for _, row := range res {
|
||||
// one line per mapping
|
||||
for _, ip := range row.IPs {
|
||||
fmt.Printf("%s\t%s\n", row.MAC, ip)
|
||||
}
|
||||
if len(row.IPs) == 0 {
|
||||
fmt.Println(row.MAC)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
res := resolve.ReverseLookupAll(ctx, matches, timeout, jobs)
|
||||
if format == "json" {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
must(enc.Encode(res))
|
||||
return
|
||||
}
|
||||
for _, row := range res {
|
||||
if row.Hostname != "" {
|
||||
fmt.Printf("%s\t%s\n", row.IP, row.Hostname)
|
||||
} else {
|
||||
fmt.Println(row.IP)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// plain output
|
||||
if format == "json" {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
must(enc.Encode(matches))
|
||||
return
|
||||
}
|
||||
if format == "csv" {
|
||||
// csv doesn't make much sense for plain extraction, but keep it consistent.
|
||||
w := csv.NewWriter(os.Stdout)
|
||||
must(w.Write([]string{"value"}))
|
||||
for _, m := range matches {
|
||||
must(w.Write([]string{m}))
|
||||
}
|
||||
w.Flush()
|
||||
if err := w.Error(); err != nil {
|
||||
fatalf("csv: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, m := range matches {
|
||||
fmt.Println(m)
|
||||
}
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func fatalf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "ipgrep: "+format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,5 @@
|
||||
module pname
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377
|
||||
@@ -0,0 +1,2 @@
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377 h1:gT+RM6gdTIAzMT7HUvmT5mL8SyG8Wx7iS3+L0V34Km4=
|
||||
github.com/erikdubbelboer/gspt v0.0.0-20210805194459-ce36a5128377/go.mod h1:v6o7m/E9bfvm79dE1iFiF+3T7zLBnrjYjkWMa1J+Hv0=
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/erikdubbelboer/gspt" // Import the new library
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Change the process title using the library
|
||||
gspt.SetProcTitle("notmyname")
|
||||
|
||||
fmt.Println("Process name changed. Press any key to exit...")
|
||||
|
||||
// Disable input buffering
|
||||
exec.Command("stty", "-F", "/dev/tty", "cbreak", "min", "1").Run()
|
||||
// Do not display entered characters on the screen
|
||||
exec.Command("stty", "-F", "/dev/tty", "-echo").Run()
|
||||
|
||||
var b []byte = make([]byte, 1)
|
||||
os.Stdin.Read(b)
|
||||
|
||||
// Restore the original terminal settings
|
||||
exec.Command("stty", "-F", "/dev/tty", "sane").Run()
|
||||
|
||||
fmt.Println("\nKey pressed. Exiting.")
|
||||
}
|
||||
File renamed without changes.
@@ -1,55 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
eprintln!("Usage: {} start-end [count] [file...]", args[0]);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let range = &args[1];
|
||||
let (start, mut end) = if let Some(dash_pos) = range.find('-') {
|
||||
(range[..dash_pos].parse().unwrap(), range[dash_pos + 1..].parse().unwrap())
|
||||
} else {
|
||||
(range.parse().unwrap(), 0)
|
||||
};
|
||||
|
||||
let mut count = 1;
|
||||
let mut files_start = 2;
|
||||
|
||||
if end == 0 {
|
||||
if args.len() > 2 {
|
||||
if let Ok(c) = args[2].parse::<usize>() {
|
||||
count = c;
|
||||
files_start = 3;
|
||||
}
|
||||
}
|
||||
end = start + count - 1;
|
||||
}
|
||||
|
||||
if args.len() > files_start {
|
||||
for filename in &args[files_start..] {
|
||||
let file = File::open(filename)?;
|
||||
let reader = BufReader::new(file);
|
||||
process_lines(reader, start, end)?;
|
||||
}
|
||||
} else {
|
||||
// No files provided, read from stdin
|
||||
let stdin = io::stdin();
|
||||
let reader = stdin.lock();
|
||||
process_lines(reader, start, end)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_lines<R: BufRead>(reader: R, start: usize, end: usize) -> io::Result<()> {
|
||||
reader.lines()
|
||||
.enumerate()
|
||||
.filter_map(|(i, line)| if i + 1 >= start && i + 1 <= end { line.ok() } else { None })
|
||||
.for_each(|line| println!("{}", line));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader};
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
if args.len() > 1 {
|
||||
for filename in &args[1..] {
|
||||
let file = File::open(filename)?;
|
||||
let reader = BufReader::new(file);
|
||||
remove_duplicates(reader)?;
|
||||
}
|
||||
} else {
|
||||
// No files provided, read from stdin
|
||||
let stdin = io::stdin();
|
||||
let reader = stdin.lock();
|
||||
remove_duplicates(reader)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_duplicates<R: BufRead>(reader: R) -> io::Result<()> {
|
||||
let mut seen_hashes = HashSet::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let mut hasher = DefaultHasher::new();
|
||||
line.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
if seen_hashes.insert(hash) {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
||||
struct HashOnlySet {
|
||||
set: HashSet<u64>,
|
||||
}
|
||||
|
||||
impl HashOnlySet {
|
||||
fn new() -> HashOnlySet {
|
||||
HashOnlySet { set: HashSet::new() }
|
||||
}
|
||||
|
||||
fn insert<T: Hash>(&mut self, item: &T) -> bool {
|
||||
let hash = Self::hash_item(item);
|
||||
self.set.insert(hash)
|
||||
}
|
||||
|
||||
fn contains<T: Hash>(&self, item: &T) -> bool {
|
||||
let hash = Self::hash_item(item);
|
||||
self.set.contains(&hash)
|
||||
}
|
||||
|
||||
fn hash_item<T: Hash>(item: &T) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
item.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut set = HashOnlySet::new();
|
||||
set.insert(&"Hello, world!");
|
||||
println!("Contains 'Hello, world!': {}", set.contains(&"Hello, world!"));
|
||||
println!("Contains 'Goodbye, world!': {}", set.contains(&"Goodbye, world!"));
|
||||
}
|
||||
Reference in new issue
Block a user