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
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ jobs:
- uses: mlugg/setup-zig@v1
with:
version: 0.14.0
- run: zig build
- run: |
zig build -Dtarget=x86_64-windows
zig build -Dtarget=aarch64-windows
zig build -Dtarget=x86_64-linux
zig build -Dtarget=aarch64-macos
123 changes: 104 additions & 19 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,55 @@
#include <format>
#include <filesystem>
#include <sstream>
#include <fstream>
#include "wc.hpp"
#include "options.hpp"
#include "params.hpp"

std::string format_output(const wc::CountResult& result, const wc::Options& options, const std::string& filename = "") {
std::string format_output(const wc::CountResult& result, const wc::Options& options, const std::string& filename = "", bool is_total = false) {
std::string output;
if (options.show_lines) {
output += std::format("{:>8}", result.lines);
}
if (options.show_words) {
output += std::format("{:>8}", result.words);
}
if (options.show_bytes) {
output += std::format("{:>8}", result.bytes);
}
if (!filename.empty()) {
output += " " + filename;

// For 'only' mode, don't add leading spaces
if (options.total == wc::TotalWhen::Only && is_total) {
if (options.show_lines) {
output += std::format("{:>8}", result.lines);
}
if (options.show_words) {
output += std::format("{:>8}", result.words);
}
if (options.show_bytes) {
output += std::format("{:>8}", result.bytes);
}
if (options.show_chars) {
output += std::format("{:>8}", result.characters);
}
if (options.show_max_line_length) {
output += std::format("{:>8}", result.max_line_length);
}
} else {
// Standard formatting with leading spaces
if (options.show_lines) {
output += std::format("{:>8}", result.lines);
}
if (options.show_words) {
output += std::format("{:>8}", result.words);
}
if (options.show_bytes) {
output += std::format("{:>8}", result.bytes);
}
if (options.show_chars) {
output += std::format("{:>8}", result.characters);
}
if (options.show_max_line_length) {
output += std::format("{:>8}", result.max_line_length);
}

// Add filename or "total" label
if (!filename.empty()) {
output += " " + filename;
}
}

return output;
}

Expand All @@ -32,7 +63,34 @@ int main(int argc, char* argv[]) {
return 0;
}

if (options.files.empty()) {
if (options.show_version) {
std::cout << "wc 0.1\n"
<< "Copyright (C) 2025 guuzaa.\n"
<< "License Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0\n"
<< "This is free software: you are free to change and redistribute it.\n"
<< "There is NO WARRANTY, to the extent permitted by law.\n\n"
<< "Written by guuzaa (guuzaa@outlook.com).\n";
return 0;
}

// Handle files0-from option
std::vector<std::string> files_to_process = options.files;
if (!options.files0_from.empty()) {
std::ifstream file_list(options.files0_from);
if (!file_list.is_open()) {
throw std::runtime_error(std::format("Failed to open file list: {}", options.files0_from));
}

std::string line;
while (std::getline(file_list, line, '\0')) {
if (!line.empty()) {
files_to_process.push_back(line);
}
}
}

// If no files specified, read from stdin
if (files_to_process.empty()) {
std::stringstream buffer;
buffer << std::cin.rdbuf();
auto result = wc::WordCounter::count_string(buffer.str());
Expand All @@ -44,20 +102,47 @@ int main(int argc, char* argv[]) {
size_t total_words = 0;
size_t total_chars = 0;
size_t total_bytes = 0;
size_t total_max_line_length = 0;

for (const auto& file : options.files) {
// Process each file
for (const auto& file : files_to_process) {
auto result = wc::WordCounter::count_file(file);
std::cout << format_output(result, options, file) << std::endl;


// Update totals
total_lines += result.lines;
total_words += result.words;
total_chars += result.characters;
total_bytes += result.bytes;
total_max_line_length = std::max(total_max_line_length, result.max_line_length);

// Print individual file results if not in 'only' mode
if (options.total != wc::TotalWhen::Only) {
std::cout << format_output(result, options, file) << std::endl;
}
}

if (options.files.size() > 1) {
wc::CountResult total{total_lines, total_words, total_chars, total_bytes};
std::cout << format_output(total, options, "total") << std::endl;
// Handle total line based on the --total option
bool should_print_total = false;

switch (options.total) {
case wc::TotalWhen::Auto:
should_print_total = files_to_process.size() > 1;
break;
case wc::TotalWhen::Always:
should_print_total = true;
break;
case wc::TotalWhen::Only:
should_print_total = true;
break;
case wc::TotalWhen::Never:
should_print_total = false;
break;
}

if (should_print_total) {
wc::CountResult total{total_lines, total_words, total_chars, total_bytes, total_max_line_length};
std::string total_label = (options.total == wc::TotalWhen::Only) ? "" : "total";
std::cout << format_output(total, options, total_label, true) << std::endl;
}

return 0;
Expand Down
82 changes: 65 additions & 17 deletions src/options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "params.hpp"
#include <stdexcept>
#include <algorithm>
#include <format>

namespace wc {

Expand All @@ -11,24 +12,38 @@ Options OptionParser::parse(int argc, char* argv[]) {
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg.starts_with("--")) {
parse_long_option(arg, options);
if (arg.find("=") != std::string::npos) {
auto [opt, value] = split_long_option(arg);
if (opt == "--total") {
options.total = parse_total_when(value);
} else if (opt == "--files0-from") {
options.files0_from = value;
} else {
throw std::runtime_error(std::format("Invalid option with value: {}", opt));
}
} else {
parse_long_option(arg, options);
}
} else if (arg.starts_with("-") && arg != "-") {
parse_option(arg, options);
} else {
options.files.push_back(arg);
}
}

// If help is requested, ensure no other options or files are present
if (options.show_help) {
if (options.show_lines || options.show_words || options.show_bytes || !options.files.empty()) {
throw std::runtime_error("Error: -h/--help cannot be combined with other options or files.");
// If help or version is requested, ensure no other options or files are present
if (options.show_help || options.show_version) {
if (options.show_lines || options.show_words || options.show_bytes ||
options.show_chars || options.show_max_line_length || !options.files.empty() ||
!options.files0_from.empty()) {
throw std::runtime_error("Error: --help and -V/--version cannot be combined with other options or files.");
}
return options;
}

// If no options were specified (and help wasn't requested), show all counts
if (!options.show_lines && !options.show_words && !options.show_bytes) {
// If no options were specified (and help/version wasn't requested), show all counts
if (!options.show_lines && !options.show_words && !options.show_bytes &&
!options.show_chars && !options.show_max_line_length) {
options.show_lines = true;
options.show_words = true;
options.show_bytes = true;
Expand All @@ -37,26 +52,53 @@ Options OptionParser::parse(int argc, char* argv[]) {
return options;
}

std::pair<std::string, std::string> OptionParser::split_long_option(const std::string& opt) {
size_t pos = opt.find("=");
if (pos == std::string::npos) {
throw std::runtime_error(std::format("Invalid option format: {}", opt));
}
return {opt.substr(0, pos), opt.substr(pos + 1)};
}

TotalWhen OptionParser::parse_total_when(const std::string& value) {
if (value == "auto") return TotalWhen::Auto;
if (value == "always") return TotalWhen::Always;
if (value == "only") return TotalWhen::Only;
if (value == "never") return TotalWhen::Never;
throw std::runtime_error(std::format("Invalid value for --total: {}", value));
}

void OptionParser::parse_option(const std::string& opt, Options& options) {
for (char c : opt.substr(1)) {
bool found = false;
for (const auto& param : COMMAND_PARAMS) {
if (param.short_name == "-" + std::string(1, c)) {
if (c == 'h') {
options.show_help = true;
} else if (c == 'l') {
options.show_lines = true;
} else if (c == 'w') {
options.show_words = true;
} else if (c == 'c') {
options.show_bytes = true;
switch (c) {
case 'l':
options.show_lines = true;
break;
case 'w':
options.show_words = true;
break;
case 'c':
options.show_bytes = true;
break;
case 'm':
options.show_chars = true;
break;
case 'L':
options.show_max_line_length = true;
break;
case 'V':
options.show_version = true;
break;
}
found = true;
break;
}
}
if (!found) {
throw std::runtime_error("Invalid option: -" + std::string(1, c));
throw std::runtime_error(std::format("Invalid option: -{}", c));
}
}
}
Expand All @@ -73,13 +115,19 @@ void OptionParser::parse_long_option(const std::string& opt, Options& options) {
options.show_words = true;
} else if (opt == "--bytes") {
options.show_bytes = true;
} else if (opt == "--chars") {
options.show_chars = true;
} else if (opt == "--max-line-length") {
options.show_max_line_length = true;
} else if (opt == "--version") {
options.show_version = true;
}
found = true;
break;
}
}
if (!found) {
throw std::runtime_error("Invalid option: " + opt);
throw std::runtime_error(std::format("Invalid option: {}", opt));
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@

#include <string>
#include <vector>
#include <utility>

namespace wc {

enum class TotalWhen {
Auto,
Always,
Only,
Never
};

struct Options {
bool show_lines = false;
bool show_words = false;
bool show_bytes = false;
bool show_chars = false;
bool show_max_line_length = false;
bool show_help = false;
bool show_version = false;
std::string files0_from;
TotalWhen total = TotalWhen::Auto;
std::vector<std::string> files;
};

Expand All @@ -20,6 +33,8 @@ class OptionParser {
private:
static void parse_option(const std::string& opt, Options& options);
static void parse_long_option(const std::string& opt, Options& options);
static TotalWhen parse_total_when(const std::string& value);
static std::pair<std::string, std::string> split_long_option(const std::string& opt);
};

} // namespace wc
37 changes: 30 additions & 7 deletions src/params.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,42 @@
#include "params.hpp"
#include <iostream>
#include <filesystem>
#include <string>

namespace wc {

void print_usage(const char* program_name) {
std::string prog = std::filesystem::path(program_name).filename().string();
std::cout << "Usage: " << prog << " [OPTION]... [FILE]...\n";
std::cout << "Print newline, word, and byte counts for each FILE.\n\n";
std::cout << "Usage: " << program_name << " [OPTION]... [FILE]...\n"
<< " or: " << program_name << " [OPTION]... --files0-from=F\n"
<< "Print newline, word, and byte counts for each FILE, and a total line if\n"
<< "more than one FILE is specified. A word is a non-zero-length sequence of\n"
<< "printable characters delimited by white space.\n\n"
<< "With no FILE, or when FILE is -, read standard input.\n\n"
<< "The options below may be used to select which counts are printed, always in\n"
<< "the following order: newline, word, character, byte, maximum line length.\n";

// Print options in the correct order
for (const auto& param : COMMAND_PARAMS) {
std::cout << " " << param.short_name << ", " << param.long_name
<< "\t" << param.description << "\n";
// Format the option string with proper alignment
std::string opt_str;
if (!param.short_name.empty() && !param.long_name.empty()) {
opt_str = std::string(param.short_name) + ", " + std::string(param.long_name);
} else if (!param.long_name.empty()) {
opt_str = std::format(" {}", param.long_name);
} else {
continue; // Skip if both short and long names are empty
}

// Print the option with proper alignment
std::cout << " " << opt_str;

// Add padding for alignment
size_t padding = 30 - opt_str.length();
if (padding > 0) {
std::cout << std::string(padding, ' ');
}

std::cout << param.description << "\n";
}

std::cout << "\nWith no FILE, or when FILE is -, read standard input.\n";
}
}
Loading