Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e41532b
changed from linear extension search to constant time lookup
Moderrek Jul 4, 2026
e4fc78a
updated version and added version, author, program_name constants
Moderrek Jul 4, 2026
15a18fa
explicitly ignoring error
Moderrek Jul 4, 2026
eea1226
changed from filepath walk to walkdir, major refactor
Moderrek Jul 4, 2026
eb3f03d
changed from filepath walk to walkdir, major refactor
Moderrek Jul 4, 2026
c46422a
improved file analysis with worker pool and channel for concurrency, …
Moderrek Jul 4, 2026
a2e0c8a
added jobs flag to specify number of concurrent jobs and updated outp…
Moderrek Jul 4, 2026
83f0e82
removed concurrent-map dependency
Moderrek Jul 4, 2026
7a49344
needToAnalyze accepts filename and improved output formatting
Moderrek Jul 4, 2026
cbaf2c7
added progress reporting and atomic counters for files found and proc…
Moderrek Jul 4, 2026
02d63c0
refactor scanner to use bufio.Reader for improved line reading and ha…
Moderrek Jul 4, 2026
27ffd8d
improved file analysis logic and added verbose logging
Moderrek Jul 5, 2026
6bc66e3
updated README
Moderrek Jul 6, 2026
fa33cf6
simplified analysis and worker
Moderrek Jul 6, 2026
a8b5242
improved error handling in analysis
Moderrek Jul 6, 2026
3e8893b
refactor: standardize naming conventions and improve README instructions
Moderrek Jul 29, 2026
ee5aebd
feat: enhance command-line options and update README for v1.3.0 release
Moderrek Jul 29, 2026
dc06219
update dependencies
Moderrek Jul 29, 2026
a62864c
remove unnecessary steps from CI
Moderrek Jul 29, 2026
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
14 changes: 2 additions & 12 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,15 @@ jobs:
with:
go-version: '1.22.4'

- name: Check formatting
run: go fmt ./...

- name: Run vet
run: go vet ./...

- name: Verify go.mod
run: go mod tidy && git diff --exit-code

- name: Build
run: go build -v ./...

- name: Test
run: go test -v -cover ./...

release:
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