2025-05-08 18:54:23 +01:00
|
|
|
/**
|
|
|
|
|
* Owl - file renaming tool
|
|
|
|
|
* Copyright (C) 2025 User SixteenThousand of github.com
|
|
|
|
|
* Email: thomsixteenthousand@gmail.com
|
|
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2025-05-24 00:14:20 +01:00
|
|
|
"io/fs"
|
2025-05-08 18:54:23 +01:00
|
|
|
"os"
|
|
|
|
|
fpath "path/filepath"
|
2025-05-17 16:14:50 +01:00
|
|
|
"slices"
|
2025-05-27 23:45:09 +01:00
|
|
|
"strconv"
|
2025-05-08 18:54:23 +01:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// TYPES
|
|
|
|
|
/**
|
|
|
|
|
* A runeset represents a list of Unicode Code Points (Hereafter referred to
|
|
|
|
|
* as "Code Points") which are considered valid by Owl. Each pair in the
|
|
|
|
|
* list should be two Code Points, which represent the lower & upper bounds
|
|
|
|
|
* (in that order) of a range of Code Points which are valid. Ranges are
|
|
|
|
|
* inclusive.
|
|
|
|
|
*/
|
2025-05-24 00:14:20 +01:00
|
|
|
type runeset [][2]rune
|
2025-05-08 18:54:23 +01:00
|
|
|
|
2025-05-28 00:32:08 +01:00
|
|
|
/**
|
|
|
|
|
* A basic search-and-replace operation. Replaces each instance of "Target"
|
|
|
|
|
* with each member of "Subs" in order, with all instances beyond
|
|
|
|
|
* length(Subs) being replaced by the last member of Subs.
|
|
|
|
|
*/
|
|
|
|
|
type replacement struct {
|
|
|
|
|
Target string
|
|
|
|
|
Subs []string
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 18:54:23 +01:00
|
|
|
// Unrelated to the standard library's "context".
|
2025-05-24 00:14:20 +01:00
|
|
|
type context struct {
|
2025-05-23 22:46:17 +01:00
|
|
|
FileList []string
|
|
|
|
|
RecurseDirs []string
|
2025-05-13 17:48:19 +01:00
|
|
|
DryRun bool
|
|
|
|
|
Strategy string
|
|
|
|
|
DoHelp bool
|
|
|
|
|
DoVersion bool
|
2025-05-27 23:05:35 +01:00
|
|
|
Rset runeset
|
2025-05-27 23:45:09 +01:00
|
|
|
TruncLen int
|
2025-05-28 00:32:08 +01:00
|
|
|
Replacements []replacement
|
2025-05-08 18:54:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// CONSTANTS
|
|
|
|
|
// The largest Unicode code point.
|
|
|
|
|
// See link below for more details.
|
|
|
|
|
// https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-2/#G25564
|
|
|
|
|
const MAX_CODE_POINT rune = 0x10ffff
|
2025-05-24 11:38:03 +01:00
|
|
|
var (
|
|
|
|
|
ErrNoPwd = errors.New("Could not get current working directory!")
|
|
|
|
|
)
|
2025-05-08 18:54:23 +01:00
|
|
|
|
|
|
|
|
var OwlVersion string
|
|
|
|
|
|
|
|
|
|
// The runeset of valid runes for file names in FAT32 & exFAT file systems.
|
|
|
|
|
var FAT_RUNESET = runeset{
|
|
|
|
|
{ 0x20, 0x21 },
|
|
|
|
|
{ 0x23, 0x29 },
|
|
|
|
|
{ 0x2b, 0x2e },
|
|
|
|
|
{ 0x30, 0x39 },
|
|
|
|
|
{ 0x3b, 0x3b },
|
|
|
|
|
{ 0x3d, 0x3d },
|
|
|
|
|
{ 0x40, 0x5b },
|
|
|
|
|
{ 0x5d, 0x7b },
|
|
|
|
|
{ 0x7d, MAX_CODE_POINT},
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 23:05:35 +01:00
|
|
|
var POSIX_PORTABLE_RUNESET = runeset{
|
2025-05-28 10:58:55 +01:00
|
|
|
{ 0x2d, 0x2e },
|
2025-05-27 23:05:35 +01:00
|
|
|
{ 0x5f, 0x5f },
|
|
|
|
|
{ 0x30, 0x39 },
|
|
|
|
|
{ 0x41, 0x5a },
|
|
|
|
|
{ 0x61, 0x7a },
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 10:58:55 +01:00
|
|
|
var SHELL_RUNESET = runeset{
|
|
|
|
|
{ 0x25, 0x25 },
|
|
|
|
|
{ 0x2d, 0x2e },
|
|
|
|
|
{ 0x30, 0x39 },
|
|
|
|
|
{ 0x41, 0x5a },
|
|
|
|
|
{ 0x5f, 0x5f },
|
|
|
|
|
{ 0x61, 0x7a },
|
|
|
|
|
{ 0xc0, MAX_CODE_POINT },
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-08 18:54:23 +01:00
|
|
|
/// MAIN FUNCTIONS
|
2025-05-17 16:14:50 +01:00
|
|
|
/**
|
2025-05-24 00:14:20 +01:00
|
|
|
* Compares paths so that all files come before the directories that contain
|
|
|
|
|
* them. Files in the same directory come in alphanumeric
|
2025-05-17 16:14:50 +01:00
|
|
|
* order.
|
2025-05-24 11:38:03 +01:00
|
|
|
* Note that paths a,b MUST be absolute.
|
2025-05-17 16:14:50 +01:00
|
|
|
*/
|
2025-05-24 00:14:20 +01:00
|
|
|
func comparePaths(a, b string) int {
|
|
|
|
|
sep := string(fpath.Separator)
|
|
|
|
|
aNumComponents := len(strings.Split(a, sep))
|
|
|
|
|
bNumComponents := len(strings.Split(b, sep))
|
|
|
|
|
if aNumComponents == bNumComponents {
|
|
|
|
|
return strings.Compare(fpath.Base(a), fpath.Base(b))
|
|
|
|
|
}
|
|
|
|
|
return bNumComponents - aNumComponents
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 13:08:48 +01:00
|
|
|
/**
|
|
|
|
|
* Parses the targeting options into a list of files to rename (stored in a
|
|
|
|
|
* map as keys, for storing their new names as values later) and a list of
|
|
|
|
|
* all files in the same directory as a target file, with their name in
|
|
|
|
|
* lower case, for checking name collisions.
|
|
|
|
|
*/
|
|
|
|
|
// TODO: Write tests for this function
|
|
|
|
|
func (ctx *context) parseFileList() ([]string, map[string]bool, error) {
|
2025-05-24 00:14:20 +01:00
|
|
|
errMsgs := []string{}
|
2025-05-27 13:08:48 +01:00
|
|
|
nearbyFiles := make(map[string]bool)
|
|
|
|
|
targets := []string{}
|
|
|
|
|
addPath := func(pathList *[]string, path string) {
|
|
|
|
|
loc, isDup := slices.BinarySearchFunc(*pathList, path, comparePaths)
|
2025-05-24 11:56:29 +01:00
|
|
|
if !isDup {
|
2025-05-27 13:08:48 +01:00
|
|
|
*pathList = slices.Insert(*pathList, loc, path)
|
2025-05-17 16:14:50 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
addRecursively := func(path string, entry fs.DirEntry, err error) error {
|
2025-05-24 00:14:20 +01:00
|
|
|
if err != nil {
|
2025-05-24 11:56:29 +01:00
|
|
|
// This can only happen with the path passed to WalkDir
|
2025-05-27 13:08:48 +01:00
|
|
|
errMsgs = append(
|
|
|
|
|
errMsgs, fmt.Sprintf("Directory <<%s>> not searchable", path))
|
|
|
|
|
return nil
|
2025-05-24 00:14:20 +01:00
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
lowerCasedPath := fpath.Join(fpath.Dir(path), strings.ToLower(entry.Name()))
|
|
|
|
|
nearbyFiles[lowerCasedPath] = true
|
|
|
|
|
addPath(&targets, path)
|
2025-05-24 00:14:20 +01:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for _, dir := range ctx.RecurseDirs {
|
2025-05-24 11:38:03 +01:00
|
|
|
dir, err := fpath.Abs(dir)
|
|
|
|
|
if err != nil {
|
2025-05-27 13:08:48 +01:00
|
|
|
return targets, nearbyFiles, ErrNoPwd
|
2025-05-24 11:38:03 +01:00
|
|
|
}
|
2025-05-24 11:56:29 +01:00
|
|
|
fpath.WalkDir(dir, addRecursively)
|
|
|
|
|
}
|
|
|
|
|
for _, path := range ctx.FileList {
|
|
|
|
|
if _, err := os.Stat(path); err != nil {
|
|
|
|
|
msg := fmt.Sprintf( "File <<%s>> does not exist", path)
|
|
|
|
|
errMsgs = append(errMsgs, msg)
|
|
|
|
|
}
|
|
|
|
|
path, err := fpath.Abs(path)
|
|
|
|
|
if err != nil {
|
2025-05-27 13:08:48 +01:00
|
|
|
return targets, nearbyFiles, ErrNoPwd
|
2025-05-24 11:56:29 +01:00
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
addPath(&targets, path)
|
2025-05-24 00:14:20 +01:00
|
|
|
}
|
|
|
|
|
if len(errMsgs) == 0 {
|
2025-05-27 13:08:48 +01:00
|
|
|
return targets, nearbyFiles, nil
|
2025-05-24 00:14:20 +01:00
|
|
|
} else {
|
2025-05-27 13:08:48 +01:00
|
|
|
return targets, nearbyFiles, errors.New(strings.Join(errMsgs, "\n"))
|
2025-05-24 00:14:20 +01:00
|
|
|
}
|
2025-05-17 16:14:50 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-27 23:05:35 +01:00
|
|
|
func inRuneset(r rune, rset runeset) bool {
|
|
|
|
|
for _, runeRange := range rset {
|
2025-05-08 18:54:23 +01:00
|
|
|
if runeRange[0] <= r && r <= runeRange[1] {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 10:58:03 +01:00
|
|
|
func (ctx *context) restrictRuneset(s string) string {
|
2025-05-08 18:54:23 +01:00
|
|
|
result := s
|
|
|
|
|
toValidSubs := make(map[rune]string)
|
2025-05-24 10:58:03 +01:00
|
|
|
if ctx.Strategy == "remove" {
|
2025-05-08 18:54:23 +01:00
|
|
|
for _, r := range result {
|
2025-05-27 23:05:35 +01:00
|
|
|
if !inRuneset(r, ctx.Rset) {
|
2025-05-08 18:54:23 +01:00
|
|
|
toValidSubs[r] = ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for _, r := range result {
|
2025-05-27 23:05:35 +01:00
|
|
|
if !inRuneset(r, ctx.Rset) {
|
2025-05-15 12:16:10 +01:00
|
|
|
toValidSubs[r] = fmt.Sprintf("_U%X_", r)
|
2025-05-08 18:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for old, new := range toValidSubs {
|
|
|
|
|
result = strings.ReplaceAll(result, string(old), new)
|
|
|
|
|
}
|
2025-05-15 12:16:10 +01:00
|
|
|
if len(result) == 0 {
|
|
|
|
|
return "_EMPTY_"
|
|
|
|
|
}
|
2025-05-08 18:54:23 +01:00
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-27 23:45:09 +01:00
|
|
|
/**
|
|
|
|
|
* Truncate to the number of bytes given by the context, but don't break in
|
|
|
|
|
* the middle of a rune.
|
|
|
|
|
*/
|
|
|
|
|
func (ctx *context) truncate(name string) string {
|
|
|
|
|
if ctx.TruncLen < 1 {
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
for byteIndex, _ := range name {
|
|
|
|
|
if byteIndex > ctx.TruncLen {
|
|
|
|
|
return name[:byteIndex]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 00:32:08 +01:00
|
|
|
func (ctx *context) searchAndReplace(name string) string {
|
|
|
|
|
for _, rep := range ctx.Replacements {
|
|
|
|
|
for i:=0; i<len(rep.Subs)-1; i++ {
|
|
|
|
|
name = strings.Replace(name, rep.Target, rep.Subs[i], 1)
|
|
|
|
|
}
|
|
|
|
|
name = strings.ReplaceAll(name, rep.Target, rep.Subs[len(rep.Subs)-1])
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 00:14:20 +01:00
|
|
|
func warn(msg string) {
|
2025-05-08 18:54:23 +01:00
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[33m%s\x1b[0m\n", msg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func kaput(err error) {
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\x1b[31m%s\x1b[0m\n", err.Error())
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 17:48:19 +01:00
|
|
|
// TODO: Collect all invalid args into one error instead of failing fast.
|
2025-05-08 18:54:23 +01:00
|
|
|
func parseCLIArgs(args []string) (context, error) {
|
2025-05-13 17:48:19 +01:00
|
|
|
// Set defaults
|
|
|
|
|
result := context{
|
2025-05-23 22:46:17 +01:00
|
|
|
FileList: []string{},
|
|
|
|
|
RecurseDirs: []string{},
|
2025-05-13 17:48:19 +01:00
|
|
|
DryRun: false,
|
2025-05-23 22:46:17 +01:00
|
|
|
Strategy: "represent",
|
2025-05-13 17:48:19 +01:00
|
|
|
DoHelp: false,
|
|
|
|
|
DoVersion: false,
|
2025-05-27 23:05:35 +01:00
|
|
|
Rset: FAT_RUNESET,
|
2025-05-27 23:45:09 +01:00
|
|
|
TruncLen: 0,
|
2025-05-28 00:32:08 +01:00
|
|
|
Replacements: []replacement{},
|
2025-05-13 17:48:19 +01:00
|
|
|
}
|
2025-05-08 18:54:23 +01:00
|
|
|
index := 1
|
|
|
|
|
isFlag := func(arg string) bool {
|
|
|
|
|
return arg[0] == '-'
|
|
|
|
|
}
|
|
|
|
|
for index < len(args) {
|
|
|
|
|
switch arg := args[index]; arg {
|
2025-05-24 11:45:32 +01:00
|
|
|
case "-r", "--recurse":
|
2025-05-13 17:48:19 +01:00
|
|
|
index++
|
2025-05-24 00:14:20 +01:00
|
|
|
result.RecurseDirs = append(result.RecurseDirs, args[index])
|
2025-05-08 18:54:23 +01:00
|
|
|
case "-n", "--dry-run":
|
2025-05-13 17:48:19 +01:00
|
|
|
result.DryRun = true
|
|
|
|
|
case "-s", "--strategy":
|
|
|
|
|
index++
|
2025-05-24 00:14:20 +01:00
|
|
|
result.Strategy = args[index]
|
2025-05-27 23:05:35 +01:00
|
|
|
case "-p", "--portable":
|
|
|
|
|
result.Rset = POSIX_PORTABLE_RUNESET
|
2025-05-28 10:58:55 +01:00
|
|
|
case "-e", "--valid-set":
|
|
|
|
|
index++
|
|
|
|
|
switch args[index] {
|
|
|
|
|
case "fat":
|
|
|
|
|
result.Rset = FAT_RUNESET
|
|
|
|
|
case "posix":
|
|
|
|
|
result.Rset = POSIX_PORTABLE_RUNESET
|
|
|
|
|
case "shell":
|
|
|
|
|
result.Rset = SHELL_RUNESET
|
|
|
|
|
default:
|
|
|
|
|
return result, errors.New(
|
|
|
|
|
"Invalid character set! Please choose one of fat,posix,shell",
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-05-28 00:32:08 +01:00
|
|
|
case "-c", "--replace":
|
|
|
|
|
index++
|
|
|
|
|
target, subs, ok := strings.Cut(args[index], ":")
|
|
|
|
|
if !ok {
|
|
|
|
|
return result, errors.New(fmt.Sprintf(
|
2025-05-28 10:58:55 +01:00
|
|
|
"Incorrect syntax (<<%s>>) for replacement: please use '{TARGET}:{REPLACEMENT1},{REPLACEMENT2},...'",
|
2025-05-28 00:32:08 +01:00
|
|
|
args[index],
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
result.Replacements = append(
|
|
|
|
|
result.Replacements,
|
|
|
|
|
replacement{ target, strings.Split(subs, ",") },
|
|
|
|
|
)
|
2025-05-27 23:45:09 +01:00
|
|
|
case "-t", "--truncate":
|
|
|
|
|
index++
|
|
|
|
|
var err error
|
|
|
|
|
result.TruncLen, err = strconv.Atoi(args[index])
|
|
|
|
|
if err != nil || result.TruncLen < 1 {
|
|
|
|
|
return result, errors.New(fmt.Sprintf(
|
|
|
|
|
"Invalid truncation length: <<%s>>",
|
|
|
|
|
args[index],
|
|
|
|
|
))
|
|
|
|
|
}
|
2025-05-13 17:48:19 +01:00
|
|
|
case "-h", "--help":
|
|
|
|
|
result.DoHelp = true
|
|
|
|
|
case "-v", "--version":
|
|
|
|
|
result.DoVersion = true
|
2025-05-08 18:54:23 +01:00
|
|
|
default:
|
|
|
|
|
if isFlag(arg) {
|
|
|
|
|
return result, errors.New(fmt.Sprintf(
|
|
|
|
|
"Invalid flag <<%s>>",
|
|
|
|
|
arg,
|
|
|
|
|
))
|
|
|
|
|
} else {
|
2025-05-23 22:46:17 +01:00
|
|
|
result.FileList = append(result.FileList, arg)
|
2025-05-08 18:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
index++
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 17:48:19 +01:00
|
|
|
// TODO: Finish writing options short help. Mention man page where relevant.
|
|
|
|
|
func printHelp() {
|
2025-05-15 12:16:10 +01:00
|
|
|
fmt.Print(`Owl - a hunter of bad characters in filenames
|
2025-05-13 17:48:19 +01:00
|
|
|
|
|
|
|
|
Usage:
|
|
|
|
|
owl [options] FILES
|
|
|
|
|
|
2025-05-23 22:46:17 +01:00
|
|
|
Rename FILES such that all characters that are invalid in FAT file systems
|
2025-05-13 17:48:19 +01:00
|
|
|
(?,\,*,etc.) are removed.
|
|
|
|
|
|
|
|
|
|
Options:
|
2025-05-28 10:58:55 +01:00
|
|
|
-s,--strategy remove|represent
|
|
|
|
|
What to do with bad characters; either replace with a representation or
|
|
|
|
|
remove
|
2025-05-13 17:48:19 +01:00
|
|
|
-h,--help
|
2025-05-28 10:58:55 +01:00
|
|
|
Show this message
|
2025-05-13 17:48:19 +01:00
|
|
|
-v,--version
|
2025-05-28 10:58:55 +01:00
|
|
|
Show version
|
2025-05-24 11:45:32 +01:00
|
|
|
-r,--recurse DIRECTORY
|
2025-05-28 10:58:55 +01:00
|
|
|
Recursively search for files in DIRECTORY with bad characters
|
2025-05-27 23:21:48 +01:00
|
|
|
-n,--dry-run
|
2025-05-28 10:58:55 +01:00
|
|
|
Do not rename anything, just print what would be done
|
|
|
|
|
-t,--truncate LENGTH
|
|
|
|
|
Truncate all filenames to at most LENGTH bytes
|
|
|
|
|
-e,--valid-set fat|posix|shell
|
|
|
|
|
Select a set of characters to limit filenames to.
|
2025-05-27 23:21:48 +01:00
|
|
|
-p,--portable
|
2025-05-28 10:58:55 +01:00
|
|
|
Alias for "--valid-set posix"
|
2025-05-15 12:16:10 +01:00
|
|
|
|
2025-05-28 10:58:55 +01:00
|
|
|
See man page for more details
|
2025-05-13 17:48:19 +01:00
|
|
|
`);
|
2025-05-08 18:54:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
ctx, err := parseCLIArgs(os.Args)
|
|
|
|
|
kaput(err)
|
2025-05-13 17:48:19 +01:00
|
|
|
if ctx.DoHelp {
|
|
|
|
|
printHelp()
|
|
|
|
|
} else if ctx.DoVersion {
|
|
|
|
|
fmt.Printf(
|
|
|
|
|
"Owl - a hunter of bad characters in file names\nversion %s\n",
|
|
|
|
|
OwlVersion,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
2025-05-27 13:08:48 +01:00
|
|
|
targets, nearbyFiles, err := ctx.parseFileList()
|
2025-05-24 11:38:03 +01:00
|
|
|
if err == ErrNoPwd {
|
|
|
|
|
kaput(err)
|
|
|
|
|
}
|
2025-05-24 00:14:20 +01:00
|
|
|
if err != nil {
|
|
|
|
|
warn(err.Error())
|
|
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
numRenamed := 0
|
|
|
|
|
for _, file := range targets {
|
|
|
|
|
// Calculate the new name
|
2025-05-13 17:48:19 +01:00
|
|
|
oldName := fpath.Base(file)
|
|
|
|
|
dirName := fpath.Dir(file)
|
2025-05-28 00:32:08 +01:00
|
|
|
newName := ctx.truncate(ctx.restrictRuneset(ctx.searchAndReplace(
|
2025-05-24 10:58:03 +01:00
|
|
|
strings.ToValidUTF8(oldName, "_INVALID_"),
|
2025-05-28 00:32:08 +01:00
|
|
|
)))
|
2025-05-24 10:58:03 +01:00
|
|
|
newPath := fpath.Join(dirName, newName)
|
2025-05-27 13:08:48 +01:00
|
|
|
// Check that we want to rename this file
|
2025-05-24 00:14:20 +01:00
|
|
|
if newPath == file {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
lowerCasedPath := fpath.Join(dirName, strings.ToLower(newName))
|
|
|
|
|
if nearbyFiles[lowerCasedPath] {
|
|
|
|
|
warn(fmt.Sprintf(
|
|
|
|
|
"Path <<%s>> would be renamed to\n <<%s>>,\nwhich collides with\n <<%s>>\nSkipping...",
|
|
|
|
|
file,
|
|
|
|
|
newPath,
|
|
|
|
|
lowerCasedPath,
|
|
|
|
|
))
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
nearbyFiles[lowerCasedPath] = true
|
|
|
|
|
// Do the rename, or just print what would happen
|
|
|
|
|
numRenamed++
|
2025-05-13 17:48:19 +01:00
|
|
|
if ctx.DryRun {
|
2025-05-24 11:38:03 +01:00
|
|
|
format := "%s -> <<%s>>\n"
|
2025-05-27 13:08:48 +01:00
|
|
|
if numRenamed % 2 == 0 {
|
2025-05-24 11:38:03 +01:00
|
|
|
format = "\x1b[2m%s -> <<%s>>\x1b[0m\n"
|
|
|
|
|
}
|
2025-05-13 17:48:19 +01:00
|
|
|
fmt.Printf(
|
2025-05-24 11:38:03 +01:00
|
|
|
format,
|
2025-05-13 17:48:19 +01:00
|
|
|
file,
|
2025-05-23 22:46:17 +01:00
|
|
|
newPath,
|
2025-05-13 17:48:19 +01:00
|
|
|
)
|
|
|
|
|
} else {
|
2025-05-23 22:46:17 +01:00
|
|
|
os.Rename(file, newPath)
|
2025-05-13 17:48:19 +01:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-27 13:08:48 +01:00
|
|
|
fmt.Printf("%d files renamed!\n", numRenamed)
|
2025-05-08 18:54:23 +01:00
|
|
|
}
|
|
|
|
|
}
|