Skip to content

Commit

Permalink
Implement customizable formatting (#43)
Browse files Browse the repository at this point in the history
* Update standardPackages for Go 1.17.5

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>

* Update Go version

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>

* Implement configurable Section based formatting logic. The CLI has been built with Cobra in a backwards compatible manner.

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>

* Add tests for file reformatting

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>

* Add test for skipping over malformed files

Signed-off-by: Norman Gehrsitz <norman.gehrsitz@gmx.de>
  • Loading branch information
ngehrsitz authored Jan 29, 2022
1 parent c86b884 commit 73cde23
Show file tree
Hide file tree
Showing 104 changed files with 3,316 additions and 531 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 Standard --Section Default --Section "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
26 changes: 26 additions & 0 deletions cmd/gci/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package gci

import (
"strings"

"github.com/spf13/cobra"
)

func subCommandOrGoFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var commandAliases []string
for _, subCmd := range cmd.Commands() {
commandAliases = append(commandAliases, subCmd.Name())
commandAliases = append(commandAliases, subCmd.Aliases...)
}
for _, subCmdStr := range commandAliases {
if strings.HasPrefix(subCmdStr, toComplete) {
// completion for commands is already provided by cobra
// do not return file completion
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
}
return goFileCompletion(cmd, args, toComplete)
}
func goFileCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"go"}, cobra.ShellCompDirectiveFilterFileExt
}
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)
}
54 changes: 54 additions & 0 deletions cmd/gci/gcicommand.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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"
)

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: goFileCompletion,
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: subCommandOrGoFileCompletion,
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)
}
Loading

0 comments on commit 73cde23

Please sign in to comment.