Writing our version of linux wc

2026-01-01

Golang
Coding Challenges

GitHub Github: Click to go to the code

Challenge: Click to go to the challenge

We have all at some point used the Linux wc command line tool while developing something, but have you ever thought of making it?

Today we will be tackling how to build our own version of wc offering most of the functionality:

  • Counting lines
  • Counting words
  • Counting characters
  • Measuring the number of bytes

Using the beloved Golang.

Left off

start by initilizing your project and create a main.go file as our entry point:

mkdir ccwc
cd ./ccwc
go mod init ccwc
touch main.go

Step 1: Counting Bytes

starting off we will need to recived command line argument from our program, so we will utilize the flag package which is helpfull in parsing CLI argumnets:

wc/main.go
import "flag"
 
var showBytes bool
 
func init() {
	flag.BoolVar(&showBytes, "c", false, "outputs the number of bytes")
	flag.Parse()
}

Now, we are going to open the specified file path, pass the file as a reader for a function that calculates it’s size and returns an int representing the number of bytes.

wc/main.go
import "io"
 
func main(){
	var reader io.ReadSeeker
 
    filePath := flag.Arg(0)
    file, err := os.Open(filePath)
 
    // make sure to close the file at the end
    defer func() {
        if err = file.Close(); err != nil {
            throwErr("Error while closing file: ", err)
        }
    }()
 
    if err != nil {
        throwErr("Error opening the specified file: ", err)
    }
 
    reader = file
}
 
// A function that calculates the number of bytes in a reader
func calculateSize(reader io.ReadSeeker) int64 {
	defer reader.Seek(0, io.SeekStart)
 
	buf := &bytes.Buffer{}
	nRead, _ := io.Copy(buf, reader)
 
	return nRead
}

And now we will modifiy our main function to check if the user has requested to show the size of bytes using the -c flag or not.

wc/main.go
 
func main(){
	var reader io.ReadSeeker
 
    filePath := flag.Arg(0)
    file, err := os.Open(filePath)
 
    // make sure to close the file at the end
    defer func() {
        if err = file.Close(); err != nil {
            throwErr("Error while closing file: ", err)
        }
    }()
 
    if err != nil {
        throwErr("Error opening the specified file: ", err)
    }
 
    reader = file
 
	output := ""
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
	fmt.Println(output)
}
 

Until now are not printing the file path to the user as it’s done in the original wc. that is because the user might not pass a file path and instead pass the file contents directly through piping:

cat test.txt | ccwc

This is the reason also we are treating the file as a reader while we might have just use the file.Stat() struct.

This is something we will tackle in the final step don’t worry.

Step 2: Counting Lines

Now, we will modify our init function to accept the -l option.

wc/main.go
var showBytes bool
var showNumOfLines bool
 
func init() {
	flag.BoolVar(&showBytes, "c", false, "outputs the number of bytes")
	flag.BoolVar(&showNumOfLines, "l", false, "outputs the number of lines")
	flag.Parse()
}

And we will add the function that calculates the number of lines from a reader

wc/main.go
// ...
func calculateNumOfLines(reader io.ReadSeeker) int {
	defer reader.Seek(0, io.SeekStart)
 
	numOfLines := 0
	scanner := bufio.NewScanner(reader)
 
	for scanner.Scan() {
		numOfLines++
	}
	return numOfLines
}

on thing left is the modify our main to show the number of lines on demand.

wc/main.go
// ...
 
func main(){
	var reader io.ReadSeeker
 
    filePath := flag.Arg(0)
    file, err := os.Open(filePath)
 
    // make sure to close the file at the end
    defer func() {
        if err = file.Close(); err != nil {
            throwErr("Error while closing file: ", err)
        }
    }()
 
    if err != nil {
        throwErr("Error opening the specified file: ", err)
    }
 
    reader = file
 
	output := ""
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
	if showNumOfLines {
		output += fmt.Sprintf("%d ", calculateNumOfLines(reader))
	}
 
	fmt.Println(output)
}
 
// ...
 

Step 3: Counting Words

We will wake the same path as we did before:

  • modify init to recieve the CLI argument -w
  • create the function the calculates the number of words
  • modify main to show the output on demand
var showBytes bool
var showNumOfLines bool
var showNumOfWords bool
 
func init() {
	flag.BoolVar(&showBytes, "c", false, "outputs the number of bytes")
	flag.BoolVar(&showNumOfLines, "l", false, "outputs the number of lines")
	flag.BoolVar(&showNumOfWords, "w", false, "outputs the number of words")
	flag.Parse()
}
 
func main() {
	var reader io.ReadSeeker
 
    filePath := flag.Arg(0)
    file, err := os.Open(filePath)
 
    // make sure to close the file at the end
    defer func() {
        if err = file.Close(); err != nil {
            throwErr("Error while closing file: ", err)
        }
    }()
 
    if err != nil {
        throwErr("Error opening the specified file: ", err)
    }
 
    reader = file
 
	output := ""
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
	if showNumOfLines {
		output += fmt.Sprintf("%d ", calculateNumOfLines(reader))
	}
 
	if showNumOfWords {
		output += fmt.Sprintf("%d ", calculateNumOfWords(reader))
	}
 
	fmt.Println(output)
}
 
// ...
 
func calculateNumOfWords(reader io.ReadSeeker) int {
	defer reader.Seek(0, io.SeekStart)
 
	numOfWords := 0
	scanner := bufio.NewScanner(reader)
	scanner.Split(bufio.ScanWords)
 
	for scanner.Scan() {
		numOfWords++
	}
	return numOfWords
}

Good job, we are half way there!

On to..

Step 4: Counting Characters

You already guessed it:

wc/main.go
var showBytes bool
var showNumOfLines bool
var showNumOfWords bool
var showNumOfChars bool
 
func init() {
	flag.BoolVar(&showBytes, "c", false, "outputs the number of bytes")
	flag.BoolVar(&showNumOfLines, "l", false, "outputs the number of lines")
	flag.BoolVar(&showNumOfWords, "w", false, "outputs the number of words")
	flag.BoolVar(&showNumOfChars, "m", false, "outputs the number of characters")
	flag.Parse()
}
 
func main() {
 
    // ...
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
	if showNumOfLines {
		output += fmt.Sprintf("%d ", calculateNumOfLines(reader))
	}
 
	if showNumOfWords {
		output += fmt.Sprintf("%d ", calculateNumOfWords(reader))
	}
 
	if showNumOfChars {
		output += fmt.Sprintf("%d ", calculateNumOfChars(reader))
	}
 
	fmt.Println(output)
}
 
// ...
 
func calculateNumOfChars(reader io.ReadSeeker) int {
	defer reader.Seek(0, io.SeekStart)
 
	numOfChars := 0
	scanner := bufio.NewScanner(reader)
	scanner.Split(bufio.ScanRunes)
 
	for scanner.Scan() {
		numOfChars++
	}
	return numOfChars
}
 

Step 5: Empty as void

In this scennario we will be handling when the user doesn’t pass any arguments, we will by default show all output.

This can be done easly by a simple if statement:

wc/main.go
func main() {
 
    // ...
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
	if showNumOfLines {
		output += fmt.Sprintf("%d ", calculateNumOfLines(reader))
	}
 
	if showNumOfWords {
		output += fmt.Sprintf("%d ", calculateNumOfWords(reader))
	}
 
	if showNumOfChars {
		output += fmt.Sprintf("%d ", calculateNumOfChars(reader))
	}
 
	if !showBytes && !showNumOfLines && !showNumOfWords && !showNumOfChars {
		output += fmt.Sprintf("%d %d %d ", calculateSize(reader), calculateNumOfLines(reader), calculateNumOfWords(reader))
	}
 
	fmt.Println(output)
}

The grand finale

Until now, we didn’t ouput the name of the file path passed to the CLI, because we kept in mind this step.

We will add a condition to check if the user has passed a file path or is piping the file contents directly to our CLI:

  • If the user has passed a file path we will read the file, pass it to our program because it already implements the ReadSeaker interface, and will output the path at the end.
  • If the user piped the file contents directly, we will turn the contents into a reader and we will not output the file path ” as it is done in the original wc”, because there is no way of knowing the original file path.
wc/main.go
func main() {
    var reader io.ReadSeeker
    var fileName string
 
	if len(flag.Args()) == 0 {
		b, _ := io.ReadAll(os.Stdin)
		reader = bytes.NewReader(b)
	} else {
		filePath := flag.Arg(0)
		file, err := os.Open(filePath)
 
		// make sure to close the file at the end
		defer func() {
			if err = file.Close(); err != nil {
				throwErr("Error while closing file: ", err)
			}
		}()
 
		if err != nil {
			throwErr("Error opening the specified file: ", err)
		}
 
		reader = file
		fileStat, _ := file.Stat()
		fileName = fileStat.Name()
	}
 
	output := ""
 
	if showBytes {
		output += fmt.Sprintf("%d ", calculateSize(reader))
	}
 
    // ...
 
	if fileName != "" {
		output += fmt.Sprintf("%v", fileName)
	}
 
	fmt.Println(output)
}

CleanUp

Our code looks a little bit clunky now, so we can extract some functioanlity into a utils.go file which will contain specficially the calculate functions.