diff --git a/cmd/convmv/convmv.go b/cmd/convmv/convmv.go index 833470b25..5e4071f81 100644 --- a/cmd/convmv/convmv.go +++ b/cmd/convmv/convmv.go @@ -34,7 +34,7 @@ var commandDefinition = &cobra.Command{ Long: strings.ReplaceAll(` convmv supports advanced path name transformations for converting and renaming files and directories by applying prefixes, suffixes, and other alterations. -`+transform.SprintList()+` +`+transform.Help()+` Multiple transformations can be used in sequence, applied in the order they are specified on the command line. diff --git a/lib/transform/cmap.go b/lib/transform/cmap.go index 7f676e792..8fac5da63 100644 --- a/lib/transform/cmap.go +++ b/lib/transform/cmap.go @@ -14,6 +14,9 @@ var ( lock sync.Mutex ) +// CharmapChoices is an enum of the character map choices. +type CharmapChoices = fs.Enum[cmapChoices] + type cmapChoices struct{} func (cmapChoices) Choices() []string { diff --git a/lib/transform/help.go b/lib/transform/gen_help.go similarity index 88% rename from lib/transform/help.go rename to lib/transform/gen_help.go index a92267844..e0cc0ddd5 100644 --- a/lib/transform/help.go +++ b/lib/transform/gen_help.go @@ -1,12 +1,20 @@ -package transform +// Create the help text for transform +// +// Run with go generate (defined in transform.go) +// +//go:build none + +package main import ( "context" "fmt" + "os" "strings" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/lib/encoder" + "github.com/rclone/rclone/lib/transform" ) type commands struct { @@ -75,11 +83,11 @@ func (e example) command() string { func (e example) output() string { ctx := context.Background() - err := SetOptions(ctx, e.flags...) + err := transform.SetOptions(ctx, e.flags...) if err != nil { fs.Errorf(nil, "error generating help text: %v", err) } - return Path(ctx, e.path, false) + return transform.Path(ctx, e.path, false) } // go run ./ convmv --help @@ -102,13 +110,11 @@ func commandTable() string { return s } -var generatingHelpText bool - // SprintList returns the example help text as a string func SprintList() string { - var algos transformAlgo - var charmaps fs.Enum[cmapChoices] - generatingHelpText = true + var algos transform.Algo + var charmaps transform.CharmapChoices + s := commandTable() s += fmt.Sprintln("Conversion modes: \n```") for _, v := range algos.Choices() { @@ -130,11 +136,20 @@ func SprintList() string { s += sprintExamples() - generatingHelpText = false return s } -// PrintList prints the example help text to stdout -func PrintList() { - fmt.Println(SprintList()) +// Output the help to stdout +func main() { + out := os.Stdout + if len(os.Args) > 1 { + var err error + out, err = os.Create(os.Args[1]) + if err != nil { + fs.Fatalf(nil, "Open output failed: %v", err) + } + defer out.Close() + } + fmt.Fprintf(out, "\n\n") + fmt.Fprintln(out, SprintList()) } diff --git a/lib/transform/options.go b/lib/transform/options.go index 880fb6482..3c02b7318 100644 --- a/lib/transform/options.go +++ b/lib/transform/options.go @@ -11,9 +11,9 @@ import ( ) type transform struct { - key transformAlgo // for example, "prefix" - value string // for example, "some_prefix_" - tag tag // file, dir, or all + key Algo // for example, "prefix" + value string // for example, "some_prefix_" + tag tag // file, dir, or all } // tag controls which part of the file path is affected (file, dir, all) @@ -171,12 +171,12 @@ func (t *transform) requiresValue() bool { return false } -// transformAlgo describes conversion setting -type transformAlgo = fs.Enum[transformChoices] +// Algo describes conversion setting +type Algo = fs.Enum[transformChoices] // Supported transform options const ( - ConvNone transformAlgo = iota + ConvNone Algo = iota ConvToNFC ConvToNFD ConvToNFKC diff --git a/lib/transform/transform.go b/lib/transform/transform.go index 0103ae87f..6c7a826a2 100644 --- a/lib/transform/transform.go +++ b/lib/transform/transform.go @@ -1,9 +1,12 @@ // Package transform holds functions for path name transformations +// +//go:generate go run gen_help.go transform.md package transform import ( "bytes" "context" + _ "embed" "encoding/base64" "errors" "fmt" @@ -24,6 +27,16 @@ import ( "golang.org/x/text/unicode/norm" ) +//go:embed transform.md +var help string + +// Help returns the help string cleaned up to simplify appending +func Help() string { + // Chop off auto generated message + nl := strings.IndexRune(help, '\n') + return strings.TrimSpace(help[nl:]) + "\n\n" +} + // Path transforms a path s according to the --name-transform options in use // // If no transforms are in use, s is returned unchanged @@ -53,7 +66,7 @@ func Path(ctx context.Context, s string, isDir bool) string { fs.Errorf(s, "Failed to transform: %v", err) } } - if old != s && !generatingHelpText { + if old != s { fs.Debugf(old, "transformed to: %v", s) } if strings.Count(old, "/") != strings.Count(s, "/") { @@ -181,7 +194,7 @@ func transformPathSegment(s string, t transform) (string, error) { case ConvMacintosh: return encodeWithReplacement(s, charmap.Macintosh), nil case ConvCharmap: - var cmapType fs.Enum[cmapChoices] + var cmapType CharmapChoices err := cmapType.Set(t.value) if err != nil { return s, err diff --git a/lib/transform/transform.md b/lib/transform/transform.md new file mode 100644 index 000000000..9d2f47662 --- /dev/null +++ b/lib/transform/transform.md @@ -0,0 +1,224 @@ + + +| Command | Description | +|------|------| +| `--name-transform prefix=XXXX` | Prepends XXXX to the file name. | +| `--name-transform suffix=XXXX` | Appends XXXX to the file name after the extension. | +| `--name-transform suffix_keep_extension=XXXX` | Appends XXXX to the file name while preserving the original file extension. | +| `--name-transform trimprefix=XXXX` | Removes XXXX if it appears at the start of the file name. | +| `--name-transform trimsuffix=XXXX` | Removes XXXX if it appears at the end of the file name. | +| `--name-transform regex=/pattern/replacement/` | Applies a regex-based transformation. | +| `--name-transform replace=old:new` | Replaces occurrences of old with new in the file name. | +| `--name-transform date={YYYYMMDD}` | Appends or prefixes the specified date format. | +| `--name-transform truncate=N` | Truncates the file name to a maximum of N characters. | +| `--name-transform base64encode` | Encodes the file name in Base64. | +| `--name-transform base64decode` | Decodes a Base64-encoded file name. | +| `--name-transform encoder=ENCODING` | Converts the file name to the specified encoding (e.g., ISO-8859-1, Windows-1252, Macintosh). | +| `--name-transform decoder=ENCODING` | Decodes the file name from the specified encoding. | +| `--name-transform charmap=MAP` | Applies a character mapping transformation. | +| `--name-transform lowercase` | Converts the file name to lowercase. | +| `--name-transform uppercase` | Converts the file name to UPPERCASE. | +| `--name-transform titlecase` | Converts the file name to Title Case. | +| `--name-transform ascii` | Strips non-ASCII characters. | +| `--name-transform url` | URL-encodes the file name. | +| `--name-transform nfc` | Converts the file name to NFC Unicode normalization form. | +| `--name-transform nfd` | Converts the file name to NFD Unicode normalization form. | +| `--name-transform nfkc` | Converts the file name to NFKC Unicode normalization form. | +| `--name-transform nfkd` | Converts the file name to NFKD Unicode normalization form. | +| `--name-transform command=/path/to/my/programfile names.` | Executes an external program to transform | + + +Conversion modes: +``` +none +nfc +nfd +nfkc +nfkd +replace +prefix +suffix +suffix_keep_extension +trimprefix +trimsuffix +index +date +truncate +base64encode +base64decode +encoder +decoder +ISO-8859-1 +Windows-1252 +Macintosh +charmap +lowercase +uppercase +titlecase +ascii +url +regex +command +``` +Char maps: +``` + +IBM-Code-Page-037 +IBM-Code-Page-437 +IBM-Code-Page-850 +IBM-Code-Page-852 +IBM-Code-Page-855 +Windows-Code-Page-858 +IBM-Code-Page-860 +IBM-Code-Page-862 +IBM-Code-Page-863 +IBM-Code-Page-865 +IBM-Code-Page-866 +IBM-Code-Page-1047 +IBM-Code-Page-1140 +ISO-8859-1 +ISO-8859-2 +ISO-8859-3 +ISO-8859-4 +ISO-8859-5 +ISO-8859-6 +ISO-8859-7 +ISO-8859-8 +ISO-8859-9 +ISO-8859-10 +ISO-8859-13 +ISO-8859-14 +ISO-8859-15 +ISO-8859-16 +KOI8-R +KOI8-U +Macintosh +Macintosh-Cyrillic +Windows-874 +Windows-1250 +Windows-1251 +Windows-1252 +Windows-1253 +Windows-1254 +Windows-1255 +Windows-1256 +Windows-1257 +Windows-1258 +X-User-Defined +``` +Encoding masks: +``` +Asterisk + BackQuote + BackSlash + Colon + CrLf + Ctl + Del + Dollar + Dot + DoubleQuote + Exclamation + Hash + InvalidUtf8 + LeftCrLfHtVt + LeftPeriod + LeftSpace + LeftTilde + LtGt + None + Percent + Pipe + Question + Raw + RightCrLfHtVt + RightPeriod + RightSpace + Semicolon + SingleQuote + Slash + SquareBracket +``` +Examples: + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,uppercase" +// Output: STORIES/THE QUICK BROWN FOX!.TXT +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,replace=Fox:Turtle" --name-transform "all,replace=Quick:Slow" +// Output: stories/The Slow Brown Turtle!.txt +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,base64encode" +// Output: c3Rvcmllcw==/VGhlIFF1aWNrIEJyb3duIEZveCEudHh0 +``` + +``` +rclone convmv "c3Rvcmllcw==/VGhlIFF1aWNrIEJyb3duIEZveCEudHh0" --name-transform "all,base64decode" +// Output: stories/The Quick Brown Fox!.txt +``` + +``` +rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,nfc" +// Output: stories/The Quick Brown 🦊 Fox Went to the Café!.txt +``` + +``` +rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,nfd" +// Output: stories/The Quick Brown 🦊 Fox Went to the Café!.txt +``` + +``` +rclone convmv "stories/The Quick Brown 🦊 Fox!.txt" --name-transform "all,ascii" +// Output: stories/The Quick Brown Fox!.txt +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,trimsuffix=.txt" +// Output: stories/The Quick Brown Fox! +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,prefix=OLD_" +// Output: OLD_stories/OLD_The Quick Brown Fox!.txt +``` + +``` +rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,charmap=ISO-8859-7" +// Output: stories/The Quick Brown _ Fox Went to the Caf_!.txt +``` + +``` +rclone convmv "stories/The Quick Brown Fox: A Memoir [draft].txt" --name-transform "all,encoder=Colon,SquareBracket" +// Output: stories/The Quick Brown Fox: A Memoir [draft].txt +``` + +``` +rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,truncate=21" +// Output: stories/The Quick Brown 🦊 Fox +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,command=echo" +// Output: stories/The Quick Brown Fox!.txt +``` + +``` +rclone convmv "stories/The Quick Brown Fox!" --name-transform "date=-{YYYYMMDD}" +// Output: stories/The Quick Brown Fox!-20250618 +``` + +``` +rclone convmv "stories/The Quick Brown Fox!" --name-transform "date=-{macfriendlytime}" +// Output: stories/The Quick Brown Fox!-2025-06-18 0148PM +``` + +``` +rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,regex=[\\.\\w]/ab" +// Output: ababababababab/ababab ababababab ababababab ababab!abababab +``` + +