Skip to content

Commit 0cd51c8

Browse files
authored
Merge pull request #27 from arran4/cli-flexibility-5294150273599874838
feat(cli): enhance CLI flexibility and fix option precedence
2 parents 889ae0d + e136f64 commit 0cd51c8

22 files changed

Lines changed: 1067 additions & 79 deletions

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,44 @@ fmt.Println(strings2.ToKebabCase(words, strings2.OptionDelimiter("|")))
8181
fmt.Println(strings2.ToSnakeCase(words, strings2.OptionCaseMode(strings2.CMScreaming)))
8282
```
8383

84+
### CLI Mode
85+
86+
The library also provides a command-line interface that exposes all these options, ensuring that the CLI mode has as much flexibility as the code (without being obligated to use smart defaults).
87+
88+
```bash
89+
strings2 camel "hello world"
90+
# Result: helloWorld
91+
92+
strings2 snake --screaming "hello world"
93+
# Result: HELLO_WORLD
94+
95+
strings2 kebab --first-upper "hello world"
96+
# Result: Hello-world
97+
```
98+
99+
You can pipe input into the CLI as well:
100+
```bash
101+
echo "hello world" | strings2 pascal
102+
# Result: HelloWorld
103+
```
104+
105+
Available flags across commands:
106+
- `--delimiter`, `-d` (string): Override the delimiter
107+
- `--screaming`, `-S`: Enforce uppercase formatting
108+
- `--whispering`, `-w`: Enforce lowercase formatting
109+
- `--first-upper`, `-U`: Capitalize the first letter
110+
- `--first-lower`, `-l`: Lowercase the first letter
111+
- `--mix-case-support`, `-m`: Enable splitting of mixed case words
112+
- `--no-smart-acronyms`: Disable acronym preservation
113+
- `--number-splitting`: Enable letter-digit boundary splitting
114+
```
115+
84116
Options are composable so multiple behaviours can be applied at once. See the documentation in `types.go` for details on further options.
85117
118+
## TODO
119+
120+
- Support slices for flags when the gosubc version supports it.
121+
86122
## License
87123
88124
This project is licensed under the BSD 3-Clause License - see the [LICENSE](LICENSE) file for details.

cli/main.go

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"github.com/arran4/strings2"
1010
)
1111

12-
func process(input string, output string, args []string, fn func(string, ...any) (string, error)) {
12+
func process(input string, output string, args []string, fn func(string, ...any) (string, error), opts ...any) {
1313
var in io.Reader
1414
if input == "-" {
1515
in = os.Stdin
@@ -33,7 +33,7 @@ func process(input string, output string, args []string, fn func(string, ...any)
3333
os.Exit(1)
3434
}
3535

36-
res, err := fn(string(b))
36+
res, err := fn(string(b), opts...)
3737
if err != nil {
3838
fmt.Fprintf(os.Stderr, "Error processing: %v\n", err)
3939
os.Exit(1)
@@ -55,15 +55,57 @@ func process(input string, output string, args []string, fn func(string, ...any)
5555
fmt.Fprintln(out, res)
5656
}
5757

58+
func buildOpts(delimiter string, screaming bool, whispering bool, firstUpper bool, firstLower bool, mixCaseSupport bool, noSmartAcronyms bool, numberSplitting bool, strict bool) []any {
59+
var opts []any
60+
if delimiter != "" {
61+
opts = append(opts, strings2.OptionDelimiter(delimiter))
62+
}
63+
if screaming {
64+
opts = append(opts, strings2.OptionCaseMode(strings2.CMScreaming))
65+
}
66+
if whispering {
67+
opts = append(opts, strings2.OptionCaseMode(strings2.CMWhispering))
68+
}
69+
if firstUpper {
70+
opts = append(opts, strings2.OptionFirstUpper())
71+
}
72+
if firstLower {
73+
opts = append(opts, strings2.OptionFirstLower())
74+
}
75+
if mixCaseSupport {
76+
opts = append(opts, strings2.OptionMixCaseSupport())
77+
}
78+
if noSmartAcronyms {
79+
opts = append(opts, strings2.WithSmartAcronyms(false))
80+
}
81+
if numberSplitting {
82+
opts = append(opts, strings2.WithNumberSplitting(true))
83+
}
84+
if strict {
85+
opts = append(opts, strings2.OptionStrict())
86+
}
87+
return opts
88+
}
89+
5890
// Camel is a subcommand `strings2 camel`
5991
//
6092
// Flags:
6193
//
6294
// input: -i --input (default: "") Input file or - for stdin
6395
// output: -o --output (default: "") Output file or - for stdout
96+
// delimiter: -d --delimiter (default: "") Delimiter
97+
// screaming: -S --screaming (default: false) Screaming mode
98+
// whispering: -w --whispering (default: false) Whispering mode
99+
// firstUpper: -U --first-upper (default: false) First char upper
100+
// firstLower: -l --first-lower (default: false) First char lower
101+
// mixCaseSupport: -m --mix-case-support (default: false) Mix case support
102+
// noSmartAcronyms: --no-smart-acronyms (default: false) Disable smart acronyms
103+
// numberSplitting: --number-splitting (default: false) Enable number splitting
104+
// strict: --strict (default: false) Strict UTF8 mode
64105
// args: ... String to convert if file/stdin not provided
65-
func Camel(input string, output string, args ...string) {
66-
process(input, output, args, strings2.ToCamel)
106+
func Camel(input string, output string, delimiter string, screaming bool, whispering bool, firstUpper bool, firstLower bool, mixCaseSupport bool, noSmartAcronyms bool, numberSplitting bool, strict bool, args ...string) {
107+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
108+
process(input, output, args, strings2.ToCamel, opts...)
67109
}
68110

69111
// Snake is a subcommand `strings2 snake`
@@ -72,9 +114,19 @@ func Camel(input string, output string, args ...string) {
72114
//
73115
// input: -i --input (default: "") Input file or - for stdin
74116
// output: -o --output (default: "") Output file or - for stdout
117+
// delimiter: -d --delimiter (default: "") Delimiter
118+
// screaming: -S --screaming (default: false) Screaming mode
119+
// whispering: -w --whispering (default: false) Whispering mode
120+
// firstUpper: -U --first-upper (default: false) First char upper
121+
// firstLower: -l --first-lower (default: false) First char lower
122+
// mixCaseSupport: -m --mix-case-support (default: false) Mix case support
123+
// noSmartAcronyms: --no-smart-acronyms (default: false) Disable smart acronyms
124+
// numberSplitting: --number-splitting (default: false) Enable number splitting
125+
// strict: --strict (default: false) Strict UTF8 mode
75126
// args: ... String to convert if file/stdin not provided
76-
func Snake(input string, output string, args ...string) {
77-
process(input, output, args, strings2.ToSnake)
127+
func Snake(input string, output string, delimiter string, screaming bool, whispering bool, firstUpper bool, firstLower bool, mixCaseSupport bool, noSmartAcronyms bool, numberSplitting bool, strict bool, args ...string) {
128+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
129+
process(input, output, args, strings2.ToSnake, opts...)
78130
}
79131

80132
// Kebab is a subcommand `strings2 kebab`
@@ -83,9 +135,19 @@ func Snake(input string, output string, args ...string) {
83135
//
84136
// input: -i --input (default: "") Input file or - for stdin
85137
// output: -o --output (default: "") Output file or - for stdout
138+
// delimiter: -d --delimiter (default: "") Delimiter
139+
// screaming: -S --screaming (default: false) Screaming mode
140+
// whispering: -w --whispering (default: false) Whispering mode
141+
// firstUpper: -U --first-upper (default: false) First char upper
142+
// firstLower: -l --first-lower (default: false) First char lower
143+
// mixCaseSupport: -m --mix-case-support (default: false) Mix case support
144+
// noSmartAcronyms: --no-smart-acronyms (default: false) Disable smart acronyms
145+
// numberSplitting: --number-splitting (default: false) Enable number splitting
146+
// strict: --strict (default: false) Strict UTF8 mode
86147
// args: ... String to convert if file/stdin not provided
87-
func Kebab(input string, output string, args ...string) {
88-
process(input, output, args, strings2.ToKebab)
148+
func Kebab(input string, output string, delimiter string, screaming bool, whispering bool, firstUpper bool, firstLower bool, mixCaseSupport bool, noSmartAcronyms bool, numberSplitting bool, strict bool, args ...string) {
149+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
150+
process(input, output, args, strings2.ToKebab, opts...)
89151
}
90152

91153
// Pascal is a subcommand `strings2 pascal`
@@ -94,7 +156,17 @@ func Kebab(input string, output string, args ...string) {
94156
//
95157
// input: -i --input (default: "") Input file or - for stdin
96158
// output: -o --output (default: "") Output file or - for stdout
159+
// delimiter: -d --delimiter (default: "") Delimiter
160+
// screaming: -S --screaming (default: false) Screaming mode
161+
// whispering: -w --whispering (default: false) Whispering mode
162+
// firstUpper: -U --first-upper (default: false) First char upper
163+
// firstLower: -l --first-lower (default: false) First char lower
164+
// mixCaseSupport: -m --mix-case-support (default: false) Mix case support
165+
// noSmartAcronyms: --no-smart-acronyms (default: false) Disable smart acronyms
166+
// numberSplitting: --number-splitting (default: false) Enable number splitting
167+
// strict: --strict (default: false) Strict UTF8 mode
97168
// args: ... String to convert if file/stdin not provided
98-
func Pascal(input string, output string, args ...string) {
99-
process(input, output, args, strings2.ToPascal)
169+
func Pascal(input string, output string, delimiter string, screaming bool, whispering bool, firstUpper bool, firstLower bool, mixCaseSupport bool, noSmartAcronyms bool, numberSplitting bool, strict bool, args ...string) {
170+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
171+
process(input, output, args, strings2.ToPascal, opts...)
100172
}

cmd/agents.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<!-- Code generated by github.com/arran4/go-subcommand/cmd/gosubc. DO NOT EDIT. -->
2+
All code under /cmd is generated code, do not place files here.
3+
4+
# Go Subcommand Information
5+
6+
Go Subcommand (`gosubc`) generates subcommand code for command-line interfaces (CLIs) in Go from source code comments.
7+
8+
## Key Features
9+
10+
- **Convention over Configuration:** Define your CLI structure with simple, intuitive code comments.
11+
- **Zero Dependencies:** The generated code is self-contained and doesn't require any external libraries.
12+
- **Automatic Code Generation:** `gosubc` parses your Go files and generates a complete, ready-to-use CLI.
13+
14+
## Installation
15+
16+
`gosubc` is a standalone tool and should not be added as a dependency in your `go.mod`. Install it using:
17+
18+
```bash
19+
go install github.com/arran4/go-subcommand/cmd/gosubc@latest
20+
```
21+
22+
## How it works
23+
24+
1. **Define Your Commands**: Create a Go file and define a function that will serve as your command. Add a comment above the function.
25+
2. **Generate**: Run `gosubc generate` (or via `go generate`).
26+
3. **Result**: `gosubc` creates a `cmd/<app-name>` directory containing the generated CLI code.
27+
28+
## Comment Syntax
29+
30+
### Command Definition
31+
32+
```go
33+
// FuncName is a subcommand 'root-cmd parent child'
34+
func FuncName() {}
35+
```
36+
37+
### Flags
38+
39+
Use a `Flags:` block or inline comments. Adhere to Go formatting.
40+
41+
```go
42+
// FuncName is a subcommand 'root-cmd parent child'
43+
//
44+
// Flags:
45+
//
46+
// username: --username -u (default: "guest") The user to greet
47+
// count: --count -c (default: 1) Number of times
48+
func FuncName(username string, count int) {}
49+
```
50+
51+
### Positional Arguments
52+
53+
Map positional arguments using `@N`.
54+
55+
```go
56+
// Greet is a subcommand 'app greet'
57+
//
58+
// Flags:
59+
//
60+
// name: @1 The name to greet
61+
func Greet(name string) {}
62+
```
63+
64+
### Variadic Arguments
65+
66+
Map remaining arguments using `...`.
67+
68+
```go
69+
// Process is a subcommand 'app process'
70+
//
71+
// Flags:
72+
//
73+
// files: ... List of files
74+
func Process(files ...string) {}
75+
```
76+
77+
## Important Note
78+
79+
Do not edit files in this directory directly if you can avoid it. They are overwritten on every generation. Modify the source code comments instead.

cmd/errors.go

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)