├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go ├── process ├── gc_options.go ├── process_flags.go └── process_workers.go ├── resources ├── benchmark.gif ├── scc_redis.png ├── stto_redis.png ├── stto_usage_1.png ├── stto_usage_2.png ├── stto_usage_3.png └── tokei_redis.png ├── tests └── stto_test.go └── utils ├── emit_output_json.go ├── emit_output_table.go ├── emit_output_yaml.go ├── handle_cmd.go ├── handle_files.go ├── lookup.go └── utils.go /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others’ private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all project spaces, and also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All project maintainers are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to stto 2 | 3 | First off, thank you for considering contributing to stto! 🎉 We appreciate your interest and effort in improving our project. 4 | 5 | ## How to Contribute 6 | 7 | ### Reporting Issues 8 | 9 | 1. **Check Existing Issues**: Before creating a new issue, please check the [issue tracker](https://github.com/mainak55512/stto/issues) to see if your issue has already been reported. 10 | 2. **Create a New Issue**: If your issue is new, [open a new issue](https://github.com/mainak55512/stto/issues/new) and provide as much detail as possible. Include: 11 | - A clear and descriptive title 12 | - A description of the problem or enhancement 13 | - Steps to reproduce the issue (if applicable) 14 | - Any relevant screenshots or error messages 15 | 16 | ### Contributing Code 17 | 18 | 1. **Fork the Repository**: Create a fork of the repository by clicking the "Fork" button at the top right of the repository page. 19 | 2. **Clone Your Fork**: Clone your fork to your local machine using: 20 | ```bash 21 | git clone https://github.com/mainak55512/stto.git 22 | ``` 23 | 3. **Create a Branch**: Create a new branch for your changes: 24 | ```bash 25 | git checkout -b your-branch-name 26 | ``` 27 | 4. **Make Changes**: Make your changes to the codebase. Ensure your changes follow the existing code style and conventions. 28 | 5. **Commit Changes**: Commit your changes with a clear and descriptive commit message: 29 | ```bash 30 | git add . 31 | git commit -m "Description of your changes" 32 | ``` 33 | 6. **Push Changes**: Push your changes to your forked repository: 34 | ```bash 35 | git push origin your-branch-name 36 | ``` 37 | 7. **Create a Pull Request**: Go to the [Pull Requests](https://github.com/mainak55512/stto/pulls) page of the original repository and click "New Pull Request." Select your branch and provide a description of your changes. 38 | 39 | 40 | If you have any questions or need help, feel free to open an issue or reach out to us! 41 | 42 | Thank you for contributing! 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MAINAK BHATTACHARJEE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STTO 2 | 3 | Command-line utility written in Go to check total line of code in a file present in a directory. 4 | 5 | ## Authors 6 | 7 | - [Mainak Bhattacharjee](https://github.com/mainak55512) 8 | 9 | ## Dependencies 10 | 11 | - go 1.22.5 12 | - github.com/mattn/go-runewidth v0.0.9 13 | - github.com/olekukonko/tablewriter v0.0.5 14 | 15 | ## Benchmark 16 | 17 | #### Benchmark was run on the clone of '[Redis](https://github.com/redis/redis)' repository 18 | 19 | ![Demo](./resources/benchmark.gif) 20 | 21 | **N.B: stto is no way near the more established options like 'scc' or 'tokei' in terms of features. It is in early development stage and isn't production ready. 22 | 23 | All the tools read over 1.5k files 24 | ![stto](./resources/stto_redis.png) 25 | ![scc](./resources/scc_redis.png) 26 | ![tokei](./resources/tokei_redis.png) 27 | 28 | ## Installation 29 | 30 | Install using the following command for latest features 31 | ```bash 32 | go install github.com/mainak55512/stto@latest 33 | ``` 34 | Alternatively you can use the optimized builds from the [release section](https://github.com/mainak55512/stto/releases) 35 | 36 | N.B. Release builds are generally more optimized but lack latest features, 'go install' command is the prefered way to download the executable. 37 | 38 | ## Usage 39 | 40 | Full usage details available in 41 | ```bash 42 | stto --help 43 | ``` 44 | 45 | ### Usage 1: 46 | ![stto_usage_1](./resources/stto_usage_1.png) 47 | 48 | ### Usage 2: 49 | ![stto_usage_2](./resources/stto_usage_2.png) 50 | 51 | ### Usage 3: 52 | ![stto_usage_3](./resources/stto_usage_3.png) 53 | 54 | N.B. 'jproc' is a json query tool writtrn in javascript. For more details check out the [repo](https://github.com/mainak55512/JSONProcessor). 55 | 56 | ## 🚀 About Me 57 | I'm a Tech enthusiast and a hobby programmer. 58 | You can visit my [Github profile](https://github.com/mainak55512) to see my other works :) 59 | 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mainak55512/stto 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/olekukonko/tablewriter v0.0.5 7 | gopkg.in/yaml.v3 v3.0.1 8 | ) 9 | 10 | require github.com/mattn/go-runewidth v0.0.9 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 2 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 3 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 4 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 5 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 6 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 7 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 8 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mainak55512/stto/process" 5 | "github.com/mainak55512/stto/utils" 6 | "sync" 7 | ) 8 | 9 | func main() { 10 | 11 | // Mutex pointer 12 | mu := &sync.RWMutex{} 13 | 14 | // Waitgroup pointer 15 | wg := &sync.WaitGroup{} 16 | 17 | // File details array where all the data is stored 18 | var file_details []utils.File_details 19 | 20 | var count_details []utils.OutputStructure 21 | 22 | // Tracks sub-directory count 23 | var folder_count int32 = 0 24 | 25 | // Tracks is git is initialized for present working directory 26 | var is_git_initialized bool = false 27 | 28 | var folder_name string = "" 29 | 30 | process.SetGCOptions() 31 | 32 | process.ProcessByFlags(&count_details, &file_details, &folder_name, &is_git_initialized, &folder_count, mu, wg) 33 | } 34 | -------------------------------------------------------------------------------- /process/gc_options.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "runtime" 5 | "runtime/debug" 6 | ) 7 | 8 | func SetGCOptions() { 9 | // Optimizing GC 10 | debug.SetGCPercent(1000) 11 | 12 | // Limiting os threads to available cpu 13 | runtime.GOMAXPROCS(runtime.NumCPU()) 14 | } 15 | -------------------------------------------------------------------------------- /process/process_flags.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/mainak55512/stto/utils" 8 | ) 9 | 10 | func ProcessCount(count_details *[]utils.OutputStructure, file_details *[]utils.File_details, folder_name *string, is_git_initialized *bool, folder_count *int32, mu *sync.RWMutex, wg *sync.WaitGroup) (utils.TotalCount, error) { 11 | err := ProcessConcurrentWorkers(file_details, folder_count, folder_name, is_git_initialized, mu, wg) 12 | if err != nil { 13 | return utils.TotalCount{}, fmt.Errorf("%w", err) 14 | } 15 | 16 | total_files, 17 | total_lines, 18 | total_gaps, 19 | total_comments, 20 | total_code := utils.GetTotalCounts(file_details) 21 | 22 | for _, item := range *file_details { 23 | *count_details = append(*count_details, utils.OutputStructure{ 24 | Ext: item.Ext, 25 | File_count: item.File_count, 26 | Code: item.Code, 27 | Gap: item.Gap, 28 | Comments: item.Comments, 29 | Line_count: item.Line_count, 30 | Code_percent: float32(item.Line_count) / float32(total_lines) * 100, 31 | }) 32 | } 33 | 34 | return utils.TotalCount{ 35 | Total_files: total_files, 36 | Total_lines: total_lines, 37 | Total_gaps: total_gaps, 38 | Total_comments: total_comments, 39 | Total_code: total_code, 40 | }, nil 41 | } 42 | 43 | func ProcessByFlags(count_details *[]utils.OutputStructure, file_details *[]utils.File_details, folder_name *string, is_git_initialized *bool, folder_count *int32, mu *sync.RWMutex, wg *sync.WaitGroup) { 44 | 45 | inpFlags := utils.HandleFlags(folder_name) 46 | 47 | if *inpFlags.Help == true { 48 | fmt.Println(utils.EmitHelpText()) 49 | } else { 50 | if *inpFlags.JSON == true { 51 | _, err := ProcessCount(count_details, file_details, folder_name, is_git_initialized, folder_count, mu, wg) 52 | if err != nil { 53 | fmt.Println(fmt.Errorf("%w", err)) 54 | } 55 | if *inpFlags.Sort == true { 56 | utils.SortResult(count_details) 57 | } 58 | jsonOutput, err := utils.EmitJSON(inpFlags.Lang, inpFlags.NLang, count_details) 59 | if err != nil { 60 | fmt.Println(fmt.Errorf("%w", err)) 61 | } 62 | fmt.Println(jsonOutput) 63 | } else if *inpFlags.YAML == true { 64 | _, err := ProcessCount(count_details, file_details, folder_name, is_git_initialized, folder_count, mu, wg) 65 | if err != nil { 66 | fmt.Println(fmt.Errorf("%w", err)) 67 | } 68 | if *inpFlags.Sort == true { 69 | utils.SortResult(count_details) 70 | } 71 | yamlOutput, err := utils.EmitYAML(inpFlags.Lang, inpFlags.NLang, count_details) 72 | if err != nil { 73 | fmt.Println(fmt.Errorf("%w", err)) 74 | } 75 | fmt.Println(yamlOutput) 76 | } else { 77 | total_counts, err := ProcessCount(count_details, file_details, folder_name, is_git_initialized, folder_count, mu, wg) 78 | if err != nil { 79 | fmt.Println(fmt.Errorf("%w", err)) 80 | } 81 | if *inpFlags.Sort == true { 82 | utils.SortResult(count_details) 83 | } 84 | err = utils.EmitTable(inpFlags.Lang, inpFlags.NLang, count_details, &total_counts, folder_name, is_git_initialized, folder_count) 85 | if err != nil { 86 | fmt.Println(fmt.Errorf("%w", err)) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /process/process_workers.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "github.com/mainak55512/stto/utils" 5 | "sync" 6 | ) 7 | 8 | func ProcessConcurrentWorkers( 9 | file_details *[]utils.File_details, 10 | folder_count *int32, 11 | folder_name *string, 12 | is_git_initialized *bool, 13 | mu *sync.RWMutex, 14 | wg *sync.WaitGroup, 15 | ) error { 16 | 17 | // Limited goroutines to 50 18 | max_goroutines := 50 19 | 20 | // Buffered jobs channel 21 | jobs := make(chan utils.File_info, 100) 22 | 23 | files, err := utils.GetFiles(is_git_initialized, folder_count, *folder_name) 24 | 25 | if err != nil { 26 | return err 27 | } 28 | 29 | worker := func(jobs <-chan utils.File_info) { 30 | defer wg.Done() 31 | for job := range jobs { 32 | utils.GetFileDetails(job, file_details, mu) 33 | } 34 | } 35 | 36 | for i := 1; i <= max_goroutines; i++ { 37 | wg.Add(1) 38 | go worker(jobs) 39 | } 40 | for _, f := range files { 41 | jobs <- f 42 | } 43 | close(jobs) 44 | wg.Wait() 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /resources/benchmark.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/benchmark.gif -------------------------------------------------------------------------------- /resources/scc_redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/scc_redis.png -------------------------------------------------------------------------------- /resources/stto_redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/stto_redis.png -------------------------------------------------------------------------------- /resources/stto_usage_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/stto_usage_1.png -------------------------------------------------------------------------------- /resources/stto_usage_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/stto_usage_2.png -------------------------------------------------------------------------------- /resources/stto_usage_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/stto_usage_3.png -------------------------------------------------------------------------------- /resources/tokei_redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mainak55512/stto/326ef811eb0316b7b134bd1b456571df6a410501/resources/tokei_redis.png -------------------------------------------------------------------------------- /tests/stto_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/mainak55512/stto/utils" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | func dummyData() []utils.OutputStructure { 13 | test_list := []utils.OutputStructure{ 14 | { 15 | Ext: "py", 16 | File_count: 10, 17 | Code: 365, 18 | Gap: 40, 19 | Comments: 23, 20 | Line_count: 428, 21 | Code_percent: 57.67, 22 | }, 23 | { 24 | Ext: "go", 25 | File_count: 10, 26 | Code: 365, 27 | Gap: 40, 28 | Comments: 23, 29 | Line_count: 428, 30 | Code_percent: 57.67, 31 | }, 32 | { 33 | Ext: "c", 34 | File_count: 10, 35 | Code: 365, 36 | Gap: 40, 37 | Comments: 23, 38 | Line_count: 428, 39 | Code_percent: 57.67, 40 | }, 41 | { 42 | Ext: "rs", 43 | File_count: 10, 44 | Code: 365, 45 | Gap: 40, 46 | Comments: 23, 47 | Line_count: 428, 48 | Code_percent: 57.67, 49 | }, 50 | { 51 | Ext: "cpp", 52 | File_count: 10, 53 | Code: 365, 54 | Gap: 40, 55 | Comments: 23, 56 | Line_count: 428, 57 | Code_percent: 57.67, 58 | }, 59 | } 60 | return test_list 61 | } 62 | 63 | func TestJSONEXT(t *testing.T) { 64 | test_data := dummyData() 65 | ext := "go, c" 66 | next := "none" 67 | expected_output_list := []utils.OutputStructure{ 68 | { 69 | Ext: "go", 70 | File_count: 10, 71 | Code: 365, 72 | Gap: 40, 73 | Comments: 23, 74 | Line_count: 428, 75 | Code_percent: 57.67, 76 | }, 77 | { 78 | Ext: "c", 79 | File_count: 10, 80 | Code: 365, 81 | Gap: 40, 82 | Comments: 23, 83 | Line_count: 428, 84 | Code_percent: 57.67, 85 | }, 86 | } 87 | 88 | jsonOP, _ := json.MarshalIndent(expected_output_list, "", " ") 89 | 90 | if out, _ := utils.EmitJSON(&ext, &next, &test_data); out != string(jsonOP) { 91 | t.Fatal("Failed!") 92 | } 93 | 94 | } 95 | 96 | func TestJSONEXTNonExistingExt(t *testing.T) { 97 | test_data := dummyData() 98 | ext := "md, rb" 99 | next := "none" 100 | expected_output_list := "No file with extension(s) 'md, rb' exists in this directory" 101 | if _, err := utils.EmitJSON(&ext, &next, &test_data); fmt.Sprintf("%s", err) != expected_output_list { 102 | t.Fatal("Failed!") 103 | } 104 | } 105 | 106 | func TestYAMLEXTNonExistingExt(t *testing.T) { 107 | test_data := dummyData() 108 | ext := "md, rb" 109 | next := "none" 110 | expected_output_list := "No file with extension(s) 'md, rb' exists in this directory" 111 | if _, err := utils.EmitYAML(&ext, &next, &test_data); fmt.Sprintf("%s", err) != expected_output_list { 112 | t.Fatal("Failed!") 113 | } 114 | } 115 | 116 | func TestYAMLEXT(t *testing.T) { 117 | test_data := dummyData() 118 | ext := "go, c" 119 | next := "none" 120 | expected_output_list := []utils.OutputStructure{ 121 | { 122 | Ext: "go", 123 | File_count: 10, 124 | Code: 365, 125 | Gap: 40, 126 | Comments: 23, 127 | Line_count: 428, 128 | Code_percent: 57.67, 129 | }, 130 | { 131 | Ext: "c", 132 | File_count: 10, 133 | Code: 365, 134 | Gap: 40, 135 | Comments: 23, 136 | Line_count: 428, 137 | Code_percent: 57.67, 138 | }, 139 | } 140 | 141 | yamlOP, _ := yaml.Marshal(expected_output_list) 142 | 143 | if out, _ := utils.EmitYAML(&ext, &next, &test_data); out != string(yamlOP) { 144 | t.Fatal("Failed!") 145 | } 146 | 147 | } 148 | 149 | func TestYAMLNEXT(t *testing.T) { 150 | test_data := dummyData() 151 | ext := "none" 152 | next := "c, go" 153 | expected_output_list := []utils.OutputStructure{ 154 | { 155 | Ext: "py", 156 | File_count: 10, 157 | Code: 365, 158 | Gap: 40, 159 | Comments: 23, 160 | Line_count: 428, 161 | Code_percent: 57.67, 162 | }, 163 | { 164 | Ext: "rs", 165 | File_count: 10, 166 | Code: 365, 167 | Gap: 40, 168 | Comments: 23, 169 | Line_count: 428, 170 | Code_percent: 57.67, 171 | }, 172 | { 173 | Ext: "cpp", 174 | File_count: 10, 175 | Code: 365, 176 | Gap: 40, 177 | Comments: 23, 178 | Line_count: 428, 179 | Code_percent: 57.67, 180 | }, 181 | } 182 | 183 | yamlOP, _ := yaml.Marshal(expected_output_list) 184 | 185 | if out, _ := utils.EmitYAML(&ext, &next, &test_data); out != string(yamlOP) { 186 | t.Fatal("Failed!") 187 | } 188 | 189 | } 190 | 191 | func TestJSONNEXT(t *testing.T) { 192 | test_data := dummyData() 193 | ext := "none" 194 | next := "c, go" 195 | expected_output_list := []utils.OutputStructure{ 196 | { 197 | Ext: "py", 198 | File_count: 10, 199 | Code: 365, 200 | Gap: 40, 201 | Comments: 23, 202 | Line_count: 428, 203 | Code_percent: 57.67, 204 | }, 205 | { 206 | Ext: "rs", 207 | File_count: 10, 208 | Code: 365, 209 | Gap: 40, 210 | Comments: 23, 211 | Line_count: 428, 212 | Code_percent: 57.67, 213 | }, 214 | { 215 | Ext: "cpp", 216 | File_count: 10, 217 | Code: 365, 218 | Gap: 40, 219 | Comments: 23, 220 | Line_count: 428, 221 | Code_percent: 57.67, 222 | }, 223 | } 224 | 225 | jsonOP, _ := json.MarshalIndent(expected_output_list, "", " ") 226 | 227 | if out, _ := utils.EmitJSON(&ext, &next, &test_data); out != string(jsonOP) { 228 | t.Fatal("Failed!") 229 | } 230 | 231 | } 232 | -------------------------------------------------------------------------------- /utils/emit_output_json.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | func EmitJSON( 11 | lang *string, 12 | nlang *string, 13 | // file_details *[]File_details, 14 | count_details *[]OutputStructure, 15 | ) (string, error) { 16 | // var count_details []OutputStructure 17 | if *lang != "none" { 18 | var valid_ext bool = false 19 | ext_list := strings.Split(*lang, ",") 20 | var temp_item_list []OutputStructure 21 | for i := range ext_list { 22 | ext_list[i] = strings.TrimSpace(ext_list[i]) 23 | } 24 | for _, item := range *count_details { 25 | // checks if extension provided 26 | // through --ext flag is present in file_details array 27 | if slices.Contains(ext_list, item.Ext) { 28 | valid_ext = true 29 | temp_item_list = append(temp_item_list, item) 30 | } 31 | } 32 | if valid_ext == false { 33 | return "", fmt.Errorf( 34 | "No file with extension(s) '%s' "+ 35 | "exists in this directory", 36 | *lang, 37 | ) 38 | } 39 | jsonOutput, err := json.MarshalIndent(temp_item_list, "", " ") 40 | return string(jsonOutput), err 41 | } 42 | if *nlang != "none" { 43 | n_ext_list := strings.Split(*nlang, ",") 44 | var temp_item_list []OutputStructure 45 | for i := range n_ext_list { 46 | n_ext_list[i] = strings.TrimSpace(n_ext_list[i]) 47 | } 48 | for _, item := range *count_details { 49 | // checks if extension provided 50 | // through --ext flag is present in file_details array 51 | if !slices.Contains(n_ext_list, item.Ext) { 52 | temp_item_list = append(temp_item_list, item) 53 | } 54 | } 55 | jsonOutput, err := json.MarshalIndent(temp_item_list, "", " ") 56 | return string(jsonOutput), err 57 | } 58 | jsonOutput, err := json.MarshalIndent(count_details, "", " ") 59 | return string(jsonOutput), err 60 | } 61 | -------------------------------------------------------------------------------- /utils/emit_output_table.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "slices" 8 | "strings" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | func EmitTable( 14 | lang *string, 15 | nlang *string, 16 | count_details *[]OutputStructure, 17 | // file_details *[]File_details, 18 | total_counts *TotalCount, 19 | folder_name *string, 20 | is_git_initialized *bool, 21 | folder_count *int32, 22 | ) error { 23 | table := tablewriter.NewWriter(os.Stdout) 24 | table.SetHeader([]string{ 25 | "File Type", 26 | "File Count", 27 | "Number of Lines", 28 | "Gap", 29 | "comments", 30 | "Code", 31 | "% Percentage", 32 | }) 33 | table.SetFooterAlignment(2) 34 | 35 | // if not extension is provided via --ext flag 36 | if *lang == "none" { 37 | for _, item := range *count_details { 38 | if *nlang != "none" { 39 | n_ext_list := strings.Split(*nlang, ",") 40 | for i := range n_ext_list { 41 | n_ext_list[i] = strings.TrimSpace(n_ext_list[i]) 42 | } 43 | if !slices.Contains(n_ext_list, item.Ext) { 44 | table.Append([]string{ 45 | item.Ext, 46 | fmt.Sprint(item.File_count), 47 | fmt.Sprint(item.Line_count), 48 | fmt.Sprint(item.Gap), 49 | fmt.Sprint(item.Comments), 50 | fmt.Sprint(item.Code), 51 | fmt.Sprintf("%.2f", item.Code_percent), 52 | }) 53 | } 54 | } else { 55 | table.Append([]string{ 56 | item.Ext, 57 | fmt.Sprint(item.File_count), 58 | fmt.Sprint(item.Line_count), 59 | fmt.Sprint(item.Gap), 60 | fmt.Sprint(item.Comments), 61 | fmt.Sprint(item.Code), 62 | fmt.Sprintf("%.2f", item.Code_percent), 63 | }) 64 | } 65 | } 66 | 67 | table.SetFooter([]string{ 68 | "Total:", 69 | fmt.Sprint(total_counts.Total_files), 70 | fmt.Sprint(total_counts.Total_lines), 71 | fmt.Sprint(total_counts.Total_gaps), 72 | fmt.Sprint(total_counts.Total_comments), 73 | fmt.Sprint(total_counts.Total_code), 74 | "100%", 75 | }) 76 | 77 | pwd, e := os.Getwd() 78 | fmt.Printf( 79 | "\nGit initialized:\t%t\nTotal sub-directories:\t%5d\n", 80 | *is_git_initialized, 81 | *folder_count, 82 | ) 83 | fmt.Println("Target directory: ", path.Join(pwd, *folder_name)) 84 | fmt.Println() 85 | 86 | table.Render() 87 | 88 | if e != nil { 89 | return e 90 | } 91 | return nil 92 | 93 | } else { 94 | 95 | // will be set to true if atleast one file 96 | // with provided extension via --ext flag is present 97 | var valid_ext bool = false 98 | ext_list := strings.Split(*lang, ",") 99 | for i := range ext_list { 100 | ext_list[i] = strings.TrimSpace(ext_list[i]) 101 | } 102 | for _, item := range *count_details { 103 | 104 | // checks if extension provided 105 | // through --ext flag is present in file_details array 106 | if slices.Contains(ext_list, item.Ext) { 107 | 108 | // found valid extension hence setting as true 109 | valid_ext = true 110 | table.Append([]string{ 111 | item.Ext, 112 | fmt.Sprint(item.File_count), 113 | fmt.Sprint(item.Line_count), 114 | fmt.Sprint(item.Gap), 115 | fmt.Sprint(item.Comments), 116 | fmt.Sprint(item.Code), 117 | fmt.Sprintf("%.2f", item.Code_percent), 118 | }) 119 | // break 120 | } 121 | } 122 | 123 | // if no file with the provided 124 | // extension is found it will throw error 125 | if valid_ext == false { 126 | // fmt.Println( 127 | // fmt.Errorf( 128 | // "No file with extension '%s' "+ 129 | // "exists in this directory", 130 | // *lang, 131 | // ), 132 | // ) 133 | return fmt.Errorf( 134 | "No file with extension '%s' "+ 135 | "exists in this directory", 136 | *lang, 137 | ) 138 | } else { 139 | table.Render() 140 | return nil 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /utils/emit_output_yaml.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "gopkg.in/yaml.v3" 6 | "slices" 7 | "strings" 8 | ) 9 | 10 | func EmitYAML( 11 | lang *string, 12 | nlang *string, 13 | // file_details *[]File_details, 14 | count_details *[]OutputStructure, 15 | ) (string, error) { 16 | if *lang != "none" { 17 | var valid_ext bool = false 18 | ext_list := strings.Split(*lang, ",") 19 | var temp_item_list []OutputStructure 20 | for i := range ext_list { 21 | ext_list[i] = strings.TrimSpace(ext_list[i]) 22 | } 23 | for _, item := range *count_details { 24 | // checks if extension provided 25 | // through --ext flag is present in file_details array 26 | if slices.Contains(ext_list, item.Ext) { 27 | valid_ext = true 28 | temp_item_list = append(temp_item_list, item) 29 | } 30 | } 31 | if valid_ext == false { 32 | return "", fmt.Errorf( 33 | "No file with extension(s) '%s' "+ 34 | "exists in this directory", 35 | *lang, 36 | ) 37 | } 38 | yamlOutput, err := yaml.Marshal(temp_item_list) 39 | return string(yamlOutput), err 40 | } 41 | if *nlang != "none" { 42 | n_ext_list := strings.Split(*nlang, ",") 43 | var temp_item_list []OutputStructure 44 | for i := range n_ext_list { 45 | n_ext_list[i] = strings.TrimSpace(n_ext_list[i]) 46 | } 47 | for _, item := range *count_details { 48 | // checks if extension provided 49 | // through --ext flag is present in file_details array 50 | if !slices.Contains(n_ext_list, item.Ext) { 51 | temp_item_list = append(temp_item_list, item) 52 | } 53 | } 54 | yamlOutput, err := yaml.Marshal(temp_item_list) 55 | return string(yamlOutput), err 56 | } 57 | yamlOutput, err := yaml.Marshal(*count_details) 58 | return string(yamlOutput), err 59 | } 60 | -------------------------------------------------------------------------------- /utils/handle_cmd.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | type FlagOptions struct { 8 | Lang *string 9 | NLang *string 10 | Help *bool 11 | JSON *bool 12 | YAML *bool 13 | Sort *bool 14 | } 15 | 16 | func HandleFlags(folder_name *string) FlagOptions { 17 | // flag --ext 18 | var lang = flag.String("ext", "none", "Filter based on extention") 19 | var nlang = flag.String("excl-ext", "none", "Exclude files with certain extention") 20 | var help = flag.Bool("help", false, "Shows help text") 21 | var json = flag.Bool("json", false, "Get output in json format") 22 | var yaml = flag.Bool("yaml", false, "Get output in yaml format") 23 | var sort = flag.Bool("sort", false, "Sort result in descending order") 24 | 25 | flag.Parse() 26 | if len(flag.Args()) > 0 { 27 | *folder_name = flag.Args()[0] 28 | } 29 | return FlagOptions{ 30 | Lang: lang, 31 | NLang: nlang, 32 | Help: help, 33 | JSON: json, 34 | YAML: yaml, 35 | Sort: sort, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /utils/handle_files.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | /* 12 | It will add or update a file_details{} structure to 13 | file_details array using the inputs received(from getFiles()) 14 | */ 15 | func GetFileDetails( 16 | file File_info, 17 | file_details *[]File_details, 18 | mu *sync.RWMutex, 19 | ) { 20 | code, gap, comments, line_count := countLines(file.path, file.ext) 21 | mu.Lock() 22 | if len(*file_details) == 0 { 23 | addNewEntry( 24 | file.ext, 25 | file_details, 26 | code, 27 | gap, 28 | comments, 29 | line_count, 30 | ) 31 | } else if len(*file_details) > 0 { 32 | 33 | // to check if the file format is already 34 | // present in file_details array 35 | check := false 36 | updateExistingEntry( 37 | file.ext, 38 | file_details, 39 | &check, 40 | code, 41 | gap, 42 | comments, 43 | line_count, 44 | ) 45 | 46 | // check == false means the file format isn't 47 | // present in file_details, hence adding new entry 48 | if check == false { 49 | addNewEntry( 50 | file.ext, 51 | file_details, 52 | code, 53 | gap, 54 | comments, 55 | line_count, 56 | ) 57 | } 58 | } 59 | mu.Unlock() 60 | } 61 | 62 | /* 63 | If not folder, it will return the path and extension of the file. 64 | */ 65 | func GetFiles( 66 | is_git_initialized *bool, 67 | folder_count *int32, 68 | file_directory_name string, 69 | ) ([]File_info, error) { 70 | var files []File_info 71 | folder_location := "." 72 | if file_directory_name != "" { 73 | folder_location = path.Join(folder_location, file_directory_name) 74 | } 75 | wgDir := &sync.WaitGroup{} 76 | muDir := &sync.RWMutex{} 77 | 78 | // Limited goroutines to 1000 79 | // buffer is set to high to avoid deadlock 80 | max_goroutines_dir := 1000 81 | 82 | // this channel will limit the goroutine number 83 | guardDir := make(chan struct{}, max_goroutines_dir) 84 | 85 | guardDir <- struct{}{} 86 | wgDir.Add(1) 87 | 88 | err := walkDirConcur(folder_location, folder_count, &files, is_git_initialized, wgDir, muDir, guardDir) 89 | wgDir.Wait() 90 | 91 | return files, err 92 | } 93 | 94 | func walkDirConcur(folder_location string, folder_count *int32, files *[]File_info, is_git_initialized *bool, wgDir *sync.WaitGroup, muDir *sync.RWMutex, guardDir chan struct{}) error { 95 | defer wgDir.Done() 96 | 97 | visitFolder := func( 98 | _path string, 99 | f os.DirEntry, 100 | err error, 101 | ) error { 102 | 103 | // Handling the error is there is any during file read 104 | if err != nil { 105 | return err 106 | } 107 | // if it is a folder, then increase the folder count 108 | if f.IsDir() && _path != folder_location { 109 | 110 | // if folder name is '.git', then 111 | // set is_git_initialized to true 112 | if _path == path.Join(folder_location, ".git") { 113 | muDir.Lock() 114 | if *is_git_initialized == false { 115 | *is_git_initialized = true 116 | } 117 | muDir.Unlock() 118 | } 119 | muDir.Lock() 120 | *folder_count++ 121 | muDir.Unlock() 122 | guardDir <- struct{}{} 123 | wgDir.Add(1) 124 | go walkDirConcur(_path, folder_count, files, is_git_initialized, wgDir, muDir, guardDir) 125 | return filepath.SkipDir 126 | } 127 | if f.Type().IsRegular() { 128 | ext := strings.Join( 129 | strings.Split(f.Name(), ".")[1:], 130 | ".", 131 | ) 132 | if ext == "" { 133 | ext = f.Name() 134 | } 135 | if _, exists := lookup_map[ext]; exists { 136 | muDir.Lock() 137 | *files = append(*files, File_info{ 138 | path: _path, 139 | ext: ext, 140 | }) 141 | muDir.Unlock() 142 | } 143 | } 144 | return nil 145 | } 146 | err := filepath.WalkDir(folder_location, visitFolder) 147 | <-guardDir 148 | return err 149 | } 150 | -------------------------------------------------------------------------------- /utils/lookup.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type Comment_Map struct { 4 | supports_multi bool 5 | single_comment string 6 | multi_comment_open string 7 | multi_comment_close string 8 | } 9 | 10 | /* 11 | language comments Map, Key: extension name , 12 | value:Comment_Map struct 13 | */ 14 | 15 | var lookup_map map[string]Comment_Map = map[string]Comment_Map{ 16 | "abap": {false, "*", "", ""}, 17 | "as": {true, "//", "/*", "*/"}, 18 | "ada": {false, "--", "", ""}, 19 | "adb": {false, "--", "", ""}, 20 | "ads": {false, "--", "", ""}, 21 | "pad": {false, "--", "", ""}, 22 | "agda": {true, "--", "{-", "-}"}, 23 | "crn": {false, "#", "", ""}, 24 | "als": {true, "//", "/*", "*/"}, 25 | "aidl": {true, "//", "/*", "*/"}, 26 | "apl": {false, "⍝", "", ""}, 27 | "aplf": {false, "⍝", "", ""}, 28 | "apln": {false, "⍝", "", ""}, 29 | "aplc": {false, "⍝", "", ""}, 30 | "dyalog": {false, "⍝", "", ""}, 31 | "applescript": {true, "--", "(*", "*)"}, 32 | "art": {false, ";", "", ""}, 33 | "asa": {false, "'", "", ""}, 34 | "asp": {false, "'", "", ""}, 35 | "asax": {true, "", "<%", "%>"}, 36 | "ascx": {true, "", "<%", "%>"}, 37 | "asmx": {true, "", "<%", "%>"}, 38 | "master": {true, "", "<%", "%>"}, 39 | "aspx": {true, "", "<%", "%>"}, 40 | "sitemap": {true, "", "<%", "%>"}, 41 | "webinfo": {true, "", "<%", "%>"}, 42 | "asm": {true, ";", "/*", "*/"}, 43 | "s": {true, ";", "/*", "*/"}, 44 | "awk": {false, "#", "", ""}, 45 | "bt": {true, "//", "/*", "*/"}, 46 | "bash": {false, "#", "", ""}, 47 | "bash_login": {false, "#", "", ""}, 48 | "bash_logout": {false, "#", "", ""}, 49 | "bash_profile": {false, "#", "", ""}, 50 | "bashrc": {false, "#", "", ""}, 51 | "bas": {false, "'", "", ""}, 52 | "bat": {false, "REM", "", ""}, 53 | "btm": {false, "REM", "", ""}, 54 | "cmd": {false, "REM", "", ""}, 55 | "bzl": {false, "#", "", ""}, 56 | "build.bazel": {false, "#", "", ""}, 57 | "build": {false, "#", "", ""}, 58 | "workspace": {false, "#", "", ""}, 59 | "bb": {false, "#", "", ""}, 60 | "bbapend": {false, "#", "", ""}, 61 | "bbclass": {false, "#", "", ""}, 62 | "bst": {false, "#", "", ""}, 63 | "boo": {true, "//", "/*", "*/"}, 64 | "bsq": {false, "//", "", ""}, 65 | "go": {true, "//", "/*", "*/"}, 66 | "c": {true, "//", "/*", "*/"}, 67 | "ec": {true, "//", "/*", "*/"}, 68 | "pgc": {true, "//", "/*", "*/"}, 69 | "h": {true, "//", "/*", "*/"}, 70 | "hh": {true, "//", "/*", "*/"}, 71 | "hpp": {true, "//", "/*", "*/"}, 72 | "hxx": {true, "//", "/*", "*/"}, 73 | "inl": {true, "//", "/*", "*/"}, 74 | "ipp": {true, "//", "/*", "*/"}, 75 | "csh": {false, "#", "", ""}, 76 | "cpp": {true, "//", "/*", "*/"}, 77 | "cc": {true, "//", "/*", "*/"}, 78 | "cxx": {true, "//", "/*", "*/"}, 79 | "c++": {true, "//", "/*", "*/"}, 80 | "pcc": {true, "//", "/*", "*/"}, 81 | "ino": {true, "//", "/*", "*/"}, 82 | "cassius": {true, "//", "/*", "*/"}, 83 | "ceylon": {true, "//", "/*", "*/"}, 84 | "chpl": {true, "//", "/*", "*/"}, 85 | "circom": {true, "//", "/*", "*/"}, 86 | "ch": {true, "//", "/*", "*/"}, 87 | "prg": {true, "//", "/*", "*/"}, 88 | "cabal": {true, "--", "{-", "-}"}, 89 | "dhall": {true, "--", "{-", "-}"}, 90 | "elm": {true, "--", "{-", "-}"}, 91 | "pures": {true, "--", "{-", "-}"}, 92 | "cognet": {false, "--", "", ""}, 93 | "cu": {true, "//", "/*", "*/"}, 94 | "css": {true, "//", "/*", "*/"}, 95 | "js": {true, "//", "/*", "*/"}, 96 | "mjs": {true, "//", "/*", "*/"}, 97 | "ts": {true, "//", "/*", "*/"}, 98 | "jsx": {true, "//", "/*", "*/"}, 99 | "tsx": {true, "//", "/*", "*/"}, 100 | "java": {true, "//", "/*", "*/"}, 101 | "jsp": {true, "//", "/*", "*/"}, 102 | "rs": {true, "//", "/*", "*/"}, 103 | "swift": {true, "//", "/*", "*/"}, 104 | "kt": {true, "//", "/*", "*/"}, 105 | "kts": {true, "//", "/*", "*/"}, 106 | "php": {true, "//", "/*", "*/"}, 107 | "m": {true, "//", "/*", "*/"}, 108 | "groovy": {true, "//", "/*", "*/"}, 109 | "grt": {true, "//", "/*", "*/"}, 110 | "gtpl": {true, "//", "/*", "*/"}, 111 | "gvy": {true, "//", "/*", "*/"}, 112 | "cs": {true, "//", "/*", "*/"}, 113 | "csx": {true, "//", "/*", "*/"}, 114 | "scala": {true, "//", "/*", "*/"}, 115 | "zig": {true, "//", "/*", "*/"}, 116 | "gleam": {true, "//", "/*", "*/"}, 117 | "d": {true, "//", "/*", "*/"}, 118 | "dart": {true, "//", "/*", "*/"}, 119 | "dts": {true, "//", "/*", "*/"}, 120 | "dtsi": {true, "//", "/*", "*/"}, 121 | "dm": {true, "//", "/*", "*/"}, 122 | "odin": {true, "//", "/*", "*/"}, 123 | "dot": {true, "//", "/*", "*/"}, 124 | "fidl": {true, "//", "/*", "*/"}, 125 | "fsh": {true, "//", "/*", "*/"}, 126 | "cj": {true, "//", "/*", "*/"}, 127 | "jai": {true, "//", "/*", "*/"}, 128 | "julius": {true, "//", "/*", "*/"}, 129 | "fsl": {false, "//", "", ""}, 130 | "gradle": {false, "//", "", ""}, 131 | "cairo": {false, "//", "", ""}, 132 | "fxml": {false, "", ""}, 133 | "py": {true, "#", `"""`, `"""`}, 134 | "pyx": {true, "#", `"""`, `"""`}, 135 | "pxi": {true, "#", `"""`, `"""`}, 136 | "pxd": {true, "#", `"""`, `"""`}, 137 | "ipynb": {false, "", "", ""}, 138 | "jpynb": {false, "", "", ""}, 139 | "graphql": {true, "#", `"""`, `"""`}, 140 | "r": {false, "#", "", ""}, 141 | "rb": {false, "#", "", ""}, 142 | "cmake": {false, "#", "", ""}, 143 | "capnp": {false, "#", "", ""}, 144 | "cmakelists.txt": {false, "#", "", ""}, 145 | "fish": {false, "#", "", ""}, 146 | "ini": {false, "#", "", ""}, 147 | "janet": {false, "#", "", ""}, 148 | "justfile": {false, "#", "", ""}, 149 | "ksh": {false, "#", "", ""}, 150 | "k": {false, "/", "", ""}, 151 | "haml": {false, "-#", "", ""}, 152 | "hamlet": {true, "", ""}, 153 | "html": {true, "", ""}, 154 | "sh": {true, "#", ": '", "'"}, 155 | "pl": {true, "#", "/*", "*/"}, 156 | "ex": {false, "#", "", ""}, 157 | "exs": {false, "#", "", ""}, 158 | "gemfile": {false, "#", "", ""}, 159 | "gitignore": {false, "#", "", ""}, 160 | "luna": {false, "#", "", ""}, 161 | "m4": {false, "#", "", ""}, 162 | "meson.build": {false, "#", "", ""}, 163 | "meson_options.txt": {false, "#", "", ""}, 164 | "jl": {true, "#", "#=", "=#"}, 165 | "lua": {true, "--", "--[[", "]]--"}, 166 | "hs": {true, "--", "/*", "*/"}, 167 | "sql": {true, "--", "/*", "*/"}, 168 | "fnc": {true, "--", "/*", "*/"}, 169 | "pkb": {true, "--", "/*", "*/"}, 170 | "pks": {true, "--", "/*", "*/"}, 171 | "prc": {true, "--", "/*", "*/"}, 172 | "trg": {true, "--", "/*", "*/"}, 173 | "vw": {true, "--", "/*", "*/"}, 174 | "cbl": {true, "*", "/*", "*/"}, 175 | "ql": {true, "*", "/*", "*/"}, 176 | "qll": {true, "*", "/*", "*/"}, 177 | "erl": {true, "%", "=begin", "=cut"}, 178 | "hrl": {true, "%", "=begin", "=cut"}, 179 | "pp": {true, "#", "=begin", "=end"}, 180 | "clj": {false, ";", "", ""}, 181 | "cljc": {false, ";", "", ""}, 182 | "cljs": {false, ";", "", ""}, 183 | "coffee": {true, "#", "###", "###"}, 184 | "md": {false, "", "", ""}, 185 | "markdown": {false, "", "", ""}, 186 | "txt": {false, "", "", ""}, 187 | "text": {false, "", "", ""}, 188 | "json": {false, "", "", ""}, 189 | "jsonl": {false, "", "", ""}, 190 | "jsonc": {true, "//", "/*", "*/"}, 191 | "jsonnet": {true, "//", "/*", "*/"}, 192 | "libsonnet": {true, "//", "/*", "*/"}, 193 | "lds": {true, "//", "/*", "*/"}, 194 | "less": {true, "//", "/*", "*/"}, 195 | "lucius": {true, "//", "/*", "*/"}, 196 | "mq4": {true, "//", "/*", "*/"}, 197 | "mq5": {true, "//", "/*", "*/"}, 198 | "proto": {true, "//", "/*", "*/"}, 199 | "nix": {true, "//", "/*", "*/"}, 200 | "qcl": {true, "//", "/*", "*/"}, 201 | "qml": {true, "//", "/*", "*/"}, 202 | "l": {true, "", "/*", "*/"}, 203 | "yaml": {false, "#", "", ""}, 204 | "yml": {false, "#", "", ""}, 205 | "nim": {false, "#", "", ""}, 206 | "prql": {false, "#", "", ""}, 207 | "lisp": {false, ";", "", ""}, 208 | "fs": {true, "//", "(*", "*)"}, 209 | "fsi": {true, "//", "(*", "*)"}, 210 | "fsx": {true, "//", "(*", "*)"}, 211 | "fsscript": {true, "//", "(*", "*)"}, 212 | "pas": {true, "//", "(*", "*)"}, 213 | "fst": {true, "", "(*", "*)"}, 214 | "f": {false, "!", "", ""}, 215 | "for": {false, "!", "", ""}, 216 | "ftn": {false, "!", "", ""}, 217 | "f77": {false, "!", "", ""}, 218 | "f03": {false, "!", "", ""}, 219 | "pfo": {false, "!", "", ""}, 220 | "f08": {false, "!", "", ""}, 221 | "f90": {false, "!", "", ""}, 222 | "f95": {false, "!", "", ""}, 223 | "tmpl": {true, "", "{{/*", "*/}}"}, 224 | "gohtml": {true, "", "{{/*", "*/}}"}, 225 | "gotxt": {true, "", "{{/*", "*/}}"}, 226 | "qs": {false, "//", "", ""}, 227 | "jade": {false, "//-", "", ""}, 228 | "jinja": {true, "", "{#", "#}"}, 229 | "j2": {true, "", "{#", "#}"}, 230 | "jinja2": {true, "", "{#", "#}"}, 231 | "tex": {true, "%", "", ""}, 232 | "lean": {true, "--", "/-", "-/"}, 233 | "hlean": {true, "--", "/-", "-/"}, 234 | "m3": {true, "#", "(*", "*)"}, 235 | "mg": {true, "#", "(*", "*)"}, 236 | "ig": {true, "#", "(*", "*)"}, 237 | "i3": {true, "#", "(*", "*)"}, 238 | "ml": {true, "", "(*", "*)"}, 239 | "mli": {true, "", "(*", "*)"}, 240 | "ps1": {true, "#", "<#", "#>"}, 241 | "psm1": {true, "#", "<#", "#>"}, 242 | "p": {true, "%", "/*", "*/"}, 243 | "pro": {true, "%", "/*", "*/"}, 244 | "tcl": {false, "#", "", ""}, 245 | "Makefile": {false, "#", "", ""}, 246 | "makefile": {false, "#", "", ""}, 247 | "mak": {false, "#", "", ""}, 248 | "bp": {false, "#", "", ""}, 249 | "mk": {false, "#", "", ""}, 250 | "csproj": {true, "", ""}, 251 | "vbproj": {true, "", ""}, 252 | "fsproj": {true, "", ""}, 253 | "vcproj": {true, "", ""}, 254 | "vcxproj": {true, "", ""}, 255 | "vcxproj.filters": {true, "", ""}, 256 | "ilproj": {true, "", ""}, 257 | "props": {true, "", ""}, 258 | "rdlc": {true, "", ""}, 259 | "resx": {true, "", ""}, 260 | "settings": {true, "", ""}, 261 | "sln": {true, "", ""}, 262 | "targets": {true, "", ""}, 263 | } 264 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | /* 11 | This is the output structure for the getFiles() function 12 | */ 13 | type File_info struct { 14 | path string 15 | ext string 16 | } 17 | 18 | /* 19 | This is the entry structure for file_details array 20 | */ 21 | type File_details struct { 22 | Ext string 23 | File_count int32 24 | Code int32 25 | Gap int32 26 | Comments int32 27 | Line_count int32 28 | } 29 | 30 | type TotalCount struct { 31 | Total_files int32 32 | Total_lines int32 33 | Total_gaps int32 34 | Total_comments int32 35 | Total_code int32 36 | } 37 | 38 | type OutputStructure struct { 39 | Ext string `json:"ext" yaml:"ext"` 40 | File_count int32 `json:"file_count" yaml:"file_count"` 41 | Code int32 `json:"code" yaml:"code"` 42 | Gap int32 `json:"gap" yaml:"gap"` 43 | Comments int32 `json:"comments" yaml:"comments"` 44 | Line_count int32 `json:"line_count" yaml:"line_count"` 45 | Code_percent float32 `json:"percentage" yaml:"percentage"` 46 | } 47 | 48 | /* 49 | This emits help text when --help tag is called 50 | */ 51 | func EmitHelpText() string { 52 | 53 | versionDetails := `0.1.9` 54 | authorDetails := `mainak55512 (mbhattacharjee432@gmail.com)` 55 | flagDetails := "--help\n--ext [list extension name]\n--excl-ext [list of extension name]\n--json\n--yaml\n--sort" 56 | helpFlagDetails := "--help\tShows the usage details\n\n\tstto --help or,\n\tstto -help\n\n" 57 | extFlagDetails := "--ext\tFilters output based on the given extension\n\n\tstto --ext [extension name] [(optional) folder name] or,\n\tstto -ext [extension name] [(optional) folder name]\n\n" 58 | exclextFlagDetails := "--excl-ext\tFilters out files from output based on the given extension\n\n\tstto --excl-ext [extension name] [(optional) folder name] or,\n\tstto -excl-ext [extension name] [(optional) folder name]\n\n" 59 | jsonFlagDetails := "--json\tEmits output in JSON format\n\n\tstto --json\n\n" 60 | yamlFlagDetails := "--yaml\tEmits output in YAML format\n\n\tstto --yaml\n\n" 61 | sortFlagDetails := "--sort\tSorts output based on descending line count\n\n\tstto --sort\n\n" 62 | generalUsageDetails := "\n\n[General usage]:\n\tstto or,\n\tstto [folder name]" 63 | returnText := "\nSTTO: a simple and quick line of code counter.\nAuthor: " + authorDetails + "\nVersion: " + versionDetails + generalUsageDetails + "\n\n[Flags]:\n" + flagDetails + "\n[Usage]:\n" + helpFlagDetails + extFlagDetails + exclextFlagDetails + jsonFlagDetails + yamlFlagDetails + sortFlagDetails 64 | 65 | return returnText 66 | } 67 | 68 | /* 69 | This concurrently reads the files and returns stats for a single file 70 | */ 71 | func countLines( 72 | file_name string, 73 | ext string, 74 | ) (int32, int32, int32, int32) { 75 | file, err := os.Open(file_name) 76 | if err != nil { 77 | panic(err) 78 | } 79 | defer file.Close() 80 | reader := bufio.NewReader(file) 81 | var code int32 = 0 82 | var gap int32 = 0 83 | var comments int32 = 0 84 | var inside_multi_line_comment bool = false 85 | var multi_exists bool = false 86 | comment_struct, exists := lookup_map[ext] 87 | 88 | if exists { 89 | multi_exists = comment_struct.supports_multi 90 | } 91 | 92 | for { 93 | content, _, err := reader.ReadLine() 94 | content_str := string(content[:]) 95 | if err != nil { 96 | break 97 | } 98 | 99 | //Trimming spaces from each line 100 | trimmed_content_str := strings.TrimSpace(content_str) 101 | 102 | //Checks if [Opening symbol] is present in the line 103 | if multi_exists && 104 | !inside_multi_line_comment && 105 | strings.Contains( 106 | trimmed_content_str, 107 | comment_struct.multi_comment_open, 108 | ) { 109 | inside_multi_line_comment = true 110 | 111 | //Checks if [Closing symbol] is present 112 | // anywhere in the line after the [Opening symbol] 113 | if strings.Contains( 114 | trimmed_content_str[strings.Index( 115 | trimmed_content_str, 116 | comment_struct.multi_comment_open, 117 | )+len(comment_struct.multi_comment_open):], 118 | comment_struct.multi_comment_close, 119 | ) { 120 | inside_multi_line_comment = false 121 | 122 | //If [Opening symbol] is found on the start 123 | // and [Closing symbol] is found on the end 124 | if strings.HasPrefix( 125 | trimmed_content_str, 126 | comment_struct.multi_comment_open, 127 | ) && strings.HasSuffix( 128 | trimmed_content_str, 129 | comment_struct.multi_comment_close, 130 | ) { 131 | comments++ 132 | continue 133 | } 134 | } 135 | 136 | //If there is some code present before 137 | // the [Opening symbol] 138 | if !strings.HasPrefix( 139 | trimmed_content_str, 140 | comment_struct.multi_comment_open, 141 | ) { 142 | code++ 143 | continue 144 | } 145 | 146 | //Checks if [Closing symbol] is present 147 | // at anywhere on the line 148 | } else if multi_exists && 149 | inside_multi_line_comment && 150 | strings.Contains( 151 | trimmed_content_str, 152 | comment_struct.multi_comment_close, 153 | ) { 154 | inside_multi_line_comment = false 155 | 156 | //Checks if nothing present after 157 | // the [Closing symbol] on the line 158 | if strings.HasSuffix( 159 | trimmed_content_str, 160 | comment_struct.multi_comment_close, 161 | ) { 162 | comments++ 163 | continue 164 | } 165 | } 166 | 167 | //Moved the inside_multi_line_comment to 168 | // top condition as it has priority over other cases 169 | if inside_multi_line_comment { 170 | comments++ 171 | } else if trimmed_content_str == "" { 172 | gap++ 173 | } else if exists && comment_struct.single_comment != "" && strings.HasPrefix( 174 | trimmed_content_str, 175 | comment_struct.single_comment, 176 | ) { 177 | comments++ 178 | } else { 179 | code++ 180 | } 181 | } 182 | return code, gap, comments, (code + gap + comments) 183 | } 184 | 185 | /* 186 | For adding new entry to file_details array 187 | */ 188 | func addNewEntry( 189 | ext string, 190 | file_details *[]File_details, 191 | code, 192 | gap, 193 | comments, 194 | line_count int32, 195 | ) { 196 | // appending new entry 197 | *file_details = append(*file_details, File_details{ 198 | Ext: ext, 199 | File_count: 1, 200 | Code: code, 201 | Gap: gap, 202 | Comments: comments, 203 | Line_count: line_count, 204 | }) 205 | } 206 | 207 | /* 208 | For updating existing entry in file_details array 209 | */ 210 | func updateExistingEntry( 211 | ext string, 212 | file_details *[]File_details, 213 | check *bool, 214 | code, 215 | gap, 216 | comments, 217 | line_count int32, 218 | ) { 219 | for i := range *file_details { 220 | if (*file_details)[i].Ext == ext { 221 | *check = true 222 | // updating existing entry 223 | (*file_details)[i].File_count += 1 224 | (*file_details)[i].Code += code 225 | (*file_details)[i].Gap += gap 226 | (*file_details)[i].Comments += comments 227 | (*file_details)[i].Line_count += line_count 228 | break 229 | } 230 | } 231 | } 232 | 233 | /* 234 | It will count total file number, line number, gap, code and comments 235 | */ 236 | func GetTotalCounts( 237 | file_details *[]File_details, 238 | ) ( 239 | int32, int32, int32, int32, int32, 240 | ) { 241 | var file_count int32 = 0 242 | var line_count int32 = 0 243 | var gap int32 = 0 244 | var code int32 = 0 245 | var comments int32 = 0 246 | for i := range *file_details { 247 | file_count += (*file_details)[i].File_count 248 | line_count += (*file_details)[i].Line_count 249 | gap += (*file_details)[i].Gap 250 | code += (*file_details)[i].Code 251 | comments += (*file_details)[i].Comments 252 | } 253 | return file_count, line_count, gap, comments, code 254 | } 255 | 256 | func SortResult(count_details *[]OutputStructure) { 257 | sort.Slice(*count_details, func(i, j int) bool { 258 | return (*count_details)[i].Line_count > (*count_details)[j].Line_count 259 | }) 260 | } 261 | --------------------------------------------------------------------------------