Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ jobs:
needs: build
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
Expand Down
100 changes: 67 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,90 @@

</div>

A concurrent, non-blank line counter for source code directories, written in GO.

A concurrent non-blank line counter for source code directories, written in Go.
It recursively walks a directory, concurrently analyzes files, and reports the number of non-blank lines of code, grouped by file extension.

The tool is designed for performance, utilizing goroutines to process files in parallel.

## Installation

To install the `lines` command-line tool, ensure you have [Go](https://go.dev/doc/install) installed and configured, then run:

```shell
go install github.com/moderrek/lines/cmd/lines@latest
go install github.com/Moderrek/lines/cmd/lines@v1.3.0
```

This will download the source, compile it, and place the `lines` binary in your Go bin directory (`$GOPATH/bin` or `$HOME/go/in`).
This will download the source, compile it, and place the `lines` binary in your Go bin directory (`$GOPATH/bin` or `$HOME/go/bin`). The binary works on Linux and Windows, and the release assets are published for both platforms.

If you want to use the library in another Go module, add it with:

```shell
go get github.com/Moderrek/lines@v1.3.0
```

Then import the package from `pkg/lines`:

```go
import "github.com/Moderrek/lines/pkg/lines"
```

## Usage

The `lines` command accepts the following flags:
The `lines` command accepts the following flags and targets:

```text
Usage: lines [options] [--dir PATH ...] [PATH ...]
```
Usage: lines [options]

You can analyze multiple directories or files in one run by repeating `--dir` or passing positional arguments.

```
Options:
-dir string
The directory to analyze (default ".")
-hidden
Include hidden files and directories in the analysis
-top uint
Show only the top N extensions by line count
-color
Force color output (e.g. when piping)
-no-color
Disable colorized output
Disable color output
-help
Print the help message
-hidden
Allows to analyze hidden files
-jobs uint
Specifies the number of jobs
-json
Output results in JSON format
-top uint
Print the top N extensions
-verbose
Verbose output
-version
Print version information and exit
-help
Show this help message and exit
Print the version
```

```shell
lines --dir ./cmd --dir ./pkg ./README.md
```

### Example

To analyze the directory `~/projects/my-app` and display the top 5 extensions:

```shell
lines --dir ~/projects/my-app --top 5
lines --top 5 ~/projects/my-app
```

To get the output in JSON format, which can be piped to other tools like `jq`:

```shell
lines --dir ~/projects/my-app --json
lines --json ~/projects/my-app
```

Example output (`--json`):
```json
{
".css": 1122,
".go": 15230,
".html": 4357,
".js": 8828,
".mod": 4980
".css": 1122,
".go": 15230,
".html": 4357,
".js": 8828,
".mod": 4980
}
```

Expand All @@ -83,10 +105,6 @@ Example output (`--json`):
The core counting logic is available as a library.
It can be imported into other Go projects.

```go
import "github.com/moderrek/lines/pkg/lines"
```

### Example

```go
Expand All @@ -96,7 +114,7 @@ import (
"fmt"
"log"

"github.com/moderrek/lines/pkg/lines"
"github.com/Moderrek/lines/pkg/lines"
)

func main() {
Expand Down Expand Up @@ -126,14 +144,14 @@ You can customize which directories and file extensions to ignore:
```go
config := lines.Config{
IncludeHidden: false,
IgnoredDirs: []string{"node_modules", "vendor", ".git", "target", "dist"},
IgnoredExtensions: []string{".exe", ".dll", ".jpg", ".png"},
IgnoredDirs: lines.IgnoredDirSet("node_modules", ".git"),
IgnoredExtensions: lines.IgnoredExtensionSet("exe", ".env"),
}
counter := lines.NewCounter(config)
result, err := counter.Run("./src")
```

If `IgnoredDirs` or `IgnoredExtensions` are not provided, the library uses sensible defaults.
If `IgnoredDirs` or `IgnoredExtensions` are not provided, the library uses sensible defaults. The helper functions above are optional, but they make the config easier to read.

## Building from Source

Expand All @@ -151,7 +169,23 @@ If `IgnoredDirs` or `IgnoredExtensions` are not provided, the library uses sensi
```
This will create a `lines` executable in the current directory.

### Releasing

For v1.3.0, the intended distribution flow is:

```shell
go install github.com/Moderrek/lines/cmd/lines@v1.3.0
```

Or, if you are integrating the library into another module:

```shell
go get github.com/Moderrek/lines@v1.3.0
```

The GitHub release pipeline builds binaries for Linux and Windows so users without Go installed can download a ready-made executable.

## License

This project is licensed under the MIT License.
See the [LICENSE](LICENSE) file for details.
See the [LICENSE](./LICENSE) file for details.
25 changes: 21 additions & 4 deletions cmd/lines/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,49 @@ package main
import (
"flag"
"io"
"strings"
)

type cliOptions struct {
dir string
dirs multiStringFlag
version bool
help bool
hidden bool
top uint
noColor bool
color bool
json bool
jobs uint
verbose bool
}

type multiStringFlag []string

func (m *multiStringFlag) String() string {
return strings.Join(*m, ",")
}

func (m *multiStringFlag) Set(value string) error {
*m = append(*m, value)
return nil
}

func parseFlags(stderr io.Writer, args []string) (*cliOptions, *flag.FlagSet, error) {
opts := &cliOptions{}
fs := flag.NewFlagSet("lines", flag.ContinueOnError)
fs := flag.NewFlagSet(ProgramName, flag.ContinueOnError)
fs.SetOutput(stderr)

fs.StringVar(&opts.dir, "dir", ".", "The directory to analyze")
fs.Var(&opts.dirs, "dir", "The directories or files to analyze. Can be repeated.")
fs.BoolVar(&opts.version, "version", false, "Print the version and exit")
fs.BoolVar(&opts.version, "v", false, "Print the version and exit")
fs.BoolVar(&opts.help, "help", false, "Print the help message and exit")
fs.BoolVar(&opts.hidden, "hidden", false, "Allows to analize hidden files")
fs.BoolVar(&opts.hidden, "hidden", false, "Allows to analyze hidden files")
fs.UintVar(&opts.top, "top", 0, "Print the top N extensions")
fs.UintVar(&opts.jobs, "jobs", 0, "Specifies the number of jobs")
fs.BoolVar(&opts.noColor, "no-color", false, "Disable color output")
fs.BoolVar(&opts.color, "color", false, "Force color output (e.g. when piping)")
fs.BoolVar(&opts.json, "json", false, "Output results in JSON format")
fs.BoolVar(&opts.verbose, "verbose", false, "Verbose output")

err := fs.Parse(args[1:])
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions cmd/lines/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package main

const ProgramName = "lines"
const Version = "v1.3.0"
const Author = "Tymon Wozniak @Moderrek"
2 changes: 1 addition & 1 deletion cmd/lines/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

func main() {
if err := run(os.Stdout, os.Stderr, os.Args); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
_, _ = fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
11 changes: 7 additions & 4 deletions cmd/lines/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,27 @@ import (
"io"
"sort"

"github.com/Moderrek/lines/pkg/lines"
"github.com/fatih/color"
"github.com/moderrek/lines/pkg/lines"
)

func printJSONOutput(w io.Writer, result *lines.Result) error {
jsonOutput, err := json.MarshalIndent(result.LinesByExtension, "", " ")
if err != nil {
return fmt.Errorf("error generating JSON: %w", err)
}
fmt.Fprintln(w, string(jsonOutput))
return nil
_, err = fmt.Fprintln(w, string(jsonOutput))
return err
}

func printHumanOutput(w io.Writer, result *lines.Result, opts *cliOptions) {
lineMap := result.LinesByExtension

sortedKeys := make([]string, 0, len(lineMap))
for key := range lineMap {
sortedKeys = append(sortedKeys, key)
}

sort.Slice(sortedKeys, func(i, j int) bool {
return lineMap[sortedKeys[i]] > lineMap[sortedKeys[j]]
})
Expand All @@ -36,13 +38,14 @@ func printHumanOutput(w io.Writer, result *lines.Result, opts *cliOptions) {
if opts.top > 0 && uint(i) >= opts.top {
break
}

linesCount := lineMap[key]
if linesCount == 0 {
continue
}

extColor.Fprintf(w, "%s", key)
fmt.Fprint(w, " ") // Separator
fmt.Fprint(w, "\t")
linesColor.Fprintf(w, "%d\n", linesCount)
}
}
Loading
Loading