From 2aa1008b327c562eef8f5e6654c460509e83925c Mon Sep 17 00:00:00 2001 From: ciao Date: Sun, 13 Apr 2025 07:59:33 +0800 Subject: [PATCH 1/4] chore: refactor wc --- src/main.cpp | 118 ++++++++++++++++++++++++++++++++++++++++-------- src/options.cpp | 82 ++++++++++++++++++++++++++------- src/options.hpp | 15 ++++++ src/params.cpp | 37 ++++++++++++--- src/params.hpp | 11 +++-- src/wc.cpp | 11 ++++- src/wc.hpp | 1 + 7 files changed, 228 insertions(+), 47 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 239e446..542f250 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,24 +2,55 @@ #include #include #include +#include #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; } @@ -32,7 +63,29 @@ int main(int argc, char* argv[]) { return 0; } - if (options.files.empty()) { + if (options.show_version) { + std::cout << "wc (GNU coreutils) 0.1\n"; + return 0; + } + + // Handle files0-from option + std::vector 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()); @@ -44,20 +97,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; diff --git a/src/options.cpp b/src/options.cpp index b37a9d4..86838a8 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -2,6 +2,7 @@ #include "params.hpp" #include #include +#include namespace wc { @@ -11,7 +12,18 @@ 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 { @@ -19,16 +31,19 @@ Options OptionParser::parse(int argc, char* argv[]) { } } - // 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; @@ -37,26 +52,53 @@ Options OptionParser::parse(int argc, char* argv[]) { return options; } +std::pair 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)); } } } @@ -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)); } } diff --git a/src/options.hpp b/src/options.hpp index 15a5f00..1526d81 100644 --- a/src/options.hpp +++ b/src/options.hpp @@ -2,14 +2,27 @@ #include #include +#include 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 files; }; @@ -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 split_long_option(const std::string& opt); }; } // namespace wc \ No newline at end of file diff --git a/src/params.cpp b/src/params.cpp index ac1a881..54d0d27 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -1,19 +1,42 @@ #include "params.hpp" #include #include +#include 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"; } } \ No newline at end of file diff --git a/src/params.hpp b/src/params.hpp index b87523e..f8c991f 100644 --- a/src/params.hpp +++ b/src/params.hpp @@ -11,11 +11,16 @@ struct CommandParam { std::string_view description; }; -constexpr std::array COMMAND_PARAMS = {{ +constexpr std::array COMMAND_PARAMS = {{ + {"-c", "--bytes", "print the byte counts"}, + {"-m", "--chars", "print the character counts"}, {"-l", "--lines", "print the newline counts"}, + {"", "--files0-from=F", "read input from the files specified by NUL-terminated names in file F"}, + {"-L", "--max-line-length", "print the maximum display width"}, {"-w", "--words", "print the word counts"}, - {"-c", "--bytes", "print the byte counts"}, - {"-h", "--help", "display this help and exit"} + {"", "--total=WHEN", "when to print a line with total counts; WHEN can be: auto, always, only, never"}, + {"-V", "--version", "output version information and exit"}, + {"", "--help", "display this help and exit"}, }}; void print_usage(const char* program_name); diff --git a/src/wc.cpp b/src/wc.cpp index a37b1f9..2b02b87 100644 --- a/src/wc.cpp +++ b/src/wc.cpp @@ -3,13 +3,14 @@ #include #include #include +#include namespace wc { CountResult WordCounter::count_file(const std::filesystem::path& path) { std::ifstream file(path); if (!file.is_open()) { - throw std::runtime_error("Failed to open file: " + path.string()); + throw std::runtime_error(std::format("Failed to open file: {}", path.string())); } std::stringstream buffer; @@ -24,13 +25,20 @@ CountResult WordCounter::count_string(std::string_view content) { CountResult WordCounter::count_content(std::string_view content) { CountResult result{}; result.bytes = content.size(); + result.max_line_length = 0; bool in_word = false; + size_t current_line_length = 0; + for (char c : content) { result.characters++; if (c == '\n') { result.lines++; + result.max_line_length = std::max(result.max_line_length, current_line_length); + current_line_length = 0; + } else { + current_line_length++; } if (std::isspace(c)) { @@ -44,6 +52,7 @@ CountResult WordCounter::count_content(std::string_view content) { // Count the last line if it doesn't end with a newline if (!content.empty() && content.back() != '\n') { result.lines++; + result.max_line_length = std::max(result.max_line_length, current_line_length); } return result; diff --git a/src/wc.hpp b/src/wc.hpp index 6c76cbd..24f570c 100644 --- a/src/wc.hpp +++ b/src/wc.hpp @@ -10,6 +10,7 @@ struct CountResult { size_t words; size_t characters; size_t bytes; + size_t max_line_length; }; class WordCounter { From 1870e6665678610bb30b4091dab7f6b53a60a772 Mon Sep 17 00:00:00 2001 From: ciao Date: Sun, 13 Apr 2025 08:04:39 +0800 Subject: [PATCH 2/4] fix: add version option support --- .github/workflows/ci.yml | 7 ++++++- src/main.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3a0fe06..1c60c45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,12 @@ permissions: jobs: build: - runs-on: ubuntu-latest + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v4 - uses: mlugg/setup-zig@v1 diff --git a/src/main.cpp b/src/main.cpp index 542f250..8fdaa2c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -64,7 +64,12 @@ int main(int argc, char* argv[]) { } if (options.show_version) { - std::cout << "wc (GNU coreutils) 0.1\n"; + 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; } From 4febdf9f56ac90667ccf67737fba01cac5965992 Mon Sep 17 00:00:00 2001 From: ciao Date: Sun, 13 Apr 2025 08:15:21 +0800 Subject: [PATCH 3/4] ci: cross compile for multiple platforms --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c60c45..f00c05e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,15 +12,15 @@ permissions: jobs: build: - name: Build on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest] + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: mlugg/setup-zig@v1 with: version: 0.14.0 - - run: zig build \ No newline at end of file + - run: | + zig build -Dtarget=x86_64-windows + zig build -Dtarget=aarch64-windows + zig build -Dtarget=x86_64-linux + zig build -Dtarget=aarch64-linux + zig build -Dtarget=aarch64-macos \ No newline at end of file From 028190816f6eea314aee476e6cc906d4720f39e2 Mon Sep 17 00:00:00 2001 From: ciao Date: Sun, 13 Apr 2025 08:21:52 +0800 Subject: [PATCH 4/4] ci: simplify ci workflow --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f00c05e..7b09a0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,5 +22,4 @@ jobs: zig build -Dtarget=x86_64-windows zig build -Dtarget=aarch64-windows zig build -Dtarget=x86_64-linux - zig build -Dtarget=aarch64-linux zig build -Dtarget=aarch64-macos \ No newline at end of file