Skip to content

Commit ae02043

Browse files
feat(cli): enhance CLI flexibility and fix option precedence
- Exposed library configuration options (delimiter, case mode, strictness, etc.) as CLI flags in `cli/main.go`. - Regenerated CLI command implementations using `gosubc` to support the new flags. - Refactored `strings2` helper functions (`ToCamel`, `ToSnake`, etc.) in `permutations.go` and `types.go` to apply default options before user options, allowing users to override defaults. - Updated `process` function in `cli/main.go` to pass parsed options to `strings2` functions as variadic arguments. - Added a section to `README.md` about the CLI mode.
1 parent b41606b commit ae02043

6 files changed

Lines changed: 337 additions & 5 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ 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+
# Screaming snake case
90+
strings2 snake --screaming "hello world"
91+
```
92+
8493
Options are composable so multiple behaviours can be applied at once. See the documentation in `types.go` for details on further options.
8594

8695
## License

README.md.orig

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# strings2
2+
3+
[![Test Status](https://github.com/arran4/strings2/actions/workflows/test.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/test.yml)
4+
[![Vet Status](https://github.com/arran4/strings2/actions/workflows/vet.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/vet.yml)
5+
[![Lint Status](https://github.com/arran4/strings2/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/golangci-lint.yml)
6+
[![Fmt Status](https://github.com/arran4/strings2/actions/workflows/fmt.yml/badge.svg)](https://github.com/arran4/strings2/actions/workflows/fmt.yml)
7+
[![Go Reference](https://pkg.go.dev/badge/github.com/arran4/strings2.svg)](https://pkg.go.dev/github.com/arran4/strings2)
8+
9+
strings2 provides utilities for converting slices of words into various casing conventions. It is intended to supplement Go's standard library `strings` package with helpers for creating formats such as `camelCase`, `PascalCase`, `snake_case` and `kebab-case`.
10+
11+
## Installation
12+
13+
```
14+
go get github.com/arran4/strings2
15+
```
16+
17+
Add the module to your project and import it:
18+
19+
```go
20+
import "github.com/arran4/strings2"
21+
```
22+
23+
## Usage
24+
25+
Words must implement `fmt.Stringer`. The package defines several helper types which satisfy this interface:
26+
27+
```go
28+
words := []strings2.Word{
29+
strings2.SingleCaseWord("hello"),
30+
strings2.SingleCaseWord("world"),
31+
}
32+
```
33+
34+
### Parsing
35+
36+
The library includes a robust parser to convert strings into typed `Word` objects, distinguishing between acronyms, casing, and delimiters.
37+
38+
```go
39+
// Auto-detect format and parse
40+
words, err := strings2.Parse("helloWorld")
41+
// Result: [SingleCaseWord("hello"), FirstUpperCaseWord("World")]
42+
43+
// Parse specific format
44+
words = strings2.ParseSnakeCase("hello_world")
45+
46+
// Configure parser
47+
words, err = strings2.Parse("N.E.W. World", strings2.ParserSmartAcronyms(true))
48+
```
49+
50+
### Case Conversion Functions
51+
52+
```go
53+
strings2.ToCamelCase(words) // "helloWorld"
54+
strings2.ToPascalCase(words) // "HelloWorld"
55+
strings2.ToKebabCase(words) // "hello-world"
56+
strings2.ToSnakeCase(words) // "hello_world"
57+
```
58+
59+
### Customising Formatting
60+
61+
Behaviour can be tuned with options passed to each function. Some commonly used options include:
62+
63+
- `OptionDelimiter(string)` – change the delimiter used between words.
64+
- `OptionCaseMode(CaseMode)` – set the case transformation mode. Modes include:
65+
- `CMVerbatim`
66+
- `CMFirstTitle`
67+
- `CMAllTitle`
68+
- `CMFirstLower`
69+
- `CMWhispering`
70+
- `CMScreaming`
71+
- `OptionFirstUpper()` – force the result to start with an uppercase letter.
72+
- `OptionFirstLower()` – force the result to start with a lowercase letter.
73+
74+
Examples:
75+
76+
```go
77+
// Custom delimiter
78+
fmt.Println(strings2.ToKebabCase(words, strings2.OptionDelimiter("|")))
79+
80+
// Screaming snake case
81+
fmt.Println(strings2.ToSnakeCase(words, strings2.OptionCaseMode(strings2.CMScreaming)))
82+
```
83+
84+
Options are composable so multiple behaviours can be applied at once. See the documentation in `types.go` for details on further options.
85+
86+
## License
87+
88+
This project is licensed under the BSD 3-Clause License - see the [LICENSE](LICENSE) file for details.

cli/main.go

Lines changed: 5 additions & 5 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, opts []any, 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
@@ -105,7 +105,7 @@ func buildOpts(delimiter string, screaming bool, whispering bool, firstUpper boo
105105
// args: ... String to convert if file/stdin not provided
106106
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) {
107107
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
108-
process(input, output, args, opts, strings2.ToCamel)
108+
process(input, output, args, strings2.ToCamel, opts...)
109109
}
110110

111111
// Snake is a subcommand `strings2 snake`
@@ -126,7 +126,7 @@ func Camel(input string, output string, delimiter string, screaming bool, whispe
126126
// args: ... String to convert if file/stdin not provided
127127
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) {
128128
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
129-
process(input, output, args, opts, strings2.ToSnake)
129+
process(input, output, args, strings2.ToSnake, opts...)
130130
}
131131

132132
// Kebab is a subcommand `strings2 kebab`
@@ -147,7 +147,7 @@ func Snake(input string, output string, delimiter string, screaming bool, whispe
147147
// args: ... String to convert if file/stdin not provided
148148
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) {
149149
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
150-
process(input, output, args, opts, strings2.ToKebab)
150+
process(input, output, args, strings2.ToKebab, opts...)
151151
}
152152

153153
// Pascal is a subcommand `strings2 pascal`
@@ -168,5 +168,5 @@ func Kebab(input string, output string, delimiter string, screaming bool, whispe
168168
// args: ... String to convert if file/stdin not provided
169169
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) {
170170
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
171-
process(input, output, args, opts, strings2.ToPascal)
171+
process(input, output, args, strings2.ToPascal, opts...)
172172
}

cli/main.go.orig

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"strings"
8+
9+
"github.com/arran4/strings2"
10+
)
11+
12+
func process(input string, output string, args []string, opts []any, fn func(string, ...any) (string, error)) {
13+
var in io.Reader
14+
if input == "-" {
15+
in = os.Stdin
16+
} else if input != "" {
17+
f, err := os.Open(input)
18+
if err != nil {
19+
fmt.Fprintf(os.Stderr, "Error opening input file: %v\n", err)
20+
os.Exit(1)
21+
}
22+
defer f.Close()
23+
in = f
24+
} else if len(args) > 0 {
25+
in = strings.NewReader(strings.Join(args, " "))
26+
} else {
27+
in = os.Stdin
28+
}
29+
30+
b, err := io.ReadAll(in)
31+
if err != nil {
32+
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
33+
os.Exit(1)
34+
}
35+
36+
res, err := fn(string(b), opts...)
37+
if err != nil {
38+
fmt.Fprintf(os.Stderr, "Error processing: %v\n", err)
39+
os.Exit(1)
40+
}
41+
42+
var out io.Writer
43+
if output == "-" || output == "" {
44+
out = os.Stdout
45+
} else {
46+
f, err := os.Create(output)
47+
if err != nil {
48+
fmt.Fprintf(os.Stderr, "Error creating output file: %v\n", err)
49+
os.Exit(1)
50+
}
51+
defer f.Close()
52+
out = f
53+
}
54+
55+
fmt.Fprintln(out, res)
56+
}
57+
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+
90+
// Camel is a subcommand `strings2 camel`
91+
//
92+
// Flags:
93+
//
94+
// input: -i --input (default: "") Input file or - for stdin
95+
// 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
105+
// args: ... String to convert if file/stdin not provided
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, opts, strings2.ToCamel)
109+
}
110+
111+
// Snake is a subcommand `strings2 snake`
112+
//
113+
// Flags:
114+
//
115+
// input: -i --input (default: "") Input file or - for stdin
116+
// 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
126+
// args: ... String to convert if file/stdin not provided
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, opts, strings2.ToSnake)
130+
}
131+
132+
// Kebab is a subcommand `strings2 kebab`
133+
//
134+
// Flags:
135+
//
136+
// input: -i --input (default: "") Input file or - for stdin
137+
// 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
147+
// args: ... String to convert if file/stdin not provided
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, opts, strings2.ToKebab)
151+
}
152+
153+
// Pascal is a subcommand `strings2 pascal`
154+
//
155+
// Flags:
156+
//
157+
// input: -i --input (default: "") Input file or - for stdin
158+
// 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
168+
// args: ... String to convert if file/stdin not provided
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, opts, strings2.ToPascal)
172+
}

patch.diff

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--- cli/main.go
2+
+++ cli/main.go
3+
@@ -9,7 +9,7 @@
4+
"github.com/arran4/strings2"
5+
)
6+
7+
-func process(input string, output string, args []string, opts []any, fn func(string, ...any) (string, error)) {
8+
+func process(input string, output string, args []string, fn func(string, ...any) (string, error), opts ...any) {
9+
var in io.Reader
10+
if input == "-" {
11+
in = os.Stdin
12+
@@ -83,7 +83,7 @@
13+
// args: ... String to convert if file/stdin not provided
14+
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) {
15+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
16+
- process(input, output, args, opts, strings2.ToCamel)
17+
+ process(input, output, args, strings2.ToCamel, opts...)
18+
}
19+
20+
// Snake is a subcommand `strings2 snake`
21+
@@ -103,7 +103,7 @@
22+
// args: ... String to convert if file/stdin not provided
23+
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) {
24+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
25+
- process(input, output, args, opts, strings2.ToSnake)
26+
+ process(input, output, args, strings2.ToSnake, opts...)
27+
}
28+
29+
// Kebab is a subcommand `strings2 kebab`
30+
@@ -123,7 +123,7 @@
31+
// args: ... String to convert if file/stdin not provided
32+
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) {
33+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
34+
- process(input, output, args, opts, strings2.ToKebab)
35+
+ process(input, output, args, strings2.ToKebab, opts...)
36+
}
37+
38+
// Pascal is a subcommand `strings2 pascal`
39+
@@ -143,6 +143,6 @@
40+
// args: ... String to convert if file/stdin not provided
41+
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) {
42+
opts := buildOpts(delimiter, screaming, whispering, firstUpper, firstLower, mixCaseSupport, noSmartAcronyms, numberSplitting, strict)
43+
- process(input, output, args, opts, strings2.ToPascal)
44+
+ process(input, output, args, strings2.ToPascal, opts...)
45+
}

patch_readme.diff

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--- README.md
2+
+++ README.md
3+
@@ -48,6 +48,15 @@
4+
fmt.Println(strings2.ToSnakeCase(words, strings2.OptionCaseMode(strings2.CMScreaming)))
5+
```
6+
7+
+### CLI Mode
8+
+
9+
+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).
10+
+
11+
+```bash
12+
+# Screaming snake case
13+
+strings2 snake --screaming "hello world"
14+
+```
15+
+
16+
Options are composable so multiple behaviours can be applied at once. See the documentation in `types.go` for details on further options.
17+
18+
## License

0 commit comments

Comments
 (0)