From a5508858b9f397aae12a56a72fb22b4d1ef7667b Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 07:54:35 +0530 Subject: [PATCH 1/7] refactor(cli): isolate flag handling logic Signed-off-by: Priyansh Sao --- challenge-01/ccwc.go | 52 ++++++++----------------------------- challenge-01/flags.go | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 challenge-01/flags.go diff --git a/challenge-01/ccwc.go b/challenge-01/ccwc.go index e7b9b86..1d0ef3e 100644 --- a/challenge-01/ccwc.go +++ b/challenge-01/ccwc.go @@ -3,7 +3,6 @@ package main import ( "bytes" "flag" - "fmt" "io" "os" "strings" @@ -27,40 +26,11 @@ func main() { // setup logger formatLogs() - // variables to store flag value - var getBytes bool - var getLines bool - var getWords bool - var getRunes bool - myFile := new(FileInfo) + opts := new(Opts) - // define flags - flag.BoolVar(&getBytes, "c", false, "print total bytes") - flag.BoolVar(&getLines, "l", false, "print total lines") - flag.BoolVar(&getWords, "w", false, "print total words") - flag.BoolVar(&getRunes, "m", false, "print total bytes(according to utf-8 encoding)") - - // add custom usage - flag.Usage = func() { - fmt.Print("\nUsage: ccwc \n") - fmt.Print("\nDefault flags: -c -w -l\n") - fmt.Print("\nFlags:\n") - flag.PrintDefaults() - fmt.Println() - } - - // parses the flags and fills the variables, - // Should be called before flags are accessed - // by program - flag.Parse() - - // set defaults if no flag provided - if flag.NFlag() == 0 { - logrus.Info("No flags provided, using defaults...") - getBytes = true - getLines = true - getWords = true + if RegisterAndParse(opts); NoFlags() { + SetDefaults(opts) } inStatus, err := os.Stdin.Stat() @@ -112,30 +82,30 @@ func main() { logrus.Fatal(err) } - if getBytes { + if opts.getByte { processBytes(buffer[:n], myFile) } - if getLines { + if opts.getLine { processLines(buffer[:n], myFile) } - if getWords { + if opts.getWord { processWords(buffer[:n], myFile, &inWord) } - if getRunes { + if opts.getRune { processRunes(buffer[:n], myFile, &leftOver) } } - if getBytes { + if opts.getByte { logrus.Println(myFile.Bytes) } - if getLines { + if opts.getLine { logrus.Println(myFile.Lines) } - if getWords { + if opts.getWord { logrus.Println(myFile.Words) } - if getRunes { + if opts.getRune { logrus.Println(myFile.Runes) } } diff --git a/challenge-01/flags.go b/challenge-01/flags.go new file mode 100644 index 0000000..78b90b9 --- /dev/null +++ b/challenge-01/flags.go @@ -0,0 +1,60 @@ +// Handles flag related work. +// cli related code + +package main + +import ( + "flag" + "fmt" +) + +type Opts struct { + // getByte holds the value of '-c' flag. + getByte bool + // getLine holds the value of '-l' flag. + getLine bool + // getWord holds the value of '-w' flag. + getWord bool + // getRune holds the value of '-m' flag. + getRune bool +} + +// NoFlags checks list of flags, +// returns true if list is empty. +func NoFlags() bool { + + return flag.NFlag() == 0 +} + +// SetDefault sets default flags. +func SetDefaults(opts *Opts) { + + // Todo: add logs here("using default flags") + opts.getByte = true + opts.getLine = true + opts.getWord = true +} + +func RegisterAndParse(opts *Opts) { + + // Register + flag.BoolVar(&opts.getByte, "c", false, "print total bytes") + flag.BoolVar(&opts.getLine, "l", false, "print total lines") + flag.BoolVar(&opts.getWord, "w", false, "print total words") + flag.BoolVar(&opts.getRune, "m", false, "print total bytes(according to utf-8 encoding)") + + // Set usage + flag.Usage = func() { + + fmt.Print("\nUsage: ccwc [flag] [file_path]\n") + fmt.Print("\nDefault flags: -c -w -l\n") + fmt.Print("\nFlags:\n") + flag.PrintDefaults() + fmt.Println() // for readability + } + + // parses the flags and fills the variables, + // Should be called before flags are accessed + // by program + flag.Parse() +} \ No newline at end of file From 19f01b71971723775fded2d2832c95ad693f9213 Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 12:54:00 +0530 Subject: [PATCH 2/7] refactor(cli): separate function for usage and check Signed-off-by: Priyansh Sao --- challenge-01/flags.go | 131 +++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 60 deletions(-) diff --git a/challenge-01/flags.go b/challenge-01/flags.go index 78b90b9..36f9959 100644 --- a/challenge-01/flags.go +++ b/challenge-01/flags.go @@ -1,60 +1,71 @@ -// Handles flag related work. -// cli related code - -package main - -import ( - "flag" - "fmt" -) - -type Opts struct { - // getByte holds the value of '-c' flag. - getByte bool - // getLine holds the value of '-l' flag. - getLine bool - // getWord holds the value of '-w' flag. - getWord bool - // getRune holds the value of '-m' flag. - getRune bool -} - -// NoFlags checks list of flags, -// returns true if list is empty. -func NoFlags() bool { - - return flag.NFlag() == 0 -} - -// SetDefault sets default flags. -func SetDefaults(opts *Opts) { - - // Todo: add logs here("using default flags") - opts.getByte = true - opts.getLine = true - opts.getWord = true -} - -func RegisterAndParse(opts *Opts) { - - // Register - flag.BoolVar(&opts.getByte, "c", false, "print total bytes") - flag.BoolVar(&opts.getLine, "l", false, "print total lines") - flag.BoolVar(&opts.getWord, "w", false, "print total words") - flag.BoolVar(&opts.getRune, "m", false, "print total bytes(according to utf-8 encoding)") - - // Set usage - flag.Usage = func() { - - fmt.Print("\nUsage: ccwc [flag] [file_path]\n") - fmt.Print("\nDefault flags: -c -w -l\n") - fmt.Print("\nFlags:\n") - flag.PrintDefaults() - fmt.Println() // for readability - } - - // parses the flags and fills the variables, - // Should be called before flags are accessed - // by program - flag.Parse() -} \ No newline at end of file +// Handles flag related work. +// cli related code + +package main + +import ( + "flag" + "fmt" +) + +type Opts struct { + // getByte holds the value of '-c' flag. + getByte bool + // getLine holds the value of '-l' flag. + getLine bool + // getWord holds the value of '-w' flag. + getWord bool + // getRune holds the value of '-m' flag. + getRune bool +} + +// PrintUsage prints usage instructions. +func PrintUsage() { + + flag.Usage() +} + +// NoArgs returns true, if no arguments provided. +func NoArgs() bool { + + return flag.NArg() == 0 +} + +// NoFlags checks list of flags, returns true if list is empty. +func NoFlags() bool { + + return flag.NFlag() == 0 +} + +// SetDefault sets default flags. +func SetDefaults(opts *Opts) { + + // Todo: add logs here("using default flags") + opts.getByte = true + opts.getLine = true + opts.getWord = true +} + +func RegisterAndParse(opts *Opts) { + + // Register + flag.BoolVar(&opts.getByte, "c", false, "print total bytes") + flag.BoolVar(&opts.getLine, "l", false, "print total lines") + flag.BoolVar(&opts.getWord, "w", false, "print total words") + flag.BoolVar(&opts.getRune, "m", false, "print total bytes(according to utf-8 encoding)") + + // Set usage + flag.Usage = func() { + + fmt.Print("\nUsage: ccwc [flag] [file_path]\n") + fmt.Print("\nDefault flags: -c -w -l\n") + fmt.Print("\nFlags:\n") + flag.PrintDefaults() + fmt.Println() // for readability + } + + // parses the flags and fills the variables, + // Should be called before flags are accessed + // by program + flag.Parse() +} From 5f6848297245df50ed16920517842233340488ae Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 12:55:19 +0530 Subject: [PATCH 3/7] refactor(cli): add input handling logic in file.go Signed-off-by: Priyansh Sao --- challenge-01/file.go | 136 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 challenge-01/file.go diff --git a/challenge-01/file.go b/challenge-01/file.go new file mode 100644 index 0000000..b00251a --- /dev/null +++ b/challenge-01/file.go @@ -0,0 +1,136 @@ +// Handles file related work, +// cli related code + +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +// IpMode type represents +// input mode. +type IpMode int + +const ( + // IpModeTerminal represents terminal as input mode. + IpModeTerminal IpMode = iota + // IpModeStdin represents stdin as input mode + IpModeStdIn +) + +func getIPMode() (IpMode, error) { + + stdStat, err := os.Stdin.Stat() + if err != nil { + return 0, err + } + + if (stdStat.Mode() & os.ModeCharDevice) == 0 { + return IpModeStdIn, nil + } + + return IpModeTerminal, nil +} + +func readTerminalInput() (*os.File, error) { + var err error + + if NoArgs() { + logrus.Errorf("Cannot read input: %v", ErrNoArgProvided) + return nil, fmt.Errorf("Cannot read input: %w", ErrNoArgProvided) + } + + // get file path + filePath := flag.CommandLine.Arg(0) + // trim unwanted space + trimmedFPath := strings.TrimSpace(filePath) + if trimmedFPath == "" { + logrus.Errorf("Cannot read input: %v", ErrEmptyFilePath) + + return nil, fmt.Errorf("Cannot read input: %w", ErrEmptyFilePath) + } + + f, err := os.Open(filePath) + if err != nil { + logrus.Error(err) + return nil, fmt.Errorf("Cannot read input: %v", err) + } + + return f, nil +} + +// Format returns ready to print slices of flags and stats. +func Format(fstat *FileInfo, opts *Opts) ([]string, []int) { + flags := []string{} + stats := []int{} + + if opts.getByte { + flags = append(flags, "bytes") + stats = append(stats, fstat.Bytes) + } + if opts.getLine { + flags = append(flags, "lines") + stats = append(stats, fstat.Lines) + } + if opts.getWord { + flags = append(flags, "words") + stats = append(stats, fstat.Words) + } + if opts.getRune { + flags = append(flags, "runes") + stats = append(stats, fstat.Runes) + } + + return flags, stats +} + +// PrintCol prints the stats with their flag as table. +func PrintCol(flags []string, stats []int) { + + fmt.Println() + for _, v := range flags { + + fmt.Printf("%-8v", v) + } + + fmt.Println() + for _, s := range stats { + + fmt.Printf("%-8d", s) + } + + fmt.Println() +} + +// ReadInput reads input, provided by user. +func ReadInput() (*os.File, error) { + + ipMode, err := getIPMode() + if err != nil { + + logrus.Errorf("Could not get IP mode: %v", err) + return nil, err + } + + file := new(os.File) + + switch ipMode { + case IpModeTerminal: + file, err = readTerminalInput() + if err != nil { + PrintUsage() + return nil, err + } + + case IpModeStdIn: + file = os.Stdin + // Todo: add default case + } + + return file, nil +} From 2d56f03cfb0128e4a441e8548fcea85bfe4f16ce Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 12:58:37 +0530 Subject: [PATCH 4/7] refactor(error): add defined errors in errors.go Signed-off-by: Priyansh Sao --- challenge-01/errors.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 challenge-01/errors.go diff --git a/challenge-01/errors.go b/challenge-01/errors.go new file mode 100644 index 0000000..334fdca --- /dev/null +++ b/challenge-01/errors.go @@ -0,0 +1,12 @@ +// Handles error related work + +package main + +import "errors" + +var ( + // ErrEmptyFilePath indicates, provided file path is empty. + ErrEmptyFilePath error = errors.New("empty file path.") + // ErrNoArgProvided indicates, no argument is provided. + ErrNoArgProvided error = errors.New("no argument provided.") +) From b567093d0de9f7b0fa54486ba303b9b7b93b989a Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 12:59:55 +0530 Subject: [PATCH 5/7] refactor(ccwc): add computation logic in compute.go Signed-off-by: Priyansh Sao --- challenge-01/compute.go | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 challenge-01/compute.go diff --git a/challenge-01/compute.go b/challenge-01/compute.go new file mode 100644 index 0000000..301e272 --- /dev/null +++ b/challenge-01/compute.go @@ -0,0 +1,108 @@ +// handles the main business logic + +package main + +import ( + "bytes" + "io" + "os" + "unicode/utf8" +) + +// FileInfo holds stats of a file. +type FileInfo struct { + // count of bytes + Bytes int + // count of lines + Lines int + // count of words + Words int + // count of unicode chars + Runes int +} + +func isSpace(b byte) bool { + // simple ASCII check + return b == ' ' || b == '\n' || b == '\t' || b == '\r' +} + +func processBytes(buffer []byte, file *FileInfo) { + + if len(buffer) > 0 { + file.Bytes += len(buffer) + } +} + +func processLines(buffer []byte, file *FileInfo) { + // counts \n in buffer + file.Lines += bytes.Count(buffer, []byte{'\n'}) +} + +func processWords(buffer []byte, file *FileInfo, inWord *bool) { + + for _, b := range buffer { + if isSpace(b) { + *inWord = false + } else { + + if !*inWord { + file.Words++ + *inWord = true + } + } + } +} + +func processRunes(buffer []byte, file *FileInfo, leftOver *[]byte) { + + buffer = append(*leftOver, buffer...) + *leftOver = (*leftOver)[:0] + + i := 0 + for i < len(buffer) { + if !utf8.FullRune(buffer[i:]) { + *leftOver = append(*leftOver, buffer[i:]...) + break + } + // no need to err check + // as we are sure there is atleast 1 rune ahed + _, size := utf8.DecodeRune(buffer[i:]) + + file.Runes++ + i += size + } +} + +// Compute computes the file stats according to the flags provided. +func Compute(f *os.File, opts *Opts) (*FileInfo, error) { + + fstat := new(FileInfo) + buffer := make([]byte, 32*1024) //32kB + leftOver := []byte{} + inWord := false + + for { + n, err := f.Read(buffer) + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + if opts.getByte { + processBytes(buffer[:n], fstat) + } + if opts.getLine { + processLines(buffer[:n], fstat) + } + if opts.getWord { + processWords(buffer[:n], fstat, &inWord) + } + if opts.getRune { + processRunes(buffer[:n], fstat, &leftOver) + } + } + + return fstat, nil +} From ca48f1dc0a849d00532e5094a7be20c48d586853 Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 13:01:32 +0530 Subject: [PATCH 6/7] refactor(ccwc): use refactored functions in ccwc.go Signed-off-by: Priyansh Sao --- challenge-01/ccwc.go | 162 +++++-------------------------------------- 1 file changed, 17 insertions(+), 145 deletions(-) diff --git a/challenge-01/ccwc.go b/challenge-01/ccwc.go index 1d0ef3e..8db3be2 100644 --- a/challenge-01/ccwc.go +++ b/challenge-01/ccwc.go @@ -1,171 +1,43 @@ package main import ( - "bytes" - "flag" - "io" "os" - "strings" - "unicode/utf8" "github.com/sirupsen/logrus" ) -type FileInfo struct { - // count of bytes - Bytes int - // count of lines - Lines int - // count of words - Words int - // count of unicode chars - Runes int +func formatLogs() { + + // remove unwanted things and enforce colors + logrus.SetFormatter(&logrus.TextFormatter{ + DisableTimestamp: true, + ForceColors: true, + PadLevelText: true, + }) } func main() { - // setup logger + formatLogs() - myFile := new(FileInfo) opts := new(Opts) if RegisterAndParse(opts); NoFlags() { SetDefaults(opts) } - inStatus, err := os.Stdin.Stat() + file, err := ReadInput() if err != nil { - logrus.Fatal(err) - } - - var file *os.File - - if (inStatus.Mode() & os.ModeCharDevice) == 0 { - file = os.Stdin - } else { - - // stop if no args - if flag.NArg() == 0 { - logrus.Errorln("No argument provided") - flag.Usage() - os.Exit(1) - } - - // get the first arg, - // should be a file path - filePath := flag.CommandLine.Arg(0) - // trim unwanted space - trimmedFPath := strings.TrimSpace(filePath) - - // empty check - if trimmedFPath != "" { - file, err = os.Open(filePath) - if err != nil { - logrus.Fatal(err) - } - defer file.Close() - } else { - logrus.Fatal("invalid argument: empty file path.") - } - } - - buffer := make([]byte, 32*1024) //32kB - leftOver := []byte{} - inWord := false - - for { - n, err := file.Read(buffer) - if err != nil { - if err == io.EOF { - break - } - logrus.Fatal(err) - } - - if opts.getByte { - processBytes(buffer[:n], myFile) - } - if opts.getLine { - processLines(buffer[:n], myFile) - } - if opts.getWord { - processWords(buffer[:n], myFile, &inWord) - } - if opts.getRune { - processRunes(buffer[:n], myFile, &leftOver) - } - } - - if opts.getByte { - logrus.Println(myFile.Bytes) - } - if opts.getLine { - logrus.Println(myFile.Lines) - } - if opts.getWord { - logrus.Println(myFile.Words) - } - if opts.getRune { - logrus.Println(myFile.Runes) + os.Exit(1) } -} - -func processBytes(buffer []byte, file *FileInfo) { - - if len(buffer) > 0 { - file.Bytes += len(buffer) - } -} - -func processLines(buffer []byte, file *FileInfo) { - // counts \n in buffer - file.Lines += bytes.Count(buffer, []byte{'\n'}) -} - -func processWords(buffer []byte, file *FileInfo, inWord *bool) { - for _, b := range buffer { - if isSpace(b) { - *inWord = false - } else { - if !*inWord { - file.Words++ - *inWord = true - } - } - } -} - -func processRunes(buffer []byte, file *FileInfo, leftOver *[]byte) { - - buffer = append(*leftOver, buffer...) - *leftOver = (*leftOver)[:0] - i := 0 - for i < len(buffer) { - if !utf8.FullRune(buffer[i:]) { - *leftOver = append(*leftOver, buffer[i:]...) - break - } - // no need to err check - // as we are sure there is atleast 1 rune ahed - _, size := utf8.DecodeRune(buffer[i:]) + fstat, err := Compute(file, opts) + if err != nil { - file.Runes++ - i += size + logrus.Errorf("Unable to compute file stats: %v", err) + os.Exit(1) } -} - -func isSpace(b byte) bool { - // simple ASCII check - return b == ' ' || b == '\n' || b == '\t' || b == '\r' -} - -func formatLogs() { - // remove unwanted things and enforce colors - logrus.SetFormatter(&logrus.TextFormatter{ - DisableTimestamp: true, - ForceColors: true, - PadLevelText: true, - }) + flags, stat := Format(fstat, opts) + PrintCol(flags, stat) } From 632a9f1927e7e9f5baed8d82287092329361a26d Mon Sep 17 00:00:00 2001 From: Priyansh Sao Date: Mon, 4 May 2026 13:02:15 +0530 Subject: [PATCH 7/7] refactor(test): rename ccwc_test.go to compute_test.go Signed-off-by: Priyansh Sao --- challenge-01/{ccwc_test.go => compute_test.go} | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) rename challenge-01/{ccwc_test.go => compute_test.go} (97%) diff --git a/challenge-01/ccwc_test.go b/challenge-01/compute_test.go similarity index 97% rename from challenge-01/ccwc_test.go rename to challenge-01/compute_test.go index 6320f2a..f8caf25 100644 --- a/challenge-01/ccwc_test.go +++ b/challenge-01/compute_test.go @@ -139,49 +139,49 @@ func Test_ProcessWords(t *testing.T) { } func Test_ProcessRunes(t *testing.T) { - + r := []byte("😍") // This is an invalid utf-8 character invalidUTF8 := []byte{0xFF} type step struct { - input []byte + input []byte leftOver []byte } tests := []struct { - name string - steps []step - expected int + name string + steps []step + expected int }{ { name: "incomplete rune", steps: []step{ {input: r[:2], leftOver: r[:2]}, {input: r[2:], leftOver: []byte{}}, - }, + }, expected: 1, }, { name: "ascii characters", steps: []step{ {input: []byte("abcd"), leftOver: nil}, - }, + }, expected: 4, }, { name: "incomplete end", steps: []step{ {input: r[:2], leftOver: r[:2]}, - }, + }, expected: 0, }, { name: "invalid utf-8", steps: []step{ {input: invalidUTF8, leftOver: nil}, - }, + }, expected: 1, }, }