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
29 changes: 29 additions & 0 deletions README_ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,37 @@ pip install diffx-python

詳細な使い方とサンプルは [ドキュメント](docs/index_ja.md) をご確認ください。

### シェル補完

Tab補完を有効にしてコマンド入力を快適に:

```bash
# Bash
diffx --completions bash > ~/.local/share/bash-completion/completions/diffx

# Zsh
diffx --completions zsh > ~/.zfunc/_diffx

# Fish
diffx --completions fish > ~/.config/fish/completions/diffx.fish

# PowerShell
diffx --completions powershell >> $PROFILE
```

### マニュアルページ

`man diffx` でマニュアルを参照できます。ビルド時に自動生成されます:

```bash
# ビルド後、man pageをシステムにインストール
sudo cp target/release/build/diffx-*/out/man/diffx.1 /usr/local/share/man/man1/
man diffx
```

### クイックドキュメントリンク

- **[実行例(テスト検証済み)](docs/examples/)** - 実際の入出力例
- **[はじめに](docs/user-guide/getting-started_ja.md)** - 基本を学ぶ
- **[インストールガイド](docs/user-guide/installation_ja.md)** - プラットフォーム別セットアップ
- **[CLIリファレンス](docs/reference/cli-reference_ja.md)** - 完全なコマンドリファレンス
Expand Down
11 changes: 11 additions & 0 deletions diffx-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rust-version.workspace = true
diffx-core = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
clap_complete = "4.5"
colored = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
Expand All @@ -28,10 +29,16 @@ walkdir = { workspace = true }
dirs = { workspace = true }
regex = { workspace = true }

[build-dependencies]
clap = { workspace = true }
clap_mangen = "0.2"
clap_complete = "4.5"
Comment on lines +32 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire up build script for man/completions generation

A new build script (diffx-cli/build.rs) now writes man pages and shell completions, and you added build-dependencies to support it (lines 32-35), but Cargo.toml still lacks a build = "build.rs" entry in the [package] section. Cargo ignores build scripts unless they are declared, so this script never runs and the documented manpage/completion artifacts will never be generated.

Useful? React with 👍 / 👎.


[dev-dependencies]
assert_cmd = { workspace = true }
predicates = { workspace = true }
tempfile = { workspace = true }
trycmd = "0.15"

[[test]]
name = "cli"
Expand All @@ -44,3 +51,7 @@ path = "tests/docs_examples/mod.rs"
[[test]]
name = "integration"
path = "tests/integration/mod.rs"

[[test]]
name = "examples"
path = "tests/examples.rs"
160 changes: 160 additions & 0 deletions diffx-cli/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use clap::{Arg, Command, ValueHint};
use clap_complete::Shell;
use clap_mangen::Man;
use std::env;
use std::fs;
use std::path::PathBuf;

fn build_cli() -> Command {
Command::new("diffx")
.version(env!("CARGO_PKG_VERSION"))
.about("A diff tool for structured data")
.arg(
Arg::new("FILE1")
.help("The first input file")
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("FILE2")
.help("The second input file")
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("completions")
.long("completions")
.value_name("SHELL")
.value_parser(clap::value_parser!(Shell))
.help("Generate shell completions for the specified shell"),
)
.arg(
Arg::new("format")
.short('f')
.long("format")
.value_name("FORMAT")
.help("Input file format (auto-detected if not specified)")
.value_parser(["json", "yaml", "csv", "toml", "ini", "xml"]),
)
.arg(
Arg::new("output")
.short('o')
.long("output")
.value_name("OUTPUT")
.help("Output format"),
)
.arg(
Arg::new("path")
.long("path")
.value_name("PATH")
.help("Filter by path (only show differences in paths containing this string)"),
)
.arg(
Arg::new("ignore-keys-regex")
.long("ignore-keys-regex")
.value_name("PATTERN")
.help("Ignore keys matching this regex pattern"),
)
.arg(
Arg::new("epsilon")
.long("epsilon")
.value_name("VALUE")
.help("Numerical comparison tolerance (for floating point numbers)"),
)
.arg(
Arg::new("array-id-key")
.long("array-id-key")
.value_name("KEY")
.help("Array comparison by ID key (compare arrays by this field instead of index)"),
)
.arg(
Arg::new("ignore-whitespace")
.long("ignore-whitespace")
.action(clap::ArgAction::SetTrue)
.help("Ignore whitespace differences"),
)
.arg(
Arg::new("ignore-case")
.long("ignore-case")
.action(clap::ArgAction::SetTrue)
.help("Ignore case differences"),
)
.arg(
Arg::new("quiet")
.short('q')
.long("quiet")
.action(clap::ArgAction::SetTrue)
.help("Suppress normal output; return only exit status"),
)
.arg(
Arg::new("brief")
.long("brief")
.action(clap::ArgAction::SetTrue)
.help("Report only whether files differ, not the differences"),
)
.arg(
Arg::new("verbose")
.short('v')
.long("verbose")
.action(clap::ArgAction::SetTrue)
.help("Show verbose processing information"),
)
.arg(
Arg::new("no-color")
.long("no-color")
.action(clap::ArgAction::SetTrue)
.help("Disable colored output"),
)
.arg(
Arg::new("memory-optimization")
.long("memory-optimization")
.action(clap::ArgAction::SetTrue)
.help("Enable memory optimization for large files"),
)
.arg(
Arg::new("batch-size")
.long("batch-size")
.value_name("SIZE")
.help("Batch size for memory optimization"),
)
.arg(
Arg::new("show-unchanged")
.long("show-unchanged")
.action(clap::ArgAction::SetTrue)
.help("Show unchanged values as well"),
)
.arg(
Arg::new("show-types")
.long("show-types")
.action(clap::ArgAction::SetTrue)
.help("Show type information in output"),
)
}

fn main() {
// Only generate man page and completions for release builds
let out_dir = match env::var_os("OUT_DIR") {
Some(dir) => PathBuf::from(dir),
None => return,
};

let cmd = build_cli();

// Generate man page
let man_dir = out_dir.join("man");
fs::create_dir_all(&man_dir).unwrap();

let man = Man::new(cmd.clone());
let mut buffer = Vec::new();
man.render(&mut buffer).unwrap();
fs::write(man_dir.join("diffx.1"), buffer).unwrap();

// Generate shell completions
let completions_dir = out_dir.join("completions");
fs::create_dir_all(&completions_dir).unwrap();

for shell in [Shell::Bash, Shell::Zsh, Shell::Fish, Shell::PowerShell] {
let mut cmd = build_cli();
clap_complete::generate_to(shell, &mut cmd, "diffx", &completions_dir).unwrap();
}

println!("cargo:rerun-if-changed=build.rs");
}
68 changes: 46 additions & 22 deletions diffx-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::{generate, Shell};
use diffx_core::{
diff, diff_paths, format_diff_output, parse_csv, parse_ini, parse_xml, value_type_name,
DiffOptions, DiffResult, DiffxSpecificOptions, OutputFormat,
Expand Down Expand Up @@ -55,12 +56,16 @@ mod color_utils {
#[command(version)]
struct Args {
/// The first input file
#[arg(value_name = "FILE1")]
input1: PathBuf,
#[arg(value_name = "FILE1", required_unless_present = "completions")]
input1: Option<PathBuf>,

/// The second input file
#[arg(value_name = "FILE2")]
input2: PathBuf,
/// The second input file
#[arg(value_name = "FILE2", required_unless_present = "completions")]
input2: Option<PathBuf>,

/// Generate shell completions for the specified shell
#[arg(long, value_enum, value_name = "SHELL")]
completions: Option<Shell>,

/// Input file format (auto-detected if not specified)
#[arg(short, long, value_enum)]
Expand Down Expand Up @@ -157,15 +162,28 @@ fn main() {

fn run() -> Result<()> {
let args = Args::parse();

// Handle shell completions
if let Some(shell) = args.completions {
let mut cmd = Args::command();
let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut io::stdout());
return Ok(());
}

// At this point, input1 and input2 are guaranteed to be present
let input1 = args.input1.as_ref().unwrap();
let input2 = args.input2.as_ref().unwrap();

let start_time = Instant::now();

// Check for stdin usage
let input1_is_stdin = args.input1.to_str() == Some("-");
let input2_is_stdin = args.input2.to_str() == Some("-");
let input1_is_stdin = input1.to_str() == Some("-");
let input2_is_stdin = input2.to_str() == Some("-");

if input1_is_stdin || input2_is_stdin {
// Handle stdin cases
return handle_stdin_input(&args, input1_is_stdin, input2_is_stdin);
return handle_stdin_input(&args, input1, input2, input1_is_stdin, input2_is_stdin);
}

// Build options from CLI arguments
Expand All @@ -178,8 +196,8 @@ fn run() -> Result<()> {
eprintln!("Batch size: {}", args.batch_size.unwrap_or(1000));

// Input file information
if let Ok(metadata1) = fs::metadata(&args.input1) {
if let Ok(metadata2) = fs::metadata(&args.input2) {
if let Ok(metadata1) = fs::metadata(input1) {
if let Ok(metadata2) = fs::metadata(input2) {
eprintln!("Input file information:");
eprintln!(" Input 1 size: {} bytes", metadata1.len());
eprintln!(" Input 2 size: {} bytes", metadata2.len());
Expand Down Expand Up @@ -215,8 +233,8 @@ fn run() -> Result<()> {
let mut options_no_filter = options.clone();
options_no_filter.path_filter = None;
let unfiltered_results = diff_paths(
&args.input1.to_string_lossy(),
&args.input2.to_string_lossy(),
&input1.to_string_lossy(),
&input2.to_string_lossy(),
Some(&options_no_filter),
)?;
Some(unfiltered_results.len())
Expand All @@ -225,8 +243,8 @@ fn run() -> Result<()> {
};

let results = diff_paths(
&args.input1.to_string_lossy(),
&args.input2.to_string_lossy(),
&input1.to_string_lossy(),
&input2.to_string_lossy(),
Some(&options),
)?;
let diff_time = parse_start.elapsed();
Expand All @@ -243,8 +261,8 @@ fn run() -> Result<()> {
} else {
println!(
"Files {} and {} differ",
args.input1.display(),
args.input2.display()
input1.display(),
input2.display()
);
}
std::process::exit(if results.is_empty() { 0 } else { 1 });
Expand Down Expand Up @@ -431,22 +449,28 @@ fn parse_content(content: &str, format: Format) -> Result<Value> {
}
}

fn handle_stdin_input(args: &Args, input1_is_stdin: bool, input2_is_stdin: bool) -> Result<()> {
fn handle_stdin_input(
args: &Args,
input1: &PathBuf,
input2: &PathBuf,
input1_is_stdin: bool,
input2_is_stdin: bool,
) -> Result<()> {
if input1_is_stdin && input2_is_stdin {
// Case 2 & 3: Both inputs from stdin - read two data sets from stdin
return handle_both_stdin(args);
}

// Case 1: One stdin, one file
let content1 = read_input(&args.input1)?;
let content2 = read_input(&args.input2)?;
let content1 = read_input(input1)?;
let content2 = read_input(input2)?;

// Determine input format
let input_format = if let Some(fmt) = args.format {
fmt
} else {
infer_format_from_path(&args.input1)
.or_else(|| infer_format_from_path(&args.input2))
infer_format_from_path(input1)
.or_else(|| infer_format_from_path(input2))
.context("Could not infer format from file extensions. Please specify --format.")?
};

Expand Down
15 changes: 15 additions & 0 deletions diffx-cli/tests/examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Example-based tests using trycmd
//!
//! These tests serve dual purposes:
//! 1. Automated regression testing
//! 2. Living documentation that shows real input/output examples
//!
//! The Markdown files in `docs/examples/` are the source of truth
//! for user documentation and are verified by these tests.

#[test]
fn cli_examples() {
trycmd::TestCases::new()
.case("../docs/examples/*.md")
.run();
}
Loading
Loading