-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
219 lines (189 loc) · 6.51 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
// loc - count lines of code in a directory or repo
// BSD 3-Clause License
//
// Copyright (c) 2024, Alex Gaetano Padula
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
// Loc is the main struct for the Loc program
type Loc struct {
TotalLines int // Total number of lines of code
Config *Config // The Loc configuration
Directory string // The directory to scan
}
// LanguageConfig is the configuration for a language
type LanguageConfig struct {
SkipPatterns []string `json:"skip_patterns"` // Patterns to skip; lines matching these patterns will not be counted
Extensions []string `json:"extensions"` // File extensions to count
}
// Config is the configuration for Loc
type Config struct {
Languages map[string]LanguageConfig `json:"languages"`
}
// CONFIG_FILE is the name of the configuration file
const CONFIG_FILE = "config.json"
// readConfig reads the Loc configuration file
func readConfig() (*Config, error) {
// get working directory
wd, err := os.Getwd()
if err != nil {
return nil, err
}
// read the config files contents into memory
configFile, err := os.ReadFile(wd + string(os.PathSeparator) + CONFIG_FILE)
if err != nil {
return nil, err
}
// create config variable
var config Config
// unmarshal the config file into the config variable
err = json.Unmarshal(configFile, &config)
if err != nil {
return nil, err
}
// return the config variable
return &config, nil
}
// scan scans the directory and counts the lines of code
func (loc *Loc) scan() error {
// Walk the directory
return filepath.Walk(loc.Directory, func(path string, info os.FileInfo, err error) error {
if err != nil { // if there is an error, return the error
return err
}
if !info.IsDir() { // if the file is not a directory we can count the lines of code
for _, langConfig := range loc.Config.Languages { // iterate over configured languages
for _, ext := range langConfig.Extensions { // iterate over the extensions for the language
if strings.HasSuffix(path, ext) { // if the file has the correct extension
lines, err := loc.countLines(path, langConfig.SkipPatterns) // count the lines of code
if err != nil {
return err
}
loc.TotalLines += lines // add the lines of code to the total
}
}
}
}
return nil
})
}
// countLines counts the lines of code in a file
func (loc *Loc) countLines(filePath string, skipPatterns []string) (int, error) {
// we need to open the file
file, err := os.Open(filePath)
if err != nil {
return 0, err
}
defer file.Close() // defer the closure of the file
scanner := bufio.NewScanner(file) // create a scanner for the file
totalLines := 0 // total lines of code in the file
// create a slice of regular expressions for the skip patterns
skipRegexps := make([]*regexp.Regexp, len(skipPatterns))
for i, pattern := range skipPatterns {
skipRegexps[i] = regexp.MustCompile(pattern) // compile the regular expression
}
for scanner.Scan() { // iterate over the lines of the file
line := scanner.Text() // get the line of the file
skip := false // set skip to false
for _, re := range skipRegexps {
if re.MatchString(line) { // if the line matches the regular expression we skip it
skip = true
break
}
}
if !skip { // if we are not skipping the line we increment the total lines
totalLines++
}
}
// check for scanner errors
if err := scanner.Err(); err != nil {
return 0, err
}
return totalLines, nil
}
// cloneRepo clones a GitHub repository to a temporary directory
func cloneRepo(repoURL string) (string, error) {
tempDir, err := os.MkdirTemp("", "loc-repo-") // create a temporary directory
if err != nil {
return "", err
}
cmd := exec.Command("git", "clone", repoURL, tempDir)
err = cmd.Run()
if err != nil {
os.RemoveAll(tempDir)
return "", err
}
return tempDir, nil
}
func main() {
var err error // global error variable
loc := Loc{} // create a new Loc struct
dir := flag.String("dir", ".", "directory to count lines of code") // create a flag for the directory
repo := flag.String("repo", ".", "github repository to count lines of code") // create a flag for a repository
flag.Parse() // parse the flags
loc.Directory = *dir // set the directory
// directory supercedes repo
if loc.Directory == "" { // if the directory is empty
fmt.Println("Directory is empty") // print an error
os.Exit(1)
} else {
if *repo != "" {
loc.Directory, err = cloneRepo(*repo)
if err != nil {
fmt.Println("Error cloning repository:", err)
return
}
defer os.RemoveAll(loc.Directory)
} else {
loc.Directory = *dir
}
}
// Read the config
loc.Config, err = readConfig()
if err != nil {
fmt.Println("Error reading config:", err)
return
}
// Scan the directory and count lines of code
err = loc.scan()
if err != nil {
fmt.Println("Error scanning directory:", err)
return
}
fmt.Printf("Total lines of code: %d\n", loc.TotalLines) // print the total lines of code
}