| clean | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| DSL.md | ||
| HTTPFilter.pm | ||
| MOCK.md | ||
| README.md | ||
| routes.json | ||
| SPEC.md | ||
| test | ||
| TODOS.md | ||
| zig_http_bugs.md | ||
PostCLI - HTTP Command Processor
A Zig-based command-line tool for executing HTTP commands with support for environment variables, macros, and JSON path extraction.
Why?
I don't really like postman style HTTP testing. This is an alternative.
- It is hard, using source control, to version postman/insomnia/hoppscotch files. I think this is because their GUI confuses people, but I don't know it if it is also hard to version the files themselves.
- It is very easy to dangerously commit secrets with aforementioned files, like production tokens, host topology, logging of requests with sensitive information.
- It is very hard to create flows that depend on prior requests without using very convoluted syntax. For example get the auth token from request #1, use that token in subsequent requests, then use the part of the JSON response from request #3 in the next request, etc.
postcli makes it easy to do the right thing, and hard to do the wrong thing
- Automatically load .env files, which are usually not included in source repositories. Your HTTP testing files use the same things your code uses.
- Easily set variables from JSON reponses using jq syntax or from headers.
- Automatically assign variables, using sensible defaults, to requests and responses and extracted JSON values. Then, you can use those variables in successive requests. And, because they are a stack of variables, you can add flows in between without changing indices to the variable.
- Use syntax very similar to curl, reduce the learning curve (in fact if you use curl style switches you can run basic scripts using curl itself)
- Enable an optional perl mode so you can add on control flow only when you need it
Installation
Prerequisites
Install Zig (version 0.11.0 or later):
# macOS (using Homebrew)
brew install zig
# Or download from https://ziglang.org/download/
Building
zig build
The executable will be created in zig-out/bin/postcli.
Usage
./zig-out/bin/postcli <script-file> [options]
./zig-out/bin/postcli --perl <script-file> [--dry-run-print]
Options
-e, --env <file>: Load additional environment variables from file (can be used multiple times)--verbose-insecure: Print request headers and body--output-insecure: Print full responses--perl: Execute script as Perl with HTTPFilter module (see Perl Mode section)--dry-run-print: Print transformed Perl code and exit (perl mode only)-h, --help: Show help message
Features
Environment Variables
- Automatically loads
.envfile from current directory - Variables are interpolated using
$VARIABLE_NAMEsyntax - Additional env files can be loaded with
--envflag
HTTP Methods
Supports: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
Command Syntax
METHOD $URL [modifiers] [| json.path]
Headers
Use h() to add headers:
GET $SERVER/api h(Authorization=Bearer token,Accept=application/json)
Or use curl-style -H flags:
GET $SERVER/api -H "Authorization: Bearer token" -H "Accept: application/json"
JSON Body
Use b() for JSON body in POST/PUT requests:
POST $SERVER/api b(name=$NAME,age=25)
Use j() for JSON body with automatic headers (adds Accept: application/json and Content-Type: application/json):
POST $SERVER/api j(name=$NAME,age=25)
JSON Path Extraction
Use pipe to extract values from response:
GET $SERVER/api | .data.id
Variable Storage
Store extracted values in variables:
GET $SERVER/api | .title
$R[0] | .description
POST $SERVER/new b(title=$V[1],desc=$V[0])
Variables are accessed in reverse order (stack-based):
$V[0]= most recent value$V[1]= second most recent value
Macros
- Place macro files in
.macros/directory - Or in
~/.config/postcli/macros/ - Use with pipe syntax:
GET|DEBUG $SERVER/api
Example Script
GET $SERVER/posts/1 | .title
$R[0] = .body
POST $SERVER/posts b(title=$V[1],body=$V[0],userId=1)
Variable Validation
When the postcli command line runs, it validates all variables to ensure they are properly defined in loaded .env files. Variable validation follows these rules:
- Variables are detected after HTTP method verbs (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
- Variables must start with a dollar sign (
$) - All variables must be defined in loaded environment files (.env)
- Undefined variables will cause the command to fail with an error
Examples:
Valid (assuming SERVER is defined in .env):
GET $SERVER/api/users
POST $SERVER/api/users b(name=$USERNAME)
Invalid (will fail validation):
GET $UNDEFINED_SERVER/api/users # $UNDEFINED_SERVER not in .env
POST $SERVER/api b(name=$MISSING) # $MISSING not in .env
The validation helps catch configuration errors early and ensures all required environment variables are properly set before executing HTTP requests.
Testing
Run the test script:
./zig-out/bin/postcli test.http --output-insecure
Run validation tests:
zig build test
Perl Mode with HTTPFilter
PostCLI includes a powerful Perl mode that transforms postcli syntax into native Perl LWP (Library for WWW in Perl) requests. This mode provides enhanced functionality and better SSL handling.
Usage
# Execute postcli script as Perl
./zig-out/bin/postcli --perl script.postcli
# Show transformed Perl code without executing
./zig-out/bin/postcli --perl --dry-run-print script.postcli
HTTPFilter Module
The HTTPFilter Perl module automatically:
- Transforms HTTP syntax - Converts postcli commands to LWP requests
- Handles SSL issues - Automatically disables SSL verification to avoid certificate problems
- Manages variables - Provides
$V[n]and$R[n]variable support - Loads environment - Automatically reads
.envfiles - Extracts JSON paths - Supports jq-style path extraction
SSL Configuration
HTTPFilter automatically configures SSL to avoid common certificate issues:
# Automatically added by HTTPFilter
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
$ua->ssl_opts(verify_hostname => 0);
Code Generation
HTTPFilter generates clean, DRY (Don't Repeat Yourself) Perl code using a centralized http_request() function:
Input (postcli syntax):
GET $SERVER/api h(auth:token) | .data.id
POST $SERVER/users b(name:John,age:30)
Output (generated Perl):
http_request('GET', '$SERVER/api', { 'auth' => 'token' }, undef, '.data.id');
http_request('POST', '$SERVER/users', undef, '{"name":"John","age":"30"}', undef);
Generated Perl Structure
The HTTPFilter generates complete Perl programs with:
#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;
use HTTP::Request;
use JSON;
# Global variables for state management
my @variables; # Stack of extracted values ($V[0], $V[1], etc.)
my @responses; # Stack of raw responses ($R[0], $R[1], etc.)
my %env_vars; # Environment variables from .env files
# SSL-configured user agent
my $ua = LWP::UserAgent->new();
$ua->ssl_opts(verify_hostname => 0);
$ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
# Centralized HTTP request function
sub http_request {
my ($method, $url, $headers, $body, $jq_path) = @_;
# ... handles all HTTP logic with proper error handling
}
# Your transformed requests
http_request('GET', '$SERVER/api', undef, undef, undef);
Benefits of Perl Mode
- Better SSL Support - Automatically handles certificate issues
- Rich Ecosystem - Access to CPAN modules and Perl libraries
- Clean Code Generation - DRY approach with centralized request handling
- Enhanced Debugging - Use
--dry-run-printto see exact Perl code - Variable Interpolation - Full support for environment and runtime variables
Installing Perl Dependencies
To use Perl mode, you need LWP modules:
# Using CPAN
cpan LWP::UserAgent HTTP::Request JSON
# Using system package manager (Ubuntu/Debian)
sudo apt-get install libwww-perl libjson-perl
# Using system package manager (macOS)
brew install perl
cpan LWP JSON
Advanced Examples
With headers and JSON extraction:
# postcli script
GET $API_BASE/users h(Authorization=Bearer $TOKEN) | .data[0].id
POST $API_BASE/posts b(title=$TITLE,userId=$V[0])
# Run in Perl mode
./zig-out/bin/postcli --perl api-script.postcli
# See generated code
./zig-out/bin/postcli --perl --dry-run-print api-script.postcli
Generated function calls:
http_request('GET', '$API_BASE/users',
{ 'Authorization' => 'Bearer $TOKEN' }, undef, '.data[0].id');
http_request('POST', '$API_BASE/posts', undef,
'{"title":"$TITLE","userId":"$V[0]"}', undef);
Migration from Native Mode
Both modes use the same postcli syntax, so scripts are fully compatible:
# Native Zig mode
./zig-out/bin/postcli script.postcli
# Perl mode (same script)
./zig-out/bin/postcli --perl script.postcli
The Perl mode provides enhanced SSL handling and debugging capabilities while maintaining full compatibility with existing postcli scripts.
Development History & Recent Improvements
Argument Parsing Modernization
- Migrated from manual parsing to zig-clap - Replaced custom argument parsing with the industry-standard zig-clap library for better reliability and maintainability
- Fixed argument order dependencies - Arguments can now be provided in any order
- Enhanced error handling - Better error messages and validation
Perl Mode Enhancements
- Added --dry-run-print functionality - Users can now see the exact Perl/LWP code that HTTPFilter generates
- DRY code generation - Refactored HTTPFilter to generate clean, maintainable Perl code using centralized
http_request()function instead of repetitive boilerplate - SSL issue resolution - Automatic SSL certificate verification bypass to handle common certificate authority issues in Perl LWP
Error Handling Improvements
- Graceful HTTP error handling - Fixed crashes on 404/500 errors; now shows proper error messages and exits cleanly
- Memory leak fixes - Proper cleanup on error conditions
- Better validation feedback - Clear messages for undefined variables and configuration issues
Code Quality
- Test structure reorganization - Cleaned up test files and improved
zig build testfunctionality - Comprehensive documentation - Added detailed README sections covering all features and modes
- Variable validation system - Proactive validation of environment variables before script execution
Before vs After Code Generation
Before (repetitive):
{
my $url = interpolate_vars('$SERVER/api');
print "GET $url\n";
my $req = HTTP::Request->new('GET', $url);
my $response = $ua->request($req);
my $response_body = $response->content;
push @responses, $response_body;
push @variables, $response_body;
}
After (DRY):
http_request('GET', '$SERVER/api', undef, undef, undef);
These improvements make PostCLI more robust, user-friendly, and maintainable while preserving full backward compatibility with existing scripts.