Skip to content

Commit 4280ec7

Browse files
committed
convmv: fix spurious "error running command echo" on Windows
Before this change the help for convmv was generated by running the examples each time rclone started up. Unfortunately this involved running the echo command which did not work on Windows. This pre-generates the help into `transform.md` and embeds it. It can be re-generated with `go generate` which is a better solution. See: https://forum.rclone.org/t/invoke-of-1-70-0-complains-of-echo-not-found/51618
1 parent b064cc2 commit 4280ec7

6 files changed

Lines changed: 276 additions & 21 deletions

File tree

cmd/convmv/convmv.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ var commandDefinition = &cobra.Command{
3434
Long: strings.ReplaceAll(`
3535
convmv supports advanced path name transformations for converting and renaming files and directories by applying prefixes, suffixes, and other alterations.
3636
37-
`+transform.SprintList()+`
37+
`+transform.Help()+`
3838
3939
Multiple transformations can be used in sequence, applied in the order they are specified on the command line.
4040

lib/transform/cmap.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ var (
1414
lock sync.Mutex
1515
)
1616

17+
// CharmapChoices is an enum of the character map choices.
18+
type CharmapChoices = fs.Enum[cmapChoices]
19+
1720
type cmapChoices struct{}
1821

1922
func (cmapChoices) Choices() []string {
Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1-
package transform
1+
// Create the help text for transform
2+
//
3+
// Run with go generate (defined in transform.go)
4+
//
5+
//go:build none
6+
7+
package main
28

39
import (
410
"context"
511
"fmt"
12+
"os"
613
"strings"
714

815
"github.com/rclone/rclone/fs"
916
"github.com/rclone/rclone/lib/encoder"
17+
"github.com/rclone/rclone/lib/transform"
1018
)
1119

1220
type commands struct {
@@ -75,11 +83,11 @@ func (e example) command() string {
7583

7684
func (e example) output() string {
7785
ctx := context.Background()
78-
err := SetOptions(ctx, e.flags...)
86+
err := transform.SetOptions(ctx, e.flags...)
7987
if err != nil {
8088
fs.Errorf(nil, "error generating help text: %v", err)
8189
}
82-
return Path(ctx, e.path, false)
90+
return transform.Path(ctx, e.path, false)
8391
}
8492

8593
// go run ./ convmv --help
@@ -102,13 +110,11 @@ func commandTable() string {
102110
return s
103111
}
104112

105-
var generatingHelpText bool
106-
107113
// SprintList returns the example help text as a string
108114
func SprintList() string {
109-
var algos transformAlgo
110-
var charmaps fs.Enum[cmapChoices]
111-
generatingHelpText = true
115+
var algos transform.Algo
116+
var charmaps transform.CharmapChoices
117+
112118
s := commandTable()
113119
s += fmt.Sprintln("Conversion modes: \n```")
114120
for _, v := range algos.Choices() {
@@ -130,11 +136,20 @@ func SprintList() string {
130136

131137
s += sprintExamples()
132138

133-
generatingHelpText = false
134139
return s
135140
}
136141

137-
// PrintList prints the example help text to stdout
138-
func PrintList() {
139-
fmt.Println(SprintList())
142+
// Output the help to stdout
143+
func main() {
144+
out := os.Stdout
145+
if len(os.Args) > 1 {
146+
var err error
147+
out, err = os.Create(os.Args[1])
148+
if err != nil {
149+
fs.Fatalf(nil, "Open output failed: %v", err)
150+
}
151+
defer out.Close()
152+
}
153+
fmt.Fprintf(out, "<!--- Docs generated by help.go - use go generate to rebuild - DO NOT EDIT --->\n\n")
154+
fmt.Fprintln(out, SprintList())
140155
}

lib/transform/options.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import (
1111
)
1212

1313
type transform struct {
14-
key transformAlgo // for example, "prefix"
15-
value string // for example, "some_prefix_"
16-
tag tag // file, dir, or all
14+
key Algo // for example, "prefix"
15+
value string // for example, "some_prefix_"
16+
tag tag // file, dir, or all
1717
}
1818

1919
// tag controls which part of the file path is affected (file, dir, all)
@@ -171,12 +171,12 @@ func (t *transform) requiresValue() bool {
171171
return false
172172
}
173173

174-
// transformAlgo describes conversion setting
175-
type transformAlgo = fs.Enum[transformChoices]
174+
// Algo describes conversion setting
175+
type Algo = fs.Enum[transformChoices]
176176

177177
// Supported transform options
178178
const (
179-
ConvNone transformAlgo = iota
179+
ConvNone Algo = iota
180180
ConvToNFC
181181
ConvToNFD
182182
ConvToNFKC

lib/transform/transform.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Package transform holds functions for path name transformations
2+
//
3+
//go:generate go run gen_help.go transform.md
24
package transform
35

46
import (
57
"bytes"
68
"context"
9+
_ "embed"
710
"encoding/base64"
811
"errors"
912
"fmt"
@@ -24,6 +27,16 @@ import (
2427
"golang.org/x/text/unicode/norm"
2528
)
2629

30+
//go:embed transform.md
31+
var help string
32+
33+
// Help returns the help string cleaned up to simplify appending
34+
func Help() string {
35+
// Chop off auto generated message
36+
nl := strings.IndexRune(help, '\n')
37+
return strings.TrimSpace(help[nl:]) + "\n\n"
38+
}
39+
2740
// Path transforms a path s according to the --name-transform options in use
2841
//
2942
// If no transforms are in use, s is returned unchanged
@@ -53,7 +66,7 @@ func Path(ctx context.Context, s string, isDir bool) string {
5366
fs.Errorf(s, "Failed to transform: %v", err)
5467
}
5568
}
56-
if old != s && !generatingHelpText {
69+
if old != s {
5770
fs.Debugf(old, "transformed to: %v", s)
5871
}
5972
if strings.Count(old, "/") != strings.Count(s, "/") {
@@ -181,7 +194,7 @@ func transformPathSegment(s string, t transform) (string, error) {
181194
case ConvMacintosh:
182195
return encodeWithReplacement(s, charmap.Macintosh), nil
183196
case ConvCharmap:
184-
var cmapType fs.Enum[cmapChoices]
197+
var cmapType CharmapChoices
185198
err := cmapType.Set(t.value)
186199
if err != nil {
187200
return s, err

lib/transform/transform.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<!--- Docs generated by help.go - use go generate to rebuild - DO NOT EDIT --->
2+
3+
| Command | Description |
4+
|------|------|
5+
| `--name-transform prefix=XXXX` | Prepends XXXX to the file name. |
6+
| `--name-transform suffix=XXXX` | Appends XXXX to the file name after the extension. |
7+
| `--name-transform suffix_keep_extension=XXXX` | Appends XXXX to the file name while preserving the original file extension. |
8+
| `--name-transform trimprefix=XXXX` | Removes XXXX if it appears at the start of the file name. |
9+
| `--name-transform trimsuffix=XXXX` | Removes XXXX if it appears at the end of the file name. |
10+
| `--name-transform regex=/pattern/replacement/` | Applies a regex-based transformation. |
11+
| `--name-transform replace=old:new` | Replaces occurrences of old with new in the file name. |
12+
| `--name-transform date={YYYYMMDD}` | Appends or prefixes the specified date format. |
13+
| `--name-transform truncate=N` | Truncates the file name to a maximum of N characters. |
14+
| `--name-transform base64encode` | Encodes the file name in Base64. |
15+
| `--name-transform base64decode` | Decodes a Base64-encoded file name. |
16+
| `--name-transform encoder=ENCODING` | Converts the file name to the specified encoding (e.g., ISO-8859-1, Windows-1252, Macintosh). |
17+
| `--name-transform decoder=ENCODING` | Decodes the file name from the specified encoding. |
18+
| `--name-transform charmap=MAP` | Applies a character mapping transformation. |
19+
| `--name-transform lowercase` | Converts the file name to lowercase. |
20+
| `--name-transform uppercase` | Converts the file name to UPPERCASE. |
21+
| `--name-transform titlecase` | Converts the file name to Title Case. |
22+
| `--name-transform ascii` | Strips non-ASCII characters. |
23+
| `--name-transform url` | URL-encodes the file name. |
24+
| `--name-transform nfc` | Converts the file name to NFC Unicode normalization form. |
25+
| `--name-transform nfd` | Converts the file name to NFD Unicode normalization form. |
26+
| `--name-transform nfkc` | Converts the file name to NFKC Unicode normalization form. |
27+
| `--name-transform nfkd` | Converts the file name to NFKD Unicode normalization form. |
28+
| `--name-transform command=/path/to/my/programfile names.` | Executes an external program to transform |
29+
30+
31+
Conversion modes:
32+
```
33+
none
34+
nfc
35+
nfd
36+
nfkc
37+
nfkd
38+
replace
39+
prefix
40+
suffix
41+
suffix_keep_extension
42+
trimprefix
43+
trimsuffix
44+
index
45+
date
46+
truncate
47+
base64encode
48+
base64decode
49+
encoder
50+
decoder
51+
ISO-8859-1
52+
Windows-1252
53+
Macintosh
54+
charmap
55+
lowercase
56+
uppercase
57+
titlecase
58+
ascii
59+
url
60+
regex
61+
command
62+
```
63+
Char maps:
64+
```
65+
66+
IBM-Code-Page-037
67+
IBM-Code-Page-437
68+
IBM-Code-Page-850
69+
IBM-Code-Page-852
70+
IBM-Code-Page-855
71+
Windows-Code-Page-858
72+
IBM-Code-Page-860
73+
IBM-Code-Page-862
74+
IBM-Code-Page-863
75+
IBM-Code-Page-865
76+
IBM-Code-Page-866
77+
IBM-Code-Page-1047
78+
IBM-Code-Page-1140
79+
ISO-8859-1
80+
ISO-8859-2
81+
ISO-8859-3
82+
ISO-8859-4
83+
ISO-8859-5
84+
ISO-8859-6
85+
ISO-8859-7
86+
ISO-8859-8
87+
ISO-8859-9
88+
ISO-8859-10
89+
ISO-8859-13
90+
ISO-8859-14
91+
ISO-8859-15
92+
ISO-8859-16
93+
KOI8-R
94+
KOI8-U
95+
Macintosh
96+
Macintosh-Cyrillic
97+
Windows-874
98+
Windows-1250
99+
Windows-1251
100+
Windows-1252
101+
Windows-1253
102+
Windows-1254
103+
Windows-1255
104+
Windows-1256
105+
Windows-1257
106+
Windows-1258
107+
X-User-Defined
108+
```
109+
Encoding masks:
110+
```
111+
Asterisk
112+
BackQuote
113+
BackSlash
114+
Colon
115+
CrLf
116+
Ctl
117+
Del
118+
Dollar
119+
Dot
120+
DoubleQuote
121+
Exclamation
122+
Hash
123+
InvalidUtf8
124+
LeftCrLfHtVt
125+
LeftPeriod
126+
LeftSpace
127+
LeftTilde
128+
LtGt
129+
None
130+
Percent
131+
Pipe
132+
Question
133+
Raw
134+
RightCrLfHtVt
135+
RightPeriod
136+
RightSpace
137+
Semicolon
138+
SingleQuote
139+
Slash
140+
SquareBracket
141+
```
142+
Examples:
143+
144+
```
145+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,uppercase"
146+
// Output: STORIES/THE QUICK BROWN FOX!.TXT
147+
```
148+
149+
```
150+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,replace=Fox:Turtle" --name-transform "all,replace=Quick:Slow"
151+
// Output: stories/The Slow Brown Turtle!.txt
152+
```
153+
154+
```
155+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,base64encode"
156+
// Output: c3Rvcmllcw==/VGhlIFF1aWNrIEJyb3duIEZveCEudHh0
157+
```
158+
159+
```
160+
rclone convmv "c3Rvcmllcw==/VGhlIFF1aWNrIEJyb3duIEZveCEudHh0" --name-transform "all,base64decode"
161+
// Output: stories/The Quick Brown Fox!.txt
162+
```
163+
164+
```
165+
rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,nfc"
166+
// Output: stories/The Quick Brown 🦊 Fox Went to the Café!.txt
167+
```
168+
169+
```
170+
rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,nfd"
171+
// Output: stories/The Quick Brown 🦊 Fox Went to the Café!.txt
172+
```
173+
174+
```
175+
rclone convmv "stories/The Quick Brown 🦊 Fox!.txt" --name-transform "all,ascii"
176+
// Output: stories/The Quick Brown Fox!.txt
177+
```
178+
179+
```
180+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,trimsuffix=.txt"
181+
// Output: stories/The Quick Brown Fox!
182+
```
183+
184+
```
185+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,prefix=OLD_"
186+
// Output: OLD_stories/OLD_The Quick Brown Fox!.txt
187+
```
188+
189+
```
190+
rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,charmap=ISO-8859-7"
191+
// Output: stories/The Quick Brown _ Fox Went to the Caf_!.txt
192+
```
193+
194+
```
195+
rclone convmv "stories/The Quick Brown Fox: A Memoir [draft].txt" --name-transform "all,encoder=Colon,SquareBracket"
196+
// Output: stories/The Quick Brown Fox: A Memoir [draft].txt
197+
```
198+
199+
```
200+
rclone convmv "stories/The Quick Brown 🦊 Fox Went to the Café!.txt" --name-transform "all,truncate=21"
201+
// Output: stories/The Quick Brown 🦊 Fox
202+
```
203+
204+
```
205+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,command=echo"
206+
// Output: stories/The Quick Brown Fox!.txt
207+
```
208+
209+
```
210+
rclone convmv "stories/The Quick Brown Fox!" --name-transform "date=-{YYYYMMDD}"
211+
// Output: stories/The Quick Brown Fox!-20250618
212+
```
213+
214+
```
215+
rclone convmv "stories/The Quick Brown Fox!" --name-transform "date=-{macfriendlytime}"
216+
// Output: stories/The Quick Brown Fox!-2025-06-18 0148PM
217+
```
218+
219+
```
220+
rclone convmv "stories/The Quick Brown Fox!.txt" --name-transform "all,regex=[\\.\\w]/ab"
221+
// Output: ababababababab/ababab ababababab ababababab ababab!abababab
222+
```
223+
224+

0 commit comments

Comments
 (0)