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:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
dist/
|
||||
ipgrep
|
||||
/ipgrep
|
||||
*.test
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.")
|
||||
}
|
||||
@@ -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