diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 53acb34..68db4c7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,8 +12,9 @@ jobs: with: submodules: true - - name: Install clang tools - run: make install_clang_tools + - run: make install_clang_tools + + - run: make configure - run: make fmt_check diff --git a/Makefile b/Makefile index 25ccde7..e4dfcf0 100644 --- a/Makefile +++ b/Makefile @@ -28,14 +28,6 @@ configure: ## Configure cmake build (debug, with compile_commands.json) -DCMAKE_CXX_COMPILER=clang++-$(CLANG_VERSION) \ -DCMAKE_CXX_FLAGS="--gcc-install-dir=$(GCC_INSTALL_DIR)" -build/compile_commands.json: CMakeLists.txt src/CMakeLists.txt - cmake -B build \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ - -DBUILD_MPQCLI=$(BUILD_MPQCLI) \ - -DCMAKE_CXX_COMPILER=clang++-$(CLANG_VERSION) \ - -DCMAKE_CXX_FLAGS="--gcc-install-dir=$(GCC_INSTALL_DIR)" - .PHONY: build build: ## Build via cmake cmake --build build @@ -111,40 +103,22 @@ fmt: ## Auto-fix C++ formatting with clang-format | xargs clang-format-$(CLANG_VERSION) -i .PHONY: lint_cpp -lint_cpp: build/compile_commands.json ## Run clang-tidy static analysis - clang-tidy-$(CLANG_VERSION) \ - --quiet -p build --header-filter="$(CURDIR)/src/.*" src/*.cpp +lint_cpp: ## Run clang-tidy static analysis (requires: make configure) + clang-tidy-$(CLANG_VERSION) --quiet -p build \ + --header-filter="$(CURDIR)/src/.*" src/*.cpp 2>&1 \ + | grep -v " warnings generated"; \ + exit $${PIPESTATUS[0]} .PHONY: lint lint: fmt_check lint_cpp ## Run all C++ linters .PHONY: ci -ci: fmt_check lint_cpp test ## Run all CI checks locally +ci: configure build fmt_check lint_cpp test ## Run all CI checks locally # CLEAN .PHONY: clean clean: build_clean test_clean ## Remove all build and test artifacts -# SUBMODULES -.PHONY: bump_stormlib -bump_stormlib: ## Bump StormLib submodule to latest remote - @read -rp "[*] Bump StormLib? (y/N) " yn; \ - case $$yn in \ - [yY] ) git submodule update --init --remote extern/StormLib;; \ - * ) echo "[*] Skipping...";; \ - esac - -.PHONY: bump_cli11 -bump_cli11: ## Bump CLI11 submodule to latest remote - @read -rp "[*] Bump CLI11? (y/N) " yn; \ - case $$yn in \ - [yY] ) git submodule update --init --remote extern/CLI11;; \ - * ) echo "[*] Skipping...";; \ - esac - -.PHONY: bump_submodules -bump_submodules: bump_stormlib bump_cli11 ## Bump all submodules to latest remote - # GET .PHONY: get_project_version get_project_version: ## Print the project version from CMakeLists.txt diff --git a/README.md b/README.md index 20d2b8f..2b73322 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ![Release Version](https://img.shields.io/github/v/release/thegraydot/mpqcli?style=flat) -![Release downloads](https://img.shields.io/github/downloads/thegraydot/mpqcli/total?label=release_downloads) ![Package downloads](https://img.shields.io/badge/package_downloads-845-green) +![Release downloads](https://img.shields.io/github/downloads/thegraydot/mpqcli/total?label=release_downloads) ![Package downloads](https://img.shields.io/badge/package_downloads-894-green) A command-line tool to create, add, remove, list, extract, read, and verify MPQ archives using the [StormLib library](https://github.com/ladislav-zezula/StormLib). @@ -19,7 +19,7 @@ A command-line tool to create, add, remove, list, extract, read, and verify MPQ - Pipe the output to `grep` or other tools to search, filter, or process files - Redirect output to files or other commands for further automation -If you require an MPQ tool with a graphical interface (GUI) and explicit support for more MPQ archive versions — I would recommend using [Ladik's MPQ Editor](http://www.zezula.net/en/mpq/download.html). +If you require an MPQ tool with a graphical interface (GUI) and explicit support for more MPQ archive versions, I would recommend using [Ladik's MPQ Editor](http://www.zezula.net/en/mpq/download.html). ## Releases diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 9e02607..dd8d3d9 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -5,6 +5,7 @@ - [Commands](./commands_list.md) - [version](./commands/version.md) - [about](./commands/about.md) + - [completion](./commands/completion.md) - [info](./commands/info.md) - [create](./commands/create.md) - [add](./commands/add.md) @@ -13,6 +14,7 @@ - [extract](./commands/extract.md) - [read](./commands/read.md) - [verify](./commands/verify.md) + - [compact](./commands/compact.md) - [Advanced Examples](./advanced.md) - [Building](./building.md) - [Contributing](./contributing.md) diff --git a/docs/changelog.md b/docs/changelog.md index 474e4d2..627c9e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,36 @@ # Changelog +## 0.10.0 - 2026-06-07 + +### Added + +- Compact subcommand to compress MPQ archive after creation/editing +- Support passing multiple files to the `add` and `remove` subcommands +- Support reading file paths from stdin in the `add` and `remove` subcommands +- Support adding entire directories with the `add` subcommand +- Add an update flag to the `add` subcommand to skip files whose archived size matches the on-disk size + +### Fixed + +- `extract` command now reports an error when the output directory cannot be created +- Path traversal guard in `extract` uses fully resolved paths, closing a potential bypass +- Crash when reading strong signatures from malformed or truncated archives +- Docker glibc image updated to ubuntu:24.04 +- Adding files is now ordered and operating system agnostic +- `add` subcommand now returns non-zero exit codes on failure +- Error messages now include StormLib error codes +- Duplicate error messages removed +- Makefile build targets corrected in `CONTRIBUTING.md` + +### Changed + +- `--filename-in-archive` and `--directory-in-archive` replaced by a unified `--path` flag +- The path flag on add now acts as an archive path prefix when a directory is given + +### Thanks + +- Thanks to @sjoblomj for the contributions in this release + ## 0.9.9 - 2026-04-05 ### Fixed diff --git a/docs/commands.md b/docs/commands.md deleted file mode 100644 index 9bf37da..0000000 --- a/docs/commands.md +++ /dev/null @@ -1,3 +0,0 @@ -# Commands - -- `add` \ No newline at end of file diff --git a/docs/commands/about.md b/docs/commands/about.md index 7e36b2b..bae7a6e 100644 --- a/docs/commands/about.md +++ b/docs/commands/about.md @@ -5,7 +5,7 @@ Print information about the tool. ```bash $ mpqcli about Name: mpqcli -Version: 0.9.8-041480a92e698514d7938426587e93582b336b7d +Version: 0.10.0-30601b2cdf0c1a1c918709bca2e7416ab88bfa18 Author: Thomas Laurenson License: MIT GitHub: https://github.com/thegraydot/mpqcli diff --git a/docs/commands/completion.md b/docs/commands/completion.md new file mode 100644 index 0000000..afe5a63 --- /dev/null +++ b/docs/commands/completion.md @@ -0,0 +1,49 @@ +# completion + +Print a shell completion script to stdout. + +## Supported Shells + +- bash +- zsh +- fish +- PowerShell + +## Bash + +Write the completion script to a file and source it from your shell profile. + +```bash +$ mpqcli completion bash > ~/.bash_completion.d/mpqcli +$ source ~/.bash_completion.d/mpqcli +``` + +Alternatively, write the script to a system-wide completions directory (requires root): + +```bash +$ mpqcli completion bash > /etc/bash_completion.d/mpqcli +``` + +## Zsh + +Write the completion script to a directory that is on your `$fpath`. + +```zsh +$ mpqcli completion zsh > "${fpath[1]}/_mpqcli" +``` + +## PowerShell + +Append the completion script to your PowerShell profile so it loads automatically. + +```powershell +PS> mpqcli completion powershell >> $PROFILE +``` + +## Fish + +Write the completion script to the fish completions directory. + +```fish +$ mpqcli completion fish > ~/.config/fish/completions/mpqcli.fish +``` diff --git a/docs/commands/extract.md b/docs/commands/extract.md index 9efc885..d8c0ee7 100644 --- a/docs/commands/extract.md +++ b/docs/commands/extract.md @@ -25,6 +25,16 @@ Extract files to a specific target directory, which will be created if it doesn' $ mpqcli extract -o patch-1.10 wow-patch.mpq ``` +## Extract all files keeping the folder structure + +Use the `-k` or `--keep` flag to preserve the directory structure from the archive when extracting. Without this flag, all files are placed flat in the output directory, with only the filename retained. + +```bash +$ mpqcli extract -k wow-patch.mpq +[*] Extracted: Interface/FrameXML/BlizzardFrameXML.lua +[*] Extracted: Interface/FrameXML/BlizzardFrameXML.xml +``` + ## Extract all files with an external listfile Older MPQ archives do not contain (complete) file paths of their content. By providing an external listfile that lists the content of the MPQ archive, the extracted files will have the correct names and paths. Listfiles can be downloaded on [Ladislav Zezula's site](http://www.zezula.net/en/mpq/download.html). diff --git a/docs/commands/version.md b/docs/commands/version.md index b16466f..20b849c 100644 --- a/docs/commands/version.md +++ b/docs/commands/version.md @@ -4,5 +4,5 @@ Print the tool version number. ```bash $ mpqcli version -0.9.8-041480a92e698514d7938426587e93582b336b7d +0.10.0-30601b2cdf0c1a1c918709bca2e7416ab88bfa18 ``` diff --git a/docs/commands_list.md b/docs/commands_list.md index 859881d..93c1de5 100644 --- a/docs/commands_list.md +++ b/docs/commands_list.md @@ -9,6 +9,7 @@ The `mpqcli` program has the following subcommands: |---|---| | [`version`](./commands/version.md) | Print the tool version number | | [`about`](./commands/about.md) | Print information about the tool | +| [`completion`](./commands/completion.md) | Print shell completion script to stdout | | [`info`](./commands/info.md) | Print information about MPQ archive properties | | [`create`](./commands/create.md) | Create an MPQ archive from a target directory or a single file | | [`add`](./commands/add.md) | Add a file to an existing MPQ archive | @@ -17,3 +18,4 @@ The `mpqcli` program has the following subcommands: | [`extract`](./commands/extract.md) | Extract one or all files from a target MPQ archive | | [`read`](./commands/read.md) | Read a specific file to stdout | | [`verify`](./commands/verify.md) | Verify a target MPQ archive signature | +| [`compact`](./commands/compact.md) | Compact (defragment) an MPQ archive | diff --git a/docs/introduction.md b/docs/introduction.md index 9bc1145..c72c356 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -1,6 +1,6 @@ # Introduction -A command-line tool to create, add, remove, list, extract, read, and verify MPQ archives using the [StormLib library](https://github.com/ladislav-zezula/StormLib). +A command-line tool to create, add, remove, list, extract, read, verify, and compact MPQ archives using the [StormLib library](https://github.com/ladislav-zezula/StormLib). > ⚠️ **Warning:** This project is under active development and will change functionality between released versions until version 1.0.0. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 21bca10..0b7a52c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,21 @@ # Configure version header configure_file(mpqcli.h.in "${CMAKE_BINARY_DIR}/mpqcli.h") +# Embed completion scripts into generated headers. +# Declare the script files as configure-time dependencies so CMake re-generates +# completion_data.h whenever the bash or zsh source scripts are edited. +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.bash" + "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.zsh" + "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.ps1" + "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.fish" +) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.bash" BASH_COMPLETION_SCRIPT) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.zsh" ZSH_COMPLETION_SCRIPT) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.ps1" PS_COMPLETION_SCRIPT) +file(READ "${CMAKE_CURRENT_SOURCE_DIR}/completion/mpqcli.fish" FISH_COMPLETION_SCRIPT) +configure_file(completion_data.h.in "${CMAKE_BINARY_DIR}/completion_data.h") + # Set output directory for the executable set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") @@ -14,6 +29,7 @@ endif() add_executable(mpqcli main.cpp commands.cpp + completion.cpp mpq.cpp helpers.cpp locales.cpp @@ -24,8 +40,10 @@ add_executable(mpqcli add_dependencies(mpqcli storm) # Include directories -target_include_directories(mpqcli PRIVATE +target_include_directories(mpqcli SYSTEM PRIVATE "${CMAKE_SOURCE_DIR}/extern/StormLib/src" +) +target_include_directories(mpqcli PRIVATE "${CMAKE_BINARY_DIR}" ) diff --git a/src/completion.cpp b/src/completion.cpp new file mode 100644 index 0000000..ce787a9 --- /dev/null +++ b/src/completion.cpp @@ -0,0 +1,21 @@ +#include "completion.h" + +#include + +#include "completion_data.h" + +void HandleCompletionBash() { + std::cout << BashCompletionScript; +} + +void HandleCompletionZsh() { + std::cout << ZshCompletionScript; +} + +void HandleCompletionPs() { + std::cout << PsCompletionScript; +} + +void HandleCompletionFish() { + std::cout << FishCompletionScript; +} diff --git a/src/completion.h b/src/completion.h new file mode 100644 index 0000000..990aa79 --- /dev/null +++ b/src/completion.h @@ -0,0 +1,9 @@ +#ifndef COMPLETION_H +#define COMPLETION_H + +void HandleCompletionBash(); +void HandleCompletionZsh(); +void HandleCompletionPs(); +void HandleCompletionFish(); + +#endif // COMPLETION_H diff --git a/src/completion/mpqcli.bash b/src/completion/mpqcli.bash new file mode 100644 index 0000000..6624dd4 --- /dev/null +++ b/src/completion/mpqcli.bash @@ -0,0 +1,227 @@ +# Use _filedir when available, otherwise fall back to compgen. +# Pass an extension (e.g. "mpq") to restrict completions to that type plus directories. +_mpqcli_filedir() { + if declare -f _filedir > /dev/null 2>&1; then + _filedir "${1-}" + else + if [[ -n "${1-}" ]]; then + local -a _f _d + mapfile -t _f < <(compgen -f -X "!*.$1" -- "$cur") + mapfile -t _d < <(compgen -d -- "$cur") + COMPREPLY=("${_f[@]}" "${_d[@]}") + else + mapfile -t COMPREPLY < <(compgen -f -- "$cur") + fi + fi +} + +_mpqcli() { + local cur prev words cword + + # Use bash-completion helpers when available, fall back to COMP_* variables. + if declare -f _init_completion > /dev/null 2>&1; then + _init_completion || return + else + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + words=("${COMP_WORDS[@]}") + cword=$COMP_CWORD + fi + + local subcommands="version about info create add remove list extract read verify compact completion" + local -a locales=( + default enUS zhTW csCZ deDE esES frFR itIT + jaJP koKR nlNL plPL ptBR ruRU zhCN enGB esMX ptPT + ) + local -a games=( + generic + diablo1 diablo d1 + lordsofmagic lomse + starcraft starcraft1 sc1 + warcraft2 wc2 war2 + diablo2 d2 + warcraft3 wc3 war3 + warcraft3-map wc3-map war3-map + wow1 wow-vanilla + wow2 wow-tbc + wow3 wow-wotlk + wow4 wow-cataclysm + wow5 wow-mop + starcraft2 sc2 + diablo3 d3 + ) + local -a info_properties=( + format-version header-offset header-size archive-size + file-count max-files signature-type + ) + local -a list_properties=( + hash-index name-hash1 name-hash2 name-hash3 locale + file-index byte-offset file-time file-size compressed-size + flags encryption-key encryption-key-raw + ) + + if [[ $cword -eq 1 ]]; then + mapfile -t COMPREPLY < <(compgen -W "$subcommands" -- "$cur") + return + fi + + local subcmd="${words[1]}" + + case "$subcmd" in + info) + case "$prev" in + -p|--property) + mapfile -t COMPREPLY < <(compgen -W "${info_properties[*]}" -- "$cur") + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-p --property" -- "$cur") + else + _mpqcli_filedir mpq + fi + ;; + + create) + case "$prev" in + --locale) + mapfile -t COMPREPLY < <(compgen -W "${locales[*]}" -- "$cur") + return ;; + -g|--game) + mapfile -t COMPREPLY < <(compgen -W "${games[*]}" -- "$cur") + return ;; + --version) + mapfile -t COMPREPLY < <(compgen -W "1 2 3 4" -- "$cur") + return ;; + -p|--path|-o|--output|--stream-flags|--sector-size|\ + --raw-chunk-size|--file-flags1|--file-flags2|--file-flags3|--attr-flags|\ + --flags|--compression|--compression-next) + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-p --path -o --output -s --sign \ +--locale -g --game --version --stream-flags --sector-size --raw-chunk-size \ +--file-flags1 --file-flags2 --file-flags3 --attr-flags \ +--flags --compression --compression-next" -- "$cur") + else + _mpqcli_filedir + fi + ;; + + add) + case "$prev" in + --locale) + mapfile -t COMPREPLY < <(compgen -W "${locales[*]}" -- "$cur") + return ;; + -g|--game) + mapfile -t COMPREPLY < <(compgen -W "${games[*]}" -- "$cur") + return ;; + -p|--path|--flags|--compression|--compression-next) + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-p --path -w --overwrite -u --update \ +--locale -g --game \ +--flags --compression --compression-next" -- "$cur") + else + _mpqcli_filedir + fi + ;; + + remove) + case "$prev" in + --locale) + mapfile -t COMPREPLY < <(compgen -W "${locales[*]}" -- "$cur") + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "--locale" -- "$cur") + else + _mpqcli_filedir + fi + ;; + + list) + case "$prev" in + -l|--listfile) + _mpqcli_filedir + return ;; + -p|--property) + mapfile -t COMPREPLY < <(compgen -W "${list_properties[*]}" -- "$cur") + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-l --listfile -d --detailed -a --all -p --property" -- "$cur") + else + _mpqcli_filedir mpq + fi + ;; + + extract) + case "$prev" in + -o|--output) + _mpqcli_filedir + return ;; + -l|--listfile) + _mpqcli_filedir + return ;; + --locale) + mapfile -t COMPREPLY < <(compgen -W "${locales[*]}" -- "$cur") + return ;; + -f|--file) + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-o --output -f --file -k --keep -l --listfile --locale" -- "$cur") + else + _mpqcli_filedir mpq + fi + ;; + + read) + case "$prev" in + --locale) + mapfile -t COMPREPLY < <(compgen -W "${locales[*]}" -- "$cur") + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "--locale" -- "$cur") + elif [[ $cword -ge 3 ]]; then + # positional 1 is an in-archive path (no filesystem completion); + # positional 2+ is the archive file + _mpqcli_filedir mpq + fi + ;; + + verify) + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-p --print" -- "$cur") + else + _mpqcli_filedir mpq + fi + ;; + + compact) + case "$prev" in + -l|--listfile) + _mpqcli_filedir + return ;; + esac + if [[ "$cur" == -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "-l --listfile" -- "$cur") + else + _mpqcli_filedir mpq + fi + ;; + + completion) + if [[ "$cur" != -* ]]; then + mapfile -t COMPREPLY < <(compgen -W "bash zsh powershell fish" -- "$cur") + fi + ;; + + version|about) ;; + esac +} + +complete -F _mpqcli mpqcli diff --git a/src/completion/mpqcli.fish b/src/completion/mpqcli.fish new file mode 100644 index 0000000..cec6666 --- /dev/null +++ b/src/completion/mpqcli.fish @@ -0,0 +1,217 @@ +# Fish shell completions for mpqcli +# Source: generated from mpqcli/src/main.cpp +# +# Install: +# cp mpqcli.fish ~/.config/fish/completions/mpqcli.fish + +# Disable file completion globally; re-enable per argument where needed. +complete -c mpqcli -f + +# Shared value sets (sourced directly from main.cpp) +set -l __mpqcli_locales \ + default \ + enUS enGB zhTW zhCN csCZ deDE esES esMX \ + frFR itIT jaJP koKR nlNL plPL ptBR ptPT ruRU + +set -l __mpqcli_game_profiles \ + generic \ + diablo1 diablo d1 \ + lordsofmagic lomse \ + starcraft starcraft1 sc1 \ + warcraft2 wc2 war2 \ + diablo2 d2 \ + warcraft3 wc3 war3 \ + warcraft3-map wc3-map war3-map \ + wow1 wow-vanilla \ + wow2 wow-tbc \ + wow3 wow-wotlk \ + wow4 wow-cataclysm \ + wow5 wow-mop \ + starcraft2 sc2 \ + diablo3 d3 + +set -l __mpqcli_info_properties \ + format-version header-offset header-size archive-size \ + file-count max-files signature-type + +set -l __mpqcli_list_properties \ + hash-index name-hash1 name-hash2 name-hash3 locale \ + file-index byte-offset file-time file-size compressed-size \ + flags encryption-key encryption-key-raw + +# Top-level subcommands (no subcommand active yet) +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a version -d 'Print program version' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a about -d 'Print program information' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a info -d 'Print info about an MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a create -d 'Create an MPQ archive from a file or directory' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a add -d 'Add files to an existing MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a remove -d 'Remove files from an existing MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a list -d 'List files in an MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a extract -d 'Extract files from an MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a read -d 'Read a file from an MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a verify -d 'Verify an MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a compact -d 'Compact the MPQ archive' +complete -c mpqcli -n 'not __fish_seen_subcommand_from version about info create add remove list extract read verify compact completion' \ + -a completion -d 'Generate shell completion script' + +# info +# info [-p/--property ] +# positional: existing file +complete -c mpqcli -n '__fish_seen_subcommand_from info' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from info' \ + -s p -l property -d 'Print only a specific property value' \ + -r -a "$__mpqcli_info_properties" + +# create +# create [-p/--path] [-o/--output] [-s/--sign] +# [--locale] [-g/--game] +# [--version] [--stream-flags] [--sector-size] [--raw-chunk-size] +# [--file-flags1/2/3] [--attr-flags] +# [--flags] [--compression] [--compression-next] +complete -c mpqcli -n '__fish_seen_subcommand_from create' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -s p -l path -d 'Archive path for a single file, or prefix for a directory' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -s o -l output -d 'Output MPQ archive path' -r -F +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -s s -l sign -d 'Sign the MPQ archive' +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l locale -d 'Locale to use for added files' \ + -r -a "$__mpqcli_locales" +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -s g -l game -d 'Game profile for MPQ creation' \ + -r -a "$__mpqcli_game_profiles" +# Game setting overrides +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l version -d 'Override MPQ archive version (1-4)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l stream-flags -d 'Override stream flags' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l sector-size -d 'Override sector size' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l raw-chunk-size -d 'Override raw chunk size (MPQ v4)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l file-flags1 -d 'Override file flags for (listfile)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l file-flags2 -d 'Override file flags for (attributes)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l file-flags3 -d 'Override file flags for (signature)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l attr-flags -d 'Override attribute flags (CRC32, FILETIME, MD5)' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l flags -d 'Override MPQ file flags for added files' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l compression -d 'Override compression for first sector of added files' -r +complete -c mpqcli -n '__fish_seen_subcommand_from create' \ + -l compression-next -d 'Override compression for subsequent sectors of added files' -r + +# add +# add [-p/--path] [-w/--overwrite] [-u/--update] +# [--locale] [-g/--game] +# [--flags] [--compression] [--compression-next] +complete -c mpqcli -n '__fish_seen_subcommand_from add' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -s p -l path -d 'Archive path for a single file, or prefix for a directory' -r +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -s w -l overwrite -d 'Overwrite file if it already exists in the archive' +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -s u -l update -d 'Skip files whose archived size matches on-disk size' +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -l locale -d 'Locale to use for added file' \ + -r -a "$__mpqcli_locales" +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -s g -l game -d 'Game profile for compression rules' \ + -r -a "$__mpqcli_game_profiles" +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -l flags -d 'Override MPQ file flags' -r +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -l compression -d 'Override compression for first sector' -r +complete -c mpqcli -n '__fish_seen_subcommand_from add' \ + -l compression-next -d 'Override compression for subsequent sectors' -r + +# remove +# remove [--locale] +complete -c mpqcli -n '__fish_seen_subcommand_from remove' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from remove' \ + -l locale -d 'Locale of the file to remove' \ + -r -a "$__mpqcli_locales" + +# list +# list [-l/--listfile] [-d/--detailed] [-a/--all] +# [-p/--property ] +complete -c mpqcli -n '__fish_seen_subcommand_from list' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from list' \ + -s l -l listfile -d 'External file listing content of the archive' -r -F +complete -c mpqcli -n '__fish_seen_subcommand_from list' \ + -s d -l detailed -d 'Show additional columns' +complete -c mpqcli -n '__fish_seen_subcommand_from list' \ + -s a -l all -d 'Include hidden files' +complete -c mpqcli -n '__fish_seen_subcommand_from list' \ + -s p -l property -d 'Print only specific property values' \ + -r -a "$__mpqcli_list_properties" + +# extract +# extract [-o/--output] [-f/--file] [-k/--keep] +# [-l/--listfile] [--locale] +complete -c mpqcli -n '__fish_seen_subcommand_from extract' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from extract' \ + -s o -l output -d 'Output directory' -r -F +complete -c mpqcli -n '__fish_seen_subcommand_from extract' \ + -s f -l file -d 'Target file to extract' -r +complete -c mpqcli -n '__fish_seen_subcommand_from extract' \ + -s k -l keep -d 'Keep folder structure' +complete -c mpqcli -n '__fish_seen_subcommand_from extract' \ + -s l -l listfile -d 'External file listing content of the archive' -r -F +complete -c mpqcli -n '__fish_seen_subcommand_from extract' \ + -l locale -d 'Preferred locale for extracted file' \ + -r -a "$__mpqcli_locales" + +# read +# read [--locale] +complete -c mpqcli -n '__fish_seen_subcommand_from read' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from read' \ + -l locale -d 'Preferred locale for read file' \ + -r -a "$__mpqcli_locales" + +# verify +# verify [-p/--print] +complete -c mpqcli -n '__fish_seen_subcommand_from verify' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from verify' \ + -s p -l print -d 'Print the digital signature (in hex)' + +# compact +# compact [-l/--listfile] +complete -c mpqcli -n '__fish_seen_subcommand_from compact' -F + +complete -c mpqcli -n '__fish_seen_subcommand_from compact' \ + -s l -l listfile -d 'External file listing content of the archive' -r -F + +# completion +# completion +complete -c mpqcli -n '__fish_seen_subcommand_from completion' \ + -a bash -d 'Generate bash completion script' +complete -c mpqcli -n '__fish_seen_subcommand_from completion' \ + -a zsh -d 'Generate zsh completion script' +complete -c mpqcli -n '__fish_seen_subcommand_from completion' \ + -a powershell -d 'Generate PowerShell completion script' +complete -c mpqcli -n '__fish_seen_subcommand_from completion' \ + -a fish -d 'Generate fish completion script' diff --git a/src/completion/mpqcli.ps1 b/src/completion/mpqcli.ps1 new file mode 100644 index 0000000..cd651ac --- /dev/null +++ b/src/completion/mpqcli.ps1 @@ -0,0 +1,274 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + PowerShell argument completer for the mpqcli tool. + +.DESCRIPTION + Provides tab/menu completion for mpqcli subcommands, options, and known + value sets (game profiles, locales, info/list properties). File-path + arguments fall back to PowerShell's native filesystem completion. + +.NOTES + Source it from your $PROFILE: + . /path/to/mpqcli-completion.ps1 + + Or copy it into a module / profile script that loads at startup. + Works for any executable named 'mpqcli' or 'mpqcli.exe'. +#> + +Register-ArgumentCompleter -Native -CommandName 'mpqcli', 'mpqcli.exe' -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + # Static value sets (derived from mpqcli source) + + $subcommands = [ordered]@{ + 'version' = 'Prints program version' + 'about' = 'Prints program information' + 'info' = 'Prints info about an MPQ archive' + 'create' = 'Create an MPQ archive from target file or directory' + 'add' = 'Add files to an existing MPQ archive' + 'remove' = 'Remove files from an existing MPQ archive' + 'list' = 'List files from the MPQ archive' + 'extract' = 'Extract files from the MPQ archive' + 'read' = 'Read a file from an MPQ archive' + 'verify' = 'Verify the MPQ archive' + 'compact' = 'Compact the MPQ archive' + 'completion' = 'Generate shell completion script' + } + + $gameProfiles = @( + 'generic', + 'diablo1', 'diablo', 'd1', + 'lordsofmagic', 'lomse', + 'starcraft', 'starcraft1', 'sc1', + 'warcraft2', 'wc2', 'war2', + 'diablo2', 'd2', + 'warcraft3', 'wc3', 'war3', + 'warcraft3-map', 'wc3-map', 'war3-map', + 'wow1', 'wow-vanilla', + 'wow2', 'wow-tbc', + 'wow3', 'wow-wotlk', + 'wow4', 'wow-cataclysm', + 'wow5', 'wow-mop', + 'starcraft2', 'sc2', + 'diablo3', 'd3' + ) + + $locales = @( + 'default', + 'enUS', 'enGB', 'zhTW', 'zhCN', 'csCZ', 'deDE', 'esES', 'esMX', + 'frFR', 'itIT', 'jaJP', 'koKR', 'nlNL', 'plPL', 'ptBR', 'ptPT', 'ruRU' + ) + + $infoProperties = @( + 'format-version', 'header-offset', 'header-size', 'archive-size', + 'file-count', 'max-files', 'signature-type' + ) + + $listProperties = @( + 'hash-index', 'name-hash1', 'name-hash2', 'name-hash3', 'locale', + 'file-index', 'byte-offset', 'file-time', 'file-size', 'compressed-size', + 'flags', 'encryption-key', 'encryption-key-raw' + ) + + # Options available per subcommand. Each value is a hashtable mapping the + # option token to a short help string. + $optionSpec = @{ + 'info' = @{ + '-p' = 'Print only a specific property value' + '--property' = 'Print only a specific property value' + } + 'create' = @{ + '-p' = 'Archive path for a single file, or prefix for a directory' + '--path' = 'Archive path for a single file, or prefix for a directory' + '-o' = 'Output MPQ archive' + '--output' = 'Output MPQ archive' + '-s' = 'Sign the MPQ archive' + '--sign' = 'Sign the MPQ archive' + '--locale' = 'Locale to use for added files' + '-g' = 'Game profile for MPQ creation' + '--game' = 'Game profile for MPQ creation' + '--version' = 'Override the MPQ archive version (1-4)' + '--stream-flags' = 'Override stream flags' + '--sector-size' = 'Override sector size' + '--raw-chunk-size'= 'Override raw chunk size for MPQ v4' + '--file-flags1' = 'Override file flags for (listfile)' + '--file-flags2' = 'Override file flags for (attributes)' + '--file-flags3' = 'Override file flags for (signature)' + '--attr-flags' = 'Override attribute flags (CRC32, FILETIME, MD5)' + '--flags' = 'Override MPQ file flags for added files' + '--compression' = 'Override compression for first sector' + '--compression-next' = 'Override compression for subsequent sectors' + } + 'add' = @{ + '-p' = 'Archive path for a single file, or prefix for a directory' + '--path' = 'Archive path for a single file, or prefix for a directory' + '-w' = 'Overwrite file if it already is in MPQ archive' + '--overwrite' = 'Overwrite file if it already is in MPQ archive' + '-u' = 'Skip files whose archived size matches on-disk size' + '--update' = 'Skip files whose archived size matches on-disk size' + '--locale' = 'Locale to use for added file' + '-g' = 'Game profile for compression rules' + '--game' = 'Game profile for compression rules' + '--flags' = 'Override MPQ file flags' + '--compression' = 'Override compression for first sector' + '--compression-next' = 'Override compression for subsequent sectors' + } + 'remove' = @{ + '--locale' = 'Locale of file to remove' + } + 'list' = @{ + '-l' = 'File listing content of an MPQ archive' + '--listfile' = 'File listing content of an MPQ archive' + '-d' = 'File listing with additional columns' + '--detailed' = 'File listing with additional columns' + '-a' = 'File listing including hidden files' + '--all' = 'File listing including hidden files' + '-p' = 'Print only specific property values' + '--property' = 'Print only specific property values' + } + 'extract' = @{ + '-o' = 'Output directory' + '--output' = 'Output directory' + '-f' = 'Target file to extract' + '--file' = 'Target file to extract' + '-k' = 'Keep folder structure' + '--keep' = 'Keep folder structure' + '-l' = 'File listing content of an MPQ archive' + '--listfile' = 'File listing content of an MPQ archive' + '--locale' = 'Preferred locale for extracted file' + } + 'read' = @{ + '--locale' = 'Preferred locale for read file' + } + 'verify' = @{ + '-p' = 'Print the digital signature (in hex)' + '--print' = 'Print the digital signature (in hex)' + } + 'compact' = @{ + '-l' = 'File listing content of an MPQ archive' + '--listfile' = 'File listing content of an MPQ archive' + } + 'version' = @{} + 'about' = @{} + 'completion' = @{} + } + + # Options whose *argument* should complete from a static value set. + $valueOptions = @{ + '--locale' = $locales + '-g' = $gameProfiles + '--game' = $gameProfiles + } + # Property options depend on the subcommand (info vs list), handled below. + + # Parse the current command line + + # Tokenized elements of the command line, excluding the executable itself. + $elements = @($commandAst.CommandElements | Select-Object -Skip 1 | + ForEach-Object { $_.ToString() }) + + # Identify the active subcommand (first element that is a known subcommand). + $subcommand = $null + foreach ($el in $elements) { + if ($subcommands.Contains($el)) { $subcommand = $el; break } + } + + # The token immediately preceding the cursor (the one we may be an arg to). + $prevToken = $null + if ($elements.Count -ge 1) { + if ([string]::IsNullOrEmpty($wordToComplete)) { + $prevToken = $elements[-1] + } + elseif ($elements.Count -ge 2) { + $prevToken = $elements[-2] + } + } + + $emit = { + param($items, $type) + foreach ($i in $items) { + if ($i.Value -like "$wordToComplete*") { + [System.Management.Automation.CompletionResult]::new( + $i.Value, $i.Value, + [System.Management.Automation.CompletionResultType]::$type, + $i.Tip) + } + } + } + + # 1) No subcommand yet -> complete subcommands + if (-not $subcommand) { + $items = foreach ($k in $subcommands.Keys) { + [pscustomobject]@{ Value = $k; Tip = $subcommands[$k] } + } + return & $emit $items 'Command' + } + + # 2) completion subcommand -> complete shell names + if ($subcommand -eq 'completion') { + if (-not ($wordToComplete -like '-*')) { + $shells = [ordered]@{ + 'bash' = 'Generate bash completion script' + 'zsh' = 'Generate zsh completion script' + 'powershell' = 'Generate PowerShell completion script' + 'fish' = 'Generate fish completion script' + } + $items = foreach ($k in $shells.Keys) { + [pscustomobject]@{ Value = $k; Tip = $shells[$k] } + } + return & $emit $items 'ParameterValue' + } + return + } + + # 4) Completing the argument to a value-bearing option + if ($prevToken) { + # Locale / game profile options. + if ($valueOptions.ContainsKey($prevToken)) { + $items = foreach ($v in $valueOptions[$prevToken]) { + [pscustomobject]@{ Value = $v; Tip = $prevToken } + } + return & $emit $items 'ParameterValue' + } + # Property options: meaning depends on the subcommand. + if ($prevToken -in @('-p', '--property')) { + if ($subcommand -eq 'info') { + $items = foreach ($v in $infoProperties) { + [pscustomobject]@{ Value = $v; Tip = 'info property' } } + return & $emit $items 'ParameterValue' + } + elseif ($subcommand -eq 'list') { + $items = foreach ($v in $listProperties) { + [pscustomobject]@{ Value = $v; Tip = 'list property' } } + return & $emit $items 'ParameterValue' + } + # For 'verify', -p/--print is a flag (no value) -> fall through. + } + # Options that take a file/dir path -> let PowerShell complete paths. + $pathOptions = @('-o', '--output', '-l', '--listfile', '-f', '--file', + '-p', '--path') + if ($prevToken -in $pathOptions -and + -not ($subcommand -in @('info', 'list') -and $prevToken -in @('-p', '--property'))) { + return # empty -> native path completion takes over + } + } + + # 5) Completing an option for the current subcommand + if ($wordToComplete -like '-*' -or [string]::IsNullOrEmpty($wordToComplete)) { + $opts = $optionSpec[$subcommand] + if ($opts) { + $items = foreach ($k in $opts.Keys) { + [pscustomobject]@{ Value = $k; Tip = $opts[$k] } + } + $results = & $emit $items 'ParameterName' + if ($wordToComplete -like '-*') { return $results } + # When word is empty we still also allow path completion, so only + # return option results if the user has started a dash. + if ($results) { return $results } + } + } + + # 6) Fall back to native filesystem completion (archives/files) + return +} diff --git a/src/completion/mpqcli.zsh b/src/completion/mpqcli.zsh new file mode 100644 index 0000000..183aa7c --- /dev/null +++ b/src/completion/mpqcli.zsh @@ -0,0 +1,179 @@ +#compdef mpqcli + +_mpqcli_locales=( + default enUS zhTW csCZ deDE esES frFR itIT + jaJP koKR nlNL plPL ptBR ruRU zhCN enGB esMX ptPT +) + +_mpqcli_games=( + generic + diablo1 diablo d1 + lordsofmagic lomse + starcraft starcraft1 sc1 + warcraft2 wc2 war2 + diablo2 d2 + warcraft3 wc3 war3 + warcraft3-map wc3-map war3-map + wow1 wow-vanilla + wow2 wow-tbc + wow3 wow-wotlk + wow4 wow-cataclysm + wow5 wow-mop + starcraft2 sc2 + diablo3 d3 +) + +_mpqcli_info_properties=( + format-version header-offset header-size archive-size + file-count max-files signature-type +) + +_mpqcli_list_properties=( + hash-index name-hash1 name-hash2 name-hash3 locale + file-index byte-offset file-time file-size compressed-size + flags encryption-key encryption-key-raw +) + +_mpqcli() { + local state + typeset -A opt_args + + _arguments -C \ + '1: :_mpqcli_cmds' \ + '*:: :->subcmd' + + case $state in + subcmd) + case $words[1] in + info) _mpqcli_info ;; + create) _mpqcli_create ;; + add) _mpqcli_add ;; + remove) _mpqcli_remove ;; + list) _mpqcli_list ;; + extract) _mpqcli_extract ;; + read) _mpqcli_read ;; + verify) _mpqcli_verify ;; + compact) _mpqcli_compact ;; + completion) _mpqcli_completion ;; + esac + ;; + esac +} + +_mpqcli_cmds() { + local cmds=( + 'version:Print program version' + 'about:Print program information' + 'info:Print info about an MPQ archive' + 'create:Create an MPQ archive from target file or directory' + 'add:Add files to an existing MPQ archive' + 'remove:Remove files from an existing MPQ archive' + 'list:List files from the MPQ archive' + 'extract:Extract files from the MPQ archive' + 'read:Read a file from an MPQ archive' + 'verify:Verify the MPQ archive' + 'compact:Compact the MPQ archive' + 'completion:Generate shell completion script' + ) + _describe 'subcommand' cmds +} + +_mpqcli_info() { + local props="${_mpqcli_info_properties[*]}" + _arguments \ + '1:archive:_files' \ + '(-p --property)'{-p,--property}'[print only a specific property]:property:('"$props"')' +} + +_mpqcli_create() { + _arguments \ + '1:target:_files' \ + '(-p --path)'{-p,--path}'[archive path for a single file, or prefix for a directory]:path' \ + '(-o --output)'{-o,--output}'[output archive]:file:_files' \ + '(-s --sign)'{-s,--sign}'[sign the archive]' \ + '--locale[locale for added files]:locale:('"${_mpqcli_locales[@]}"')' \ + '(-g --game)'{-g,--game}'[game profile]:profile:('"${_mpqcli_games[@]}"')' \ + '--version[MPQ archive version (1-4)]:version:(1 2 3 4)' \ + '--stream-flags[override stream flags]:flags' \ + '--sector-size[override sector size]:size' \ + '--raw-chunk-size[override raw chunk size for MPQ v4]:size' \ + '--file-flags1[override file flags for listfile]:flags' \ + '--file-flags2[override file flags for attributes]:flags' \ + '--file-flags3[override file flags for signature]:flags' \ + '--attr-flags[override attribute flags]:flags' \ + '--flags[override MPQ file flags for added files]:flags' \ + '--compression[override compression for first sector]:compression' \ + '--compression-next[override compression for subsequent sectors]:compression' +} + +_mpqcli_add() { + _arguments \ + '1:archive:_files' \ + '*:files:_files' \ + '(-p --path)'{-p,--path}'[archive path or prefix for directory add]:path' \ + '(-w --overwrite)'{-w,--overwrite}'[overwrite existing file]' \ + '(-u --update)'{-u,--update}'[skip unchanged files in directory add]' \ + '--locale[locale for added file]:locale:('"${_mpqcli_locales[@]}"')' \ + '(-g --game)'{-g,--game}'[game profile]:profile:('"${_mpqcli_games[@]}"')' \ + '--flags[override MPQ file flags]:flags' \ + '--compression[override compression for first sector]:compression' \ + '--compression-next[override compression for subsequent sectors]:compression' +} + +_mpqcli_remove() { + _arguments \ + '1:archive:_files' \ + '*:archive paths' \ + '--locale[locale of file to remove]:locale:('"${_mpqcli_locales[@]}"')' +} + +_mpqcli_list() { + local props="${_mpqcli_list_properties[*]}" + _arguments \ + '1:archive:_files' \ + '(-l --listfile)'{-l,--listfile}'[listfile path]:file:_files' \ + '(-d --detailed)'{-d,--detailed}'[detailed listing with extra columns]' \ + '(-a --all)'{-a,--all}'[include hidden files]' \ + '(-p --property)'{-p,--property}'[print specific property values]:property:('"$props"')' +} + +_mpqcli_extract() { + _arguments \ + '1:archive:_files' \ + '(-o --output)'{-o,--output}'[output directory]:dir:_files -/' \ + '(-f --file)'{-f,--file}'[target file to extract]:file' \ + '(-k --keep)'{-k,--keep}'[keep folder structure]' \ + '(-l --listfile)'{-l,--listfile}'[listfile path]:file:_files' \ + '--locale[preferred locale for extracted file]:locale:('"${_mpqcli_locales[@]}"')' +} + +_mpqcli_read() { + _arguments \ + '1:file-in-archive' \ + '2:archive:_files' \ + '--locale[preferred locale for read file]:locale:('"${_mpqcli_locales[@]}"')' +} + +_mpqcli_verify() { + _arguments \ + '1:archive:_files' \ + '(-p --print)'{-p,--print}'[print the digital signature in hex]' +} + +_mpqcli_compact() { + _arguments \ + '1:archive:_files' \ + '(-l --listfile)'{-l,--listfile}'[listfile path]:file:_files' +} + +_mpqcli_completion() { + local shells=( + 'bash:Generate bash completion script' + 'zsh:Generate zsh completion script' + 'powershell:Generate PowerShell completion script' + 'fish:Generate fish completion script' + ) + _describe 'shell' shells +} + +_mpqcli "$@" diff --git a/src/completion_data.h.in b/src/completion_data.h.in new file mode 100644 index 0000000..081fc13 --- /dev/null +++ b/src/completion_data.h.in @@ -0,0 +1,16 @@ +#ifndef COMPLETION_DATA_H +#define COMPLETION_DATA_H + +// Auto-generated from src/completion/mpqcli.bash, do not edit directly +static constexpr char BashCompletionScript[] = R"BASH_MPQCLI(@BASH_COMPLETION_SCRIPT@)BASH_MPQCLI"; + +// Auto-generated from src/completion/mpqcli.zsh, do not edit directly +static constexpr char ZshCompletionScript[] = R"ZSH_MPQCLI(@ZSH_COMPLETION_SCRIPT@)ZSH_MPQCLI"; + +// Auto-generated from src/completion/mpqcli.ps1, do not edit directly +static constexpr char PsCompletionScript[] = R"PS_MPQCLI(@PS_COMPLETION_SCRIPT@)PS_MPQCLI"; + +// Auto-generated from src/completion/mpqcli.fish, do not edit directly +static constexpr char FishCompletionScript[] = R"FISH_MPQCLI(@FISH_COMPLETION_SCRIPT@)FISH_MPQCLI"; + +#endif // COMPLETION_DATA_H diff --git a/src/main.cpp b/src/main.cpp index d3c83e4..9ec5999 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include #include "commands.h" +#include "completion.h" #include "gamerules.h" #include "validators.h" @@ -213,7 +214,7 @@ int main(int argc, char **argv) { ->check(CLI::ExistingFile); verify->add_flag("-p,--print", verifyPrintSignature, "Print the digital signature (in hex)"); - // Subcommand: compact + // Subcommand: Compact CLI::App *compact = app.add_subcommand("compact", "Compact the MPQ archive"); compact->add_option("target", baseTarget, "Target MPQ archive") ->required() @@ -221,6 +222,16 @@ int main(int argc, char **argv) { compact->add_option("-l,--listfile", baseListfileName, "File listing content of an MPQ archive") ->check(CLI::ExistingFile); + // Subcommand: Completion + CLI::App *completion = app.add_subcommand("completion", "Generate shell completion script"); + CLI::App *completionBash = + completion->add_subcommand("bash", "Generate bash completion script"); + CLI::App *completionZsh = completion->add_subcommand("zsh", "Generate zsh completion script"); + CLI::App *completionPs = + completion->add_subcommand("powershell", "Generate PowerShell completion script"); + CLI::App *completionFish = + completion->add_subcommand("fish", "Generate fish completion script"); + try { app.parse(argc, argv); } catch (const CLI::ParseError &e) { @@ -307,5 +318,26 @@ int main(int argc, char **argv) { return HandleCompact(baseTarget, baseListfileName); } + if (app.got_subcommand(completion)) { + if (completion->got_subcommand(completionBash)) { + HandleCompletionBash(); + return 0; + } + if (completion->got_subcommand(completionZsh)) { + HandleCompletionZsh(); + return 0; + } + if (completion->got_subcommand(completionPs)) { + HandleCompletionPs(); + return 0; + } + if (completion->got_subcommand(completionFish)) { + HandleCompletionFish(); + return 0; + } + std::cerr << completion->help(); + return 1; + } + return 0; } diff --git a/src/mpq.cpp b/src/mpq.cpp index 140de9b..606f4d2 100644 --- a/src/mpq.cpp +++ b/src/mpq.cpp @@ -404,7 +404,7 @@ int ListFiles(HANDLE hArchive, const std::optional &listfileName, b true; // If the user specified properties, we need to print the detailed output } - // Map of property name to SFileInfoClass — defined once, outside the loop + // Map of property name to SFileInfoClass, defined once, outside the loop static const std::map kPropertyInfoClass = { {"hash-index", SFileInfoHashIndex}, {"name-hash1", SFileInfoNameHash1}, diff --git a/test/test_add.py b/test/test_add.py index 299fcfb..efdaa91 100644 --- a/test/test_add.py +++ b/test/test_add.py @@ -575,8 +575,6 @@ def test_add_file_after_all_locale_variants_removed(binary_path, generate_locale ) -# ---- Directory add tests ---- - def test_add_directory_to_mpq_archive(binary_path, generate_test_files): _ = generate_test_files script_dir = Path(__file__).parent @@ -710,8 +708,6 @@ def test_add_directory_without_overwrite_skips_existing(binary_path, generate_te shutil.rmtree(add_dir, ignore_errors=True) -# ---- --update flag tests ---- - def test_add_update_skips_unchanged_files(binary_path, generate_test_files): _ = generate_test_files script_dir = Path(__file__).parent @@ -826,8 +822,6 @@ def test_add_update_single_file_emits_warning(binary_path, generate_test_files): assert "--update is only meaningful when adding a directory" in result.stderr -# ---- stdin tests ---- - def test_add_files_via_stdin(binary_path, generate_test_files): _ = generate_test_files script_dir = Path(__file__).parent @@ -862,8 +856,6 @@ def test_add_files_via_stdin(binary_path, generate_test_files): assert "stdin_b.txt" in list_result.stdout -# ---- Helpers ---- - def create_mpq_archive_for_test(binary_path, script_dir): target_dir = script_dir / "data" / "files" target_file = target_dir.with_suffix(".mpq") diff --git a/test/test_completion.py b/test/test_completion.py new file mode 100644 index 0000000..9914232 --- /dev/null +++ b/test/test_completion.py @@ -0,0 +1,128 @@ +import subprocess +from pathlib import Path + + +def test_completion_bash(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "bash"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert "_mpqcli()" in result.stdout, "Bash completion function not found in output" + assert "complete -F _mpqcli mpqcli" in result.stdout, "Bash complete registration not found in output" + + +def test_completion_zsh(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "zsh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert "#compdef mpqcli" in result.stdout, "Zsh compdef directive not found in output" + assert "_mpqcli()" in result.stdout, "Zsh completion function not found in output" + + +def test_completion_bash_output_size(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "bash"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + script_path = Path(__file__).parent.parent / "src" / "completion" / "mpqcli.bash" + assert len(result.stdout) == len(script_path.read_text()) + + +def test_completion_zsh_output_size(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "zsh"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + script_path = Path(__file__).parent.parent / "src" / "completion" / "mpqcli.zsh" + assert len(result.stdout) == len(script_path.read_text()) + + +def test_completion_powershell(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "powershell"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert "Register-ArgumentCompleter" in result.stdout, "PS completer registration not found in output" + assert "mpqcli" in result.stdout, "mpqcli reference not found in output" + + +def test_completion_powershell_output_size(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "powershell"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + script_path = Path(__file__).parent.parent / "src" / "completion" / "mpqcli.ps1" + assert len(result.stdout) == len(script_path.read_text()) + + +def test_completion_fish(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "fish"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + assert "complete -c mpqcli" in result.stdout, "Fish complete directive not found in output" + + +def test_completion_fish_output_size(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "fish"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode == 0, f"mpqcli failed with error: {result.stderr}" + script_path = Path(__file__).parent.parent / "src" / "completion" / "mpqcli.fish" + assert len(result.stdout) == len(script_path.read_text()) + + +def test_completion_no_shell(binary_path): + result = subprocess.run( + [str(binary_path), "completion"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode != 0, "Expected non-zero exit code when no shell is specified" + + +def test_completion_invalid_shell(binary_path): + result = subprocess.run( + [str(binary_path), "completion", "nushell"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + assert result.returncode != 0, "Expected non-zero exit code for unsupported shell" + assert result.stderr, "Expected error output for unsupported shell" diff --git a/test/test_extract.py b/test/test_extract.py index 9b24471..45aa4e9 100644 --- a/test/test_extract.py +++ b/test/test_extract.py @@ -80,9 +80,9 @@ def test_extract_mpq_default_options(binary_path, generate_test_files): expected_size = 28 if platform.system() == "Windows" else 27 assert cats_file.read_text(encoding="utf-8") == expected_cats_content, \ - f"Unexpected content in cats.txt" + "Unexpected content in cats.txt" assert dogs_file.read_text(encoding="utf-8") == expected_dogs_content, \ - f"Unexpected content in dogs.txt" + "Unexpected content in dogs.txt" assert cats_file.stat().st_size == expected_size, \ f"Unexpected size for cats.txt: {cats_file.stat().st_size}" assert dogs_file.stat().st_size == expected_size, \ @@ -142,9 +142,9 @@ def test_extract_mpq_output_directory_specified(binary_path, generate_test_files expected_size = 28 if platform.system() == "Windows" else 27 assert cats_file.read_text(encoding="utf-8") == expected_cats_content, \ - f"Unexpected content in cats.txt" + "Unexpected content in cats.txt" assert dogs_file.read_text(encoding="utf-8") == expected_dogs_content, \ - f"Unexpected content in dogs.txt" + "Unexpected content in dogs.txt" assert cats_file.stat().st_size == expected_size, \ f"Unexpected size for cats.txt: {cats_file.stat().st_size}" assert dogs_file.stat().st_size == expected_size, \ @@ -199,7 +199,7 @@ def test_extract_file_from_mpq_output_directory_specified(binary_path, generate_ expected_size = 28 if platform.system() == "Windows" else 27 assert cats_file.read_text(encoding="utf-8") == expected_cats_content, \ - f"Unexpected content in cats.txt" + "Unexpected content in cats.txt" assert cats_file.stat().st_size == expected_size, \ f"Unexpected size for cats.txt: {cats_file.stat().st_size}"