Skip to content

Commit 3bd7763

Browse files
cofycclaude
andcommitted
fix: prevent segfault in usage display and enhance build system
- Fix null pointer dereference in argparse_usage() when options->help is NULL - Add installation support to both Makefile and CMake with PREFIX configuration - Add pkg-config file generation for easier library integration - Enhance test coverage with comprehensive test suite: - Test NULL help text handling to prevent segfaults - Test callback functionality with multiple scenarios - Test option groups display formatting - Test ARGPARSE_IGNORE_UNKNOWN_ARGS flag behavior - Add stress test with 71 options to verify scalability - Add CLAUDE.md with project guidance and build instructions - Configure .claude/settings.json for development workflow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent eb22de8 commit 3bd7763

16 files changed

Lines changed: 595 additions & 1 deletion

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(gh issue view:*)",
5+
"Bash(make:*)",
6+
"Bash(make:*)"
7+
],
8+
"deny": []
9+
}
10+
}

CLAUDE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Build Commands
6+
7+
This is a C library project with both Makefile and CMake build systems:
8+
9+
- Build library: `make` (builds both shared and static libraries)
10+
- Run tests: `make test`
11+
- Clean: `make clean`
12+
- CMake build: `mkdir build && cd build && cmake .. && make`
13+
14+
## Test System
15+
16+
Tests use TAP (Test Anything Protocol) with `prove` command:
17+
- Test directory: `tests/`
18+
- Run all tests: `make test` (from root) or `prove *.sh` (from tests/)
19+
- Test executables are built from `.c` files and run via `.sh` shell scripts
20+
- Tests link against `../argparse.c` directly
21+
- All commands in sub-directory tests can be executed without user confirmation.
22+
- Always use `make -C tests <target>` to run test cases.
23+
24+
## Library Architecture
25+
26+
Single-header library design:
27+
- `argparse.h` - Public API with option definitions and parser structure
28+
- `argparse.c` - Implementation with argument parsing logic
29+
- Supports both boolean and value options (integer, float, string)
30+
- Options can have short (`-f`) and long (`--force`) forms
31+
- Automatic help generation and error handling
32+
33+
Key structures:
34+
- `struct argparse` - Main parser context
35+
- `struct argparse_option` - Option definition with type, names, and callbacks
36+
- Option types: `ARGPARSE_OPT_BOOLEAN`, `ARGPARSE_OPT_INTEGER`, `ARGPARSE_OPT_FLOAT`, `ARGPARSE_OPT_STRING`
37+
38+
Two usage patterns demonstrated in tests:
39+
- Basic options parsing (`tests/basic.c`)
40+
- Subcommand handling (`tests/subcommands.c`)

CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,28 @@ if(ARGPARSE_STATIC)
2525
set_target_properties(argparse_static PROPERTIES OUTPUT_NAME argparse_static)
2626
endif()
2727

28+
# Installation
29+
include(GNUInstallDirs)
30+
31+
# Install header
32+
install(FILES argparse.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
33+
34+
# Install libraries
35+
if(ARGPARSE_SHARED)
36+
install(TARGETS argparse_shared
37+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
38+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
39+
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
40+
endif()
41+
42+
if(ARGPARSE_STATIC)
43+
install(TARGETS argparse_static
44+
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
45+
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
46+
endif()
47+
48+
# Generate and install pkg-config file
49+
configure_file(argparse.pc.in argparse.pc @ONLY)
50+
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/argparse.pc
51+
DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
52+

Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Defaults
2+
PREFIX ?= /usr/local
23
CFLAGS ?= -O3 -g -ggdb
34
LDFLAGS ?=
45
CROSS_COMPILE =
@@ -42,6 +43,18 @@ $(STLIBNAME): argparse.o
4243
test:
4344
make -C tests/ test
4445

46+
install: $(DYLIBNAME) $(STLIBNAME)
47+
install -d $(PREFIX)/lib
48+
install -d $(PREFIX)/include
49+
install -m 644 argparse.h $(PREFIX)/include/
50+
install -m 755 $(DYLIBNAME) $(PREFIX)/lib/
51+
install -m 644 $(STLIBNAME) $(PREFIX)/lib/
52+
53+
uninstall:
54+
rm -f $(PREFIX)/include/argparse.h
55+
rm -f $(PREFIX)/lib/$(DYLIBNAME)
56+
rm -f $(PREFIX)/lib/$(STLIBNAME)
57+
4558
clean:
4659
rm -rf *.[ao]
4760
rm -rf *.so

argparse.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ argparse_usage(struct argparse *self)
369369
fputc('\n', stdout);
370370
pad = usage_opts_width;
371371
}
372-
fprintf(stdout, "%*s%s\n", (int)pad + 2, "", options->help);
372+
fprintf(stdout, "%*s%s\n", (int)pad + 2, "", options->help ? options->help : "");
373373
}
374374

375375
// print epilog

argparse.pc.in

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
prefix=@CMAKE_INSTALL_PREFIX@
2+
exec_prefix=${prefix}
3+
libdir=@CMAKE_INSTALL_FULL_LIBDIR@
4+
includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
5+
6+
Name: argparse
7+
Description: Command-line argument parsing library for C/C++
8+
Version: @PROJECT_VERSION@
9+
Libs: -L${libdir} -largparse
10+
Cflags: -I${includedir}

tests/test_callbacks.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
#include "../argparse.h"
5+
6+
static int callback_count = 0;
7+
static char callback_results[256] = "";
8+
9+
static int
10+
simple_callback(struct argparse *self, const struct argparse_option *option)
11+
{
12+
(void)self;
13+
callback_count++;
14+
strcat(callback_results, option->long_name);
15+
strcat(callback_results, ",");
16+
return 0;
17+
}
18+
19+
static int
20+
value_callback(struct argparse *self, const struct argparse_option *option)
21+
{
22+
(void)self;
23+
callback_count++;
24+
if (option->value && *(const char **)option->value) {
25+
strcat(callback_results, *(const char **)option->value);
26+
strcat(callback_results, ":");
27+
}
28+
strcat(callback_results, option->long_name);
29+
strcat(callback_results, ",");
30+
return 0;
31+
}
32+
33+
static int
34+
error_callback(struct argparse *self, const struct argparse_option *option)
35+
{
36+
(void)self;
37+
(void)option;
38+
return -1; // Simulate error
39+
}
40+
41+
static const char *const usages[] = {
42+
"test_callbacks [options]",
43+
NULL,
44+
};
45+
46+
int main(int argc, const char **argv)
47+
{
48+
int force = 0;
49+
const char *path = NULL;
50+
const char *name = NULL;
51+
52+
struct argparse_option options[] = {
53+
OPT_HELP(),
54+
OPT_GROUP("Basic options"),
55+
OPT_BOOLEAN('f', "force", &force, "force mode", &simple_callback, 0, 0),
56+
OPT_STRING('p', "path", &path, "path value", &value_callback, 0, 0),
57+
OPT_STRING('n', "name", &name, "name value", &value_callback, 0, 0),
58+
OPT_BOOLEAN('e', "error", NULL, "trigger error", &error_callback, 0, 0),
59+
OPT_END(),
60+
};
61+
62+
struct argparse argparse;
63+
argparse_init(&argparse, options, usages, 0);
64+
argparse_describe(&argparse, "\nTest program for callback functionality.", NULL);
65+
66+
// Parse with potential error callback
67+
argc = argparse_parse(&argparse, argc, argv);
68+
69+
printf("callback_count: %d\n", callback_count);
70+
printf("callback_results: %s\n", callback_results);
71+
printf("force: %d\n", force);
72+
printf("path: %s\n", path ? path : "(null)");
73+
printf("name: %s\n", name ? name : "(null)");
74+
printf("remaining_args: %d\n", argc);
75+
76+
return 0;
77+
}

tests/test_callbacks.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
3+
. $(dirname "$0")/tap-functions
4+
5+
plan_tests 10
6+
7+
# Compile the test
8+
gcc -o test_callbacks test_callbacks.c ../argparse.c || bailout "Failed to compile test_callbacks"
9+
10+
# Test 1: No callbacks triggered
11+
output=$(./test_callbacks 2>&1)
12+
is "$?" "0" "No callbacks - exit code 0"
13+
echo "$output" | grep -q "callback_count: 0"
14+
is "$?" "0" "No callbacks triggered"
15+
16+
# Test 2: Simple boolean callback
17+
output=$(./test_callbacks --force 2>&1)
18+
is "$?" "0" "Boolean callback - exit code 0"
19+
echo "$output" | grep -q "callback_count: 1"
20+
is "$?" "0" "One callback triggered"
21+
echo "$output" | grep -q "callback_results: force,"
22+
is "$?" "0" "Callback recorded force option"
23+
24+
# Test 3: Value callback with string
25+
output=$(./test_callbacks --path=/tmp/test 2>&1)
26+
is "$?" "0" "String callback - exit code 0"
27+
echo "$output" | grep -q "callback_results: /tmp/test:path,"
28+
is "$?" "0" "Callback recorded path value"
29+
30+
# Test 4: Multiple callbacks
31+
output=$(./test_callbacks --force --path=/tmp --name=test 2>&1)
32+
is "$?" "0" "Multiple callbacks - exit code 0"
33+
echo "$output" | grep -q "callback_count: 3"
34+
is "$?" "0" "Three callbacks triggered"
35+
echo "$output" | grep -q "force,/tmp:path,test:name,"
36+
is "$?" "0" "All callbacks recorded in order"
37+
38+
# Cleanup
39+
rm -f test_callbacks

tests/test_groups.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include "../argparse.h"
4+
5+
static const char *const usages[] = {
6+
"test_groups [options]",
7+
NULL,
8+
};
9+
10+
int main(int argc, const char **argv)
11+
{
12+
int verbose = 0;
13+
int debug = 0;
14+
int force = 0;
15+
int quiet = 0;
16+
const char *input = NULL;
17+
const char *output = NULL;
18+
const char *format = NULL;
19+
int threads = 1;
20+
21+
struct argparse_option options[] = {
22+
OPT_HELP(),
23+
OPT_GROUP("General options"),
24+
OPT_BOOLEAN('v', "verbose", &verbose, "enable verbose output", NULL, 0, 0),
25+
OPT_BOOLEAN('d', "debug", &debug, "enable debug mode", NULL, 0, 0),
26+
OPT_BOOLEAN('q', "quiet", &quiet, "suppress normal output", NULL, 0, 0),
27+
OPT_GROUP("Input/Output options"),
28+
OPT_STRING('i', "input", &input, "input file path", NULL, 0, 0),
29+
OPT_STRING('o', "output", &output, "output file path", NULL, 0, 0),
30+
OPT_STRING('f', "format", &format, "output format (json, xml, csv)", NULL, 0, 0),
31+
OPT_GROUP("Processing options"),
32+
OPT_INTEGER('t', "threads", &threads, "number of threads", NULL, 0, 0),
33+
OPT_BOOLEAN('F', "force", &force, "force overwrite", NULL, 0, 0),
34+
OPT_GROUP(""), // Empty group name
35+
OPT_BOOLEAN(0, "hidden", NULL, "hidden option", NULL, 0, 0),
36+
OPT_END(),
37+
};
38+
39+
struct argparse argparse;
40+
argparse_init(&argparse, options, usages, 0);
41+
argparse_describe(&argparse, "\nTest program for option groups display.",
42+
"\nThis program demonstrates how option groups are displayed in help.");
43+
44+
// Just trigger help to see group display
45+
if (argc == 1) {
46+
argparse_usage(&argparse);
47+
return 0;
48+
}
49+
50+
argc = argparse_parse(&argparse, argc, argv);
51+
52+
printf("Options parsed successfully\n");
53+
54+
return 0;
55+
}

tests/test_groups.sh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/bin/bash
2+
3+
. $(dirname "$0")/tap-functions
4+
5+
plan_tests 8
6+
7+
# Compile the test
8+
gcc -o test_groups test_groups.c ../argparse.c || bailout "Failed to compile test_groups"
9+
10+
# Test 1: Check that help displays all groups
11+
output=$(./test_groups 2>&1)
12+
is "$?" "0" "Help display exits with 0"
13+
14+
# Test 2-5: Verify each group header appears
15+
echo "$output" | grep -q "General options"
16+
is "$?" "0" "General options group displayed"
17+
18+
echo "$output" | grep -q "Input/Output options"
19+
is "$?" "0" "Input/Output options group displayed"
20+
21+
echo "$output" | grep -q "Processing options"
22+
is "$?" "0" "Processing options group displayed"
23+
24+
# Test 6: Verify options appear under correct groups
25+
echo "$output" | grep -A3 "General options" | grep -q -- "--verbose"
26+
is "$?" "0" "verbose option under General options"
27+
28+
echo "$output" | grep -A3 "Input/Output options" | grep -q -- "--input"
29+
is "$?" "0" "input option under Input/Output options"
30+
31+
# Test 7: Verify help formatting with groups
32+
echo "$output" | grep -q -- "-h, --help"
33+
is "$?" "0" "Help option displayed correctly"
34+
35+
# Test 8: Test actual parsing still works
36+
output=$(./test_groups --verbose --input=test.txt 2>&1)
37+
is "$?" "0" "Options parse successfully with groups"
38+
39+
# Cleanup
40+
rm -f test_groups

0 commit comments

Comments
 (0)