Skip to content

Commit

Permalink
Implement configurable Section based formatting logic. The CLI has be…
Browse files Browse the repository at this point in the history
…en built with Cobra in a backwards compatible manner.

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>
  • Loading branch information
ngehrsitz committed Jan 26, 2022
1 parent a1b1064 commit d632c61
Show file tree
Hide file tree
Showing 55 changed files with 2,865 additions and 543 deletions.
137 changes: 73 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# GCI

GCI, a tool that control golang package import order and make it always deterministic.
GCI, a tool that controls golang package import order and makes it always deterministic.

It handles empty lines more smartly than `goimport` does.
The desired output format is highly configurable and allows for more custom formatting than `goimport` does.

## Download

Expand All @@ -11,19 +11,81 @@ $ go get github.com/daixiang0/gci
```

## Usage
GCI supports three modes of operation
```shell
$ gci print -h
Print outputs the formatted file. If you want to apply the changes to a file use write instead!

Usage:
gci print path... [flags]

Aliases:
print, output

Flags:
--NoInlineComments Drops inline comments while formatting
--NoPrefixComments Drops comment lines above an import statement while formatting
-s, --Section strings Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). A section can contain a Prefix and a Suffix section which is delimited by ":". These sections can be used for formatting and will only be rendered if the main section contains an entry.
Comment(your text here) | CommentLine(your text here) - Prints the specified indented comment
Def | Default - Contains all imports that could not be matched to another section type
NL | NewLine - Prints an empty line
Prefix(gitlab.com/myorg) | pkgPrefix(gitlab.com/myorg) - Groups all imports with the specified Prefix. Imports will be matched to the longest Prefix.
Std | Standard - Captures all standard packages if they do not match another section
(default [Standard,Default])
-x, --SectionSeparator strings SectionSeparators are inserted between Sections (default [NewLine])
-h, --help help for print
```
```shell
$ gci write -h
Write modifies the specified files in-place

Usage:
gci write path... [flags]

Aliases:
write, overwrite

Flags:
--NoInlineComments Drops inline comments while formatting
--NoPrefixComments Drops comment lines above an import statement while formatting
-s, --Section strings Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). A section can contain a Prefix and a Suffix section which is delimited by ":". These sections can be used for formatting and will only be rendered if the main section contains an entry.
Comment(your text here) | CommentLine(your text here) - Prints the specified indented comment
Def | Default - Contains all imports that could not be matched to another section type
NL | NewLine - Prints an empty line
Prefix(gitlab.com/myorg) | pkgPrefix(gitlab.com/myorg) - Groups all imports with the specified Prefix. Imports will be matched to the longest Prefix.
Std | Standard - Captures all standard packages if they do not match another section
(default [Standard,Default])
-x, --SectionSeparator strings SectionSeparators are inserted between Sections (default [NewLine])
-h, --help help for write
```
```shell
$ gci -h
usage: gci [flags] [path ...]
-d display diffs instead of rewriting files
-local string
put imports beginning with this string after 3rd-party packages, only support one string
-w write result to (source) file instead of stdout
$ gci diff -h
Diff prints a patch in the style of the diff tool that contains the required changes to the file to make it adhere to the specified formatting.

Usage:
gci diff path... [flags]

Flags:
--NoInlineComments Drops inline comments while formatting
--NoPrefixComments Drops comment lines above an import statement while formatting
-s, --Section strings Sections define how inputs will be processed. Section names are case-insensitive and may contain parameters in (). A section can contain a Prefix and a Suffix section which is delimited by ":". These sections can be used for formatting and will only be rendered if the main section contains an entry.
Comment(your text here) | CommentLine(your text here) - Prints the specified indented comment
Def | Default - Contains all imports that could not be matched to another section type
NL | NewLine - Prints an empty line
Prefix(gitlab.com/myorg) | pkgPrefix(gitlab.com/myorg) - Groups all imports with the specified Prefix. Imports will be matched to the longest Prefix.
Std | Standard - Captures all standard packages if they do not match another section
(default [Standard,Default])
-x, --SectionSeparator strings SectionSeparators are inserted between Sections (default [NewLine])
-d, --debug Enables debug output from the formatter
-h, --help help for diff
```
Support for the old CLI style is still present if you do not specify the subcommands. The only difference is that `--local` requires two dashes now.
## Examples
Run `gci -w -local github.com/daixiang0/gci main.go` and you will handle following cases.
Run `gci write --Section std --Section nl:default --Section "NewLine:Prefix(github.com/daixiang0/gci)" main.go` and you will handle following cases.
### simple case
Expand Down Expand Up @@ -58,76 +120,23 @@ package main
import (
"fmt"
go "github.com/golang"
"github.com/daixiang0"
)
```

to

```go
package main
import (
"fmt"

go "github.com/golang"

"github.com/daixiang0/gci"
)
```

### with comment and alias

```go
package main
import (
"fmt"
_ "github.com/golang" // golang
"github.com/daixiang0"
)
```

to

```go
package main
import (
"fmt"

// golang
_ "github.com/golang"

"github.com/daixiang0/gci"
)
```
### with above comment and alias

```go
package main
import (
"fmt"
// golang
_ "github.com/golang"
"github.com/daixiang0"
)
```

to
```go
package main
import (
"fmt"
// golang
_ "github.com/golang"
go "github.com/golang"
"github.com/daixiang0/gci"
)
```
## TODO
- Support multi-3rd-party packages
- Support multiple lines of comment in import block
- Add testcases
- Add more testcases
16 changes: 16 additions & 0 deletions cmd/gci/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gci

import (
"github.com/daixiang0/gci/pkg/gci"
)

// diffCmd represents the diff command
func (e *Executor) initDiff() {
e.newGciCommand(
"diff path...",
"Prints a git style diff to STDOUT",
"Diff prints a patch in the style of the diff tool that contains the required changes to the file to make it adhere to the specified formatting.",
[]string{},
true,
gci.DiffFormattedFiles)
}
58 changes: 58 additions & 0 deletions cmd/gci/gcicommand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gci

import (
"fmt"

"github.com/daixiang0/gci/pkg/configuration"
"github.com/daixiang0/gci/pkg/constants"
"github.com/daixiang0/gci/pkg/gci"
sectionsPkg "github.com/daixiang0/gci/pkg/gci/sections"

"github.com/spf13/cobra"
)

func goFileArg(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"go"}, cobra.ShellCompDirectiveFilterFileExt
}

type processingFunc = func(args []string, gciCfg gci.GciConfiguration) error

func (e *Executor) newGciCommand(use, short, long string, aliases []string, stdInSupport bool, processingFunc processingFunc) *cobra.Command {
var noInlineComments, noPrefixComments, debug *bool
var sectionStrings, sectionSeparatorStrings *[]string
cmd := cobra.Command{
Use: use,
Aliases: aliases,
Short: short,
Long: long,
ValidArgsFunction: goFileArg,
RunE: func(cmd *cobra.Command, args []string) error {
fmtCfg := configuration.FormatterConfiguration{*noInlineComments, *noPrefixComments, *debug}
gciCfg, err := gci.GciStringConfiguration{fmtCfg, *sectionStrings, *sectionSeparatorStrings}.Parse()
if err != nil {
return err
}
return processingFunc(args, *gciCfg)
},
}
if !stdInSupport {
cmd.Args = cobra.MinimumNArgs(1)
}

// register command as subcommand
e.rootCmd.AddCommand(&cmd)

sectionHelp := "Sections define how inputs will be processed. " +
"Section names are case-insensitive and may contain parameters in (). " +
fmt.Sprintf("A section can contain a Prefix and a Suffix section which is delimited by %q. ", constants.SectionSeparator) +
"These sections can be used for formatting and will only be rendered if the main section contains an entry." +
"\n" +
sectionsPkg.SectionParserInst.SectionHelpTexts()
// add flags
debug = cmd.Flags().BoolP("debug", "d", false, "Enables debug output from the formatter")
noInlineComments = cmd.Flags().Bool("NoInlineComments", false, "Drops inline comments while formatting")
noPrefixComments = cmd.Flags().Bool("NoPrefixComments", false, "Drops comment lines above an import statement while formatting")
sectionStrings = cmd.Flags().StringSliceP("Section", "s", gci.DefaultSections().String(), sectionHelp)
sectionSeparatorStrings = cmd.Flags().StringSliceP("SectionSeparator", "x", gci.DefaultSectionSeparators().String(), "SectionSeparators are inserted between Sections")
return &cmd
}
16 changes: 16 additions & 0 deletions cmd/gci/print.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gci

import (
"github.com/daixiang0/gci/pkg/gci"
)

// printCmd represents the print command
func (e *Executor) initPrint() {
e.newGciCommand(
"print path...",
"Outputs the formatted file to STDOUT",
"Print outputs the formatted file. If you want to apply the changes to a file use write instead!",
[]string{"output"},
true,
gci.PrintFormattedFiles)
}
67 changes: 67 additions & 0 deletions cmd/gci/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package gci

import (
"fmt"
"os"

"github.com/daixiang0/gci/pkg/configuration"
"github.com/daixiang0/gci/pkg/gci"

"github.com/spf13/cobra"
)

type Executor struct {
rootCmd *cobra.Command
diffMode *bool
writeMode *bool
localFlags *[]string
}

func NewExecutor(version string) *Executor {
e := Executor{}
rootCmd := cobra.Command{
Use: "gci [-diff | -write] [-local localPackageURLs] path...",
Short: "Gci controls golang package import order and makes it always deterministic",
Long: "Gci enables automatic formatting of imports in a deterministic manner" +
"\n" +
"If you want to integrate this as part of your CI take a look at golangci-lint.",
ValidArgsFunction: goFileArg,
Args: cobra.MinimumNArgs(1),
Version: version,
RunE: e.runInCompatibilityMode,
}
e.rootCmd = &rootCmd
e.diffMode = rootCmd.Flags().BoolP("diff", "d", false, "display diffs instead of rewriting files")
e.writeMode = rootCmd.Flags().BoolP("write", "w", false, "write result to (source) file instead of stdout")
e.localFlags = rootCmd.Flags().StringSliceP("local", "l", []string{}, "put imports beginning with this string after 3rd-party packages, separate imports by comma")
e.initDiff()
e.initPrint()
e.initWrite()
return &e
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func (e *Executor) Execute() error {
return e.rootCmd.Execute()
}

func (e *Executor) runInCompatibilityMode(cmd *cobra.Command, args []string) error {
// Workaround since the Deprecation message in Cobra can not be printed to STDERR
_, _ = fmt.Fprintln(os.Stderr, "Using the old parameters is deprecated, please use the named subcommands!")

if *e.writeMode && *e.diffMode {
return fmt.Errorf("diff and write must not be specified at the same time")
}
// generate section specification from old localFlags format
sections := gci.LocalFlagsToSections(*e.localFlags)
sectionSeparators := gci.DefaultSectionSeparators()
cfg := gci.GciConfiguration{configuration.FormatterConfiguration{false, false, false}, sections, sectionSeparators}
if *e.writeMode {
return gci.WriteFormattedFiles(args, cfg)
}
if *e.diffMode {
return gci.DiffFormattedFiles(args, cfg)
}
return gci.PrintFormattedFiles(args, cfg)
}
16 changes: 16 additions & 0 deletions cmd/gci/write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gci

import (
"github.com/daixiang0/gci/pkg/gci"
)

// writeCmd represents the write command
func (e *Executor) initWrite() {
e.newGciCommand(
"write path...",
"Formats the specified files in-place",
"Write modifies the specified files in-place",
[]string{"overwrite"},
false,
gci.WriteFormattedFiles)
}
18 changes: 16 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,23 @@ module github.com/daixiang0/gci

go 1.17

require golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394
require (
github.com/hexops/gotextdiff v1.0.3
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/tools v0.1.5
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)

require (
golang.org/x/mod v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kr/pretty v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.5.0 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)
Loading

0 comments on commit d632c61

Please sign in to comment.