Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 44 additions & 0 deletions .github/scripts/smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Smoke test for ccwc. Compiles must already have produced ./out (javac -d out src/ccwc/*.java).
# Verifies every distinct code path in Counter against the known-good values for test.txt
# documented in AGENTS.md / README.md: 7145 lines, 58164 words, 342190 bytes, 339292 chars.
#
# Runnable locally from the repo root: bash .github/scripts/smoke-test.sh
set -euo pipefail
cd "$(dirname "$0")/../.."

fail=0
check() { # desc expected actual
if [ "$3" = "$2" ]; then
echo "OK $1"
else
echo "FAIL $1 -- expected [$2], got [$3]"
fail=1
fi
}

# --- File input, single flag: exercises the Files.size() -c shortcut (Counter.java:41) ---
check "-c file" 342190 "$(java -cp out ccwc.Main -c test.txt | awk '{print $1}')"
check "-l file" 7145 "$(java -cp out ccwc.Main -l test.txt | awk '{print $1}')"
check "-w file" 58164 "$(java -cp out ccwc.Main -w test.txt | awk '{print $1}')"
check "-m file" 339292 "$(java -cp out ccwc.Main -m test.txt | awk '{print $1}')"

# --- File input, default combo: the columnar printf branch (Main.java:45-46) ---
check "default file" "7145 58164 342190" "$(java -cp out ccwc.Main test.txt | awk '{print $1, $2, $3}')"

# --- Stdin input, single flag: -c alone exercises the raw 8KB-loop shortcut (Counter.java:69-76) ---
check "-c stdin" 342190 "$(cat test.txt | java -cp out ccwc.Main -c | awk '{print $1}')"
check "-l stdin" 7145 "$(cat test.txt | java -cp out ccwc.Main -l | awk '{print $1}')"
check "-w stdin" 58164 "$(cat test.txt | java -cp out ccwc.Main -w | awk '{print $1}')"
check "-m stdin" 339292 "$(cat test.txt | java -cp out ccwc.Main -m | awk '{print $1}')"

# --- Stdin input, default combo ---
check "default stdin" "7145 58164 342190" "$(cat test.txt | java -cp out ccwc.Main | awk '{print $1, $2, $3}')"

# --- Stdin, -c combined with another flag: the ONLY path that exercises the
# CountingInputStream decorator (Counter.java:78-79) instead of either shortcut ---
result="$(cat test.txt | java -cp out ccwc.Main -c -l)"
check "-c -l stdin bytes (decorator path)" 342190 "$(sed -n 1p <<< "$result" | awk '{print $1}')"
check "-c -l stdin lines (decorator path)" 7145 "$(sed -n 2p <<< "$result" | awk '{print $1}')"

exit $fail
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: CI

on:
push:
branches: [master]
pull_request:

jobs:
build-and-verify:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
java: ['17', '24']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: ${{ matrix.java }}

- name: Compile
shell: bash
run: javac -d out src/ccwc/*.java

- name: Smoke test
shell: bash
run: bash .github/scripts/smoke-test.sh
106 changes: 106 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`ccwc` is a Java clone of the Unix `wc` command (a Coding Challenges exercise). It
counts bytes, lines, words, and characters in a file or from standard input.

## Build & run

There is no build tool (no Maven/Gradle) and no test suite. It is an IntelliJ IDEA
project that compiles with `javac` to `out/`. Run all commands from the repo root.

```bash
# Compile (mirrors what IntelliJ does)
javac -d out src/ccwc/*.java

# Run against a file
java -cp out ccwc.Main -l test.txt

# Run against stdin
cat test.txt | java -cp out ccwc.Main -w
```

`test.txt` is a sample input fixture (not a test). The project targets Java language
level 24 (`.idea/misc.xml`); a newer JDK also compiles it.

## Flags

`-c` bytes, `-l` lines, `-w` words, `-m` characters. With no flags, the default is
`-c -l -w` (matching `wc`). The first non-flag argument is the filename; if absent,
input is read from stdin.

## Architecture

Four classes in package `ccwc` (`src/ccwc/`):

- **`Main`** — entry point. Delegates parsing to `Options`, invokes `Counter`, then
formats output in `printResults`. Output has two modes: the classic aligned `wc`
format (`%8d %8d %8d filename`) only when exactly `-c -l -w` are active (and not
`-m`); otherwise one metric per line.
- **`Options`** — parses flags and holds them as public fields; applies the
"no flags → `-c -l -w`" default.
- **`Counter`** — the counting engine. Accumulates results in public fields
(`bytes`, `lines`, `words`, `chars`).
- **`CountingInputStream`** — a `FilterInputStream` that tallies bytes read.

**Key design — single pass (the reason `CountingInputStream` exists):** line, word,
and char counts are gathered by decoding the stream as UTF-8 through a
`BufferedReader` in one pass. Byte counting normally can't share that pass (the
reader consumes decoded chars, not raw bytes), so `Counter` wraps the raw stream in
`CountingInputStream` to tally bytes *underneath* the reader — one read pass yields
all four counts. Two shortcuts avoid reading data when possible:
- File + only `-c` → `Files.size(path)`, no read at all.
- Stdin + only `-c` → a raw byte loop, no character decoding.

`Counter` has two `count()` overloads (one taking a `Path`, one taking an
`InputStream`) that funnel into the private `countFromStream`.

## Known gotcha

When reading from **stdin** with any flag other than the default combination,
`printResults` still appends `opts.fileName`, which is `null` — so output looks like
`58164 null`. The aligned default-format branch handles the null filename correctly;
the per-metric branch does not.
166 changes: 166 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<a id="top"></a>

<div align="center">

# ccwc - A Java Implementation of the Unix `wc` Tool

[![Java](https://img.shields.io/badge/Java-17+-ED8B00?style=for-the-badge&logo=java&logoColor=white)](https://www.oracle.com/java/)
[![CI](https://github.com/MohammadRokib/wc-tool-java/actions/workflows/ci.yml/badge.svg)](https://github.com/MohammadRokib/wc-tool-java/actions/workflows/ci.yml)

`ccwc` (Coding Challenges Word Count) is a custom implementation of the classic Unix `wc` (word count) command-line utility, written entirely in Java. It is built to be memory-efficient, scalable for massive files, and fully compatible with standard Unix pipelines.

This project was built as part of the [Build Your Own wc Tool Challenge](https://codingchallenges.fyi/challenges/challenge-wc).

</div>

<p align="center">
<a href="#features">Explore the docs</a> ·
<a href="https://github.com/MohammadRokib/wc-tool-java/issues">Report Bug</a> ·
<a href="https://www.linkedin.com/in/your-profile/">LinkedIn</a> ·
<a href="mailto:your.email@example.com">Email</a>
</p>

---

## Features

- **`-c`**: Count bytes in a file or stream.
- **`-l`**: Count lines (newline characters).
- **`-w`**: Count words (sequences of characters delimited by whitespace).
- **`-m`**: Count characters (correctly handles multi-byte UTF-8 encoded text).
<br/>

- **Default Mode**: Output lines, words, and bytes simultaneously when no flag is provided.
- **Standard Input (stdin)**: Supports Unix piping (e.g., `cat file.txt | ccwc -l`).
- **Memory Safe**: Uses a streaming single-pass architecture. It can process files larger than available RAM without crashing.

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Prerequisites

- **Java Development Kit (JDK) 17** or higher.
- A terminal/command prompt environment.

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Installation & Building

Because this project uses standard Java libraries with no external dependencies, you can compile it directly using `javac`.

1. Clone the repository:
```bash
git clone https://github.com/MohammadRokib/wc-tool-java.git
cd ccwc
```

2. Compile the Java source files into an `out` directory:
```bash
# On Linux / macOS / Git Bash / Windows CMD
javac -d out src/ccwc/*.java
```

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Usage

The application is run via the `java` command, pointing to the `out` directory as the classpath.

### Syntax
```bash
java -cp out ccwc.Main [-c] [-l] [-w] [-m] [filename]
```

*If no `filename` is provided, the tool automatically reads from standard input (`stdin`).*

### Examples

**1. Count bytes in a file:**
```bash
$ java -cp out ccwc.Main -c test.txt
342190 test.txt
```

**2. Count lines in a file:**
```bash
$ java -cp out ccwc.Main -l test.txt
7145 test.txt
```

**3. Count words in a file:**
```bash
$ java -cp out ccwc.Main -w test.txt
58164 test.txt
```

**4. Count characters in a file (UTF-8 aware):**
```bash
$ java -cp out ccwc.Main -m test.txt
339292 test.txt
```

**5. Default mode (lines, words, bytes):**
```bash
$ java -cp out ccwc.Main test.txt
7145 58164 342190 test.txt
```

**6. Reading from Standard Input (Piping):**
When reading from `stdin`, the filename is omitted from the output.
```bash
$ cat test.txt | java -cp out ccwc.Main -l
7145
```

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Architecture & Design

Instead of reading entire files into memory (which causes `OutOfMemoryError` on large files), `ccwc` uses a **single-pass, streaming architecture**.

The project is divided into four main components:

1. **`Main.java`**: The entry point. Delegates argument parsing to `Options`, invokes the `Counter`, and formats the standard output using `System.out.printf`.
2. **`Options.java`**: A Data Transfer Object (DTO) that parses command-line arguments into boolean flags. If no flags are provided, it automatically enables the default metrics (lines, words, bytes).
3. **`Counter.java`**: The core engine. It features two entry points:
- `count(Path path, Options)`: For file inputs. Uses `Files.size()` for an instant O(1) byte count, avoiding unnecessary disk reads.
- `count(InputStream in, Options)`: For standard input. Uses a shared `countFromStream` method that loops through decoded characters exactly once, checking for line breaks, word boundaries, and character counts simultaneously.
4. **`CountingInputStream.java`**: Extends `FilterInputStream` (Decorator Pattern). When reading from `stdin`, this class sits at the bottom of the stream stack, intercepting raw bytes to tally the total byte count as they flow up to the character decoder.

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Environment Notes (PowerShell Users)

If you are testing the standard input byte count (`-c`) on Windows using **PowerShell**, you may notice a 3-byte discrepancy compared to reading the file directly (e.g., `342187` instead of `342190`).

**Why?** PowerShell's `cat` alias (`Get-Content`) decodes files into .NET strings and silently strips the 3-byte UTF-8 Byte Order Mark (BOM) before piping the data to external executables like `java.exe`.

Your Java code is correct. To verify raw byte piping on Windows, use **Command Prompt (`cmd.exe`)** with the `type` command, or use **Git Bash**:
```cmd
:: In cmd.exe
type test.txt | java -cp out ccwc.Main -c
```

<p align="right"><a href="#top">Back to top ⬆️</a></p>

---

## Contact

Mohammad Rokib

- **[LinkedIn](https://www.linkedin.com/in/m0hammadrokib/)**
- **[Email](mailto:mohammadrokibkhan@gmail.com)**
- **[GitHub](https://github.com/MohammadRokib)**
- **[Project Link: wc-tool-java](https://github.com/MohammadRokib/wc-tool-java)**

<p align="right"><a href="#top">Back to top ⬆️</a></p>
Loading
Loading