2022-11-20 12:54:26 -08:00
// Copyright (c) 2022 torque <torque@users.noreply.github.com>
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
const std = @import ( " std " ) ;
const StructField = std . builtin . Type . StructField ;
2022-11-26 20:29:23 -08:00
pub const meta = @import ( " ./meta.zig " ) ;
2022-11-27 01:31:20 -08:00
pub const params = @import ( " ./params.zig " ) ;
pub const Command = @import ( " ./bakery.zig " ) . Command ;
2022-11-20 12:54:26 -08:00
pub const OptionError = error {
BadShortOption ,
BadLongOption ,
UnknownOption ,
MissingOption ,
MissingArgument ,
ExtraArguments ,
} ;
2022-11-27 01:31:20 -08:00
/// spec is a tuple of Option, Flag, and Argument
2022-11-26 20:29:23 -08:00
pub fn CommandParser (
2022-11-27 01:31:20 -08:00
comptime commandData : params . CommandData ,
2022-11-20 12:54:26 -08:00
comptime spec : anytype ,
2022-11-26 20:29:23 -08:00
comptime UserContext : type ,
comptime callback : * const fn ( UserContext , CommandResult ( spec , UserContext ) ) anyerror ! void ,
2022-11-20 12:54:26 -08:00
) type {
comptime var argCount = 0 ;
comptime for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
2022-11-20 12:54:26 -08:00
. Argument = > argCount + = 1 ,
2022-11-26 20:29:23 -08:00
. Option , . Flag , . Command = > continue ,
2022-11-20 12:54:26 -08:00
}
} ;
2022-11-26 20:29:23 -08:00
const ResultType = CommandResult ( spec , UserContext ) ;
2022-11-20 12:54:26 -08:00
const RequiredType = RequiredTracker ( spec ) ;
const ParseState = enum { Mixed , ForcedArgs } ;
return struct {
2022-11-27 01:31:20 -08:00
pub const brand : params . Brand = . Command ;
2022-11-26 20:29:23 -08:00
pub const ContextType = UserContext ;
// this should be copied at compile time
2022-11-27 01:31:20 -08:00
var data : params . CommandData = commandData ;
2022-11-20 12:54:26 -08:00
/// parse command line arguments from an iterator
2022-11-26 20:29:23 -08:00
pub fn execute ( self : @This ( ) , alloc : std . mem . Allocator , comptime argit_type : type , argit : * argit_type , context : UserContext ) ! void {
try self . attachSubcommands ( alloc ) ;
2022-11-20 12:54:26 -08:00
var result : ResultType = createCommandresult ( ) ;
var required : RequiredType = . { } ;
var parseState : ParseState = . Mixed ;
2022-11-26 20:29:23 -08:00
try extractEnvVars ( alloc , & result , & required , context ) ;
2022-11-20 12:54:26 -08:00
var seenArgs : u32 = 0 ;
argloop : while ( argit . next ( ) ) | arg | {
if ( parseState = = . Mixed and arg . len > 1 and arg [ 0 ] = = '-' ) {
if ( std . mem . eql ( u8 , " -- " , arg ) ) {
// TODO: the way this works, -- only forces argument
// parsing until a subcommand is found. This seems
// reasonable to me, but it may be unexpected that
// `command -a -- subcommand -b` parses b as an option
// flag. We could propagate the forced args flag to
// subcommands, but I'm not sure that would be better.
//
// Another option is to stop parsing altogether when --
// is hit, but that means that subcommands cannot be
// invoked at the same time as forced arguments, which
// seems worse somehow, as it affects macroscopic CLI
// behavior.
parseState = . ForcedArgs ;
continue : argloop ;
}
if ( arg [ 1 ] = = '-' ) {
// we have a long flag or option
specloop : inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
2022-11-20 12:54:26 -08:00
. Option = > {
// have to force lower the handler to runtime
2022-11-26 20:29:23 -08:00
// var handler = param.handler.?;
2022-11-20 12:54:26 -08:00
if ( param . long ) | flag | {
if ( std . mem . eql ( u8 , flag , arg ) ) {
if ( comptime param . required ( ) ) {
@field ( required , param . name ) = true ;
}
const val = argit . next ( ) orelse return OptionError . MissingArgument ;
if ( param . hideResult = = false ) {
2022-11-26 20:29:23 -08:00
@field ( result , param . name ) = try param . handler . ? ( context , val ) ;
2022-11-20 12:54:26 -08:00
}
continue : argloop ;
}
}
} ,
. Flag = > {
inline for ( . { . { param . truthy . long , true } , . { param . falsy . long , false } } ) | variant | {
if ( variant [ 0 ] ) | flag | {
if ( std . mem . eql ( u8 , flag , arg ) ) {
if ( param . eager ) | handler | {
2022-11-26 20:29:23 -08:00
try handler ( context , data ) ;
2022-11-20 12:54:26 -08:00
}
if ( param . hideResult = = false ) {
@field ( result , param . name ) = variant [ 1 ] ;
}
continue : argloop ;
}
}
}
} ,
. Argument , . Command = > continue : specloop ,
}
}
// nothing matched
return OptionError . UnknownOption ;
} else {
// we have a short flag, which may be multiple fused flags
2023-03-20 23:02:13 -07:00
shortloop : for ( arg [ 1 . . ] , 0 . . ) | shorty , idx | {
2022-11-20 12:54:26 -08:00
specloop : inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
2022-11-20 12:54:26 -08:00
. Option = > {
2022-11-26 20:29:23 -08:00
// var handler = param.handler.?;
2022-11-20 12:54:26 -08:00
if ( param . short ) | flag | {
if ( flag [ 1 ] = = shorty ) {
if ( comptime param . required ( ) ) {
@field ( required , param . name ) = true ;
}
const val = if ( arg . len > ( idx + 2 ) )
arg [ ( idx + 2 ) . . ]
else
argit . next ( ) orelse return OptionError . MissingArgument ;
if ( param . hideResult = = false ) {
2022-11-26 20:29:23 -08:00
@field ( result , param . name ) = try param . handler . ? ( context , val ) ;
2022-11-20 12:54:26 -08:00
}
continue : argloop ;
}
}
} ,
. Flag = > {
inline for ( . { . { param . truthy . short , true } , . { param . falsy . short , false } } ) | variant | {
if ( variant [ 0 ] ) | flag | {
if ( flag [ 1 ] = = shorty ) {
if ( param . eager ) | handler | {
2022-11-26 20:29:23 -08:00
try handler ( context , data ) ;
2022-11-20 12:54:26 -08:00
}
if ( param . hideResult = = false ) {
@field ( result , param . name ) = variant [ 1 ] ;
}
continue : shortloop ;
}
}
}
} ,
. Argument , . Command = > continue : specloop ,
}
}
// nothing matched
return OptionError . UnknownOption ;
}
}
} else {
// we have a subcommand or an Argument. Arguments are parsed first, exclusively.
defer seenArgs + = 1 ;
comptime var idx = 0 ;
inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
. Command = > {
if ( std . mem . eql ( u8 , @TypeOf ( param ) . data . name , arg ) ) {
// we're calling a subcommand
try checkErrors ( seenArgs , required ) ;
try callback ( context , result ) ;
return param . execute ( alloc , argit_type , argit , context ) ;
}
} ,
2022-11-20 12:54:26 -08:00
. Argument = > {
if ( seenArgs = = idx ) {
2022-11-26 20:29:23 -08:00
if ( comptime param . required ( ) ) {
@field ( required , param . name ) = true ;
}
// var handler = param.handler;
@field ( result , param . name ) = try param . handler . ? ( context , arg ) ;
2022-11-20 12:54:26 -08:00
continue : argloop ;
}
idx + = 1 ;
} ,
else = > continue ,
}
}
}
}
try checkErrors ( seenArgs , required ) ;
2022-11-26 20:29:23 -08:00
try callback ( context , result ) ;
2022-11-20 12:54:26 -08:00
}
2022-11-27 01:31:20 -08:00
pub fn OutType ( ) type {
return CommandResult ( spec , UserContext ) ;
}
2022-11-20 12:54:26 -08:00
inline fn checkErrors ( seenArgs : u32 , required : RequiredType ) OptionError ! void {
if ( seenArgs < argCount ) {
return OptionError . MissingArgument ;
} else if ( seenArgs > argCount ) {
return OptionError . ExtraArguments ;
}
2022-11-26 20:29:23 -08:00
describeError ( required ) ;
2022-11-20 12:54:26 -08:00
inline for ( @typeInfo ( @TypeOf ( required ) ) . Struct . fields ) | field | {
if ( @field ( required , field . name ) = = false ) {
return OptionError . MissingOption ;
}
}
}
2022-11-26 20:29:23 -08:00
pub fn describeError ( required : RequiredType ) void {
inline for ( @typeInfo ( @TypeOf ( required ) ) . Struct . fields ) | field | {
if ( @field ( required , field . name ) = = false ) {
std . debug . print ( " missing {s} \n " , . { field . name } ) ;
}
}
}
fn attachSubcommands ( _ : @This ( ) , alloc : std . mem . Allocator ) ! void {
2022-11-20 12:54:26 -08:00
if ( data . subcommands = = null ) {
2022-11-27 01:31:20 -08:00
data . subcommands = std . ArrayList ( * params . CommandData ) . init ( alloc ) ;
2022-11-20 12:54:26 -08:00
}
inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
2022-11-20 12:54:26 -08:00
. Command = > {
2022-11-26 20:29:23 -08:00
try data . subcommands . ? . append ( & @TypeOf ( param ) . data ) ;
2022-11-20 12:54:26 -08:00
} ,
else = > continue ,
}
}
}
2023-03-20 23:02:13 -07:00
fn scryTruthiness ( input : [ ] const u8 ) bool {
2022-11-20 12:54:26 -08:00
// empty string is falsy.
if ( input . len = = 0 ) return false ;
if ( input . len < = 5 ) {
2023-03-20 23:02:13 -07:00
var lowerBuf : [ 5 ] u8 = undefined ;
const comp = std . ascii . lowerString ( & lowerBuf , input ) ;
2022-11-20 12:54:26 -08:00
inline for ( [ _ ] [ ] const u8 { " false " , " no " , " 0 " } ) | candidate | {
if ( std . mem . eql ( u8 , comp , candidate ) ) {
return false ;
}
}
}
// TODO: actually try float conversion on input string? This seems
// really silly to me, in the context of the shell, but for example
// MY_VAR=0 evaluates to false but MY_VAR=0.0 evaluates to true. And
// if we accept multiple representations of zero, a whole can of
// worms gets opened. Should 0x0 be falsy? 0o0? That's a lot of
// goofy edge cases.
// any nonempty value is considered to be truthy.
return true ;
}
2022-11-26 20:29:23 -08:00
fn extractEnvVars (
alloc : std . mem . Allocator ,
result : * ResultType ,
required : * RequiredType ,
context : UserContext ,
) ! void {
2022-11-20 12:54:26 -08:00
var env : std . process . EnvMap = try std . process . getEnvMap ( alloc ) ;
defer env . deinit ( ) ;
inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
const ParamType = @TypeOf ( param ) ;
switch ( ParamType . brand ) {
2022-11-20 12:54:26 -08:00
. Option = > {
if ( param . envVar ) | want | {
if ( env . get ( want ) ) | value | {
2022-11-26 20:29:23 -08:00
if ( param . required ( ) ) {
2022-11-20 12:54:26 -08:00
@field ( required , param . name ) = true ;
}
2022-11-26 20:29:23 -08:00
@field ( result , param . name ) = try param . handler . ? ( context , value ) ;
2022-11-20 12:54:26 -08:00
}
}
} ,
. Flag = > {
if ( param . envVar ) | want | {
if ( env . get ( want ) ) | value | {
2023-03-20 23:02:13 -07:00
@field ( result , param . name ) = scryTruthiness ( value ) ;
2022-11-20 12:54:26 -08:00
}
}
} ,
. Argument , . Command = > continue ,
}
}
}
inline fn createCommandresult ( ) ResultType {
var result : ResultType = undefined ;
inline for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
switch ( @TypeOf ( param ) . brand ) {
2022-11-20 12:54:26 -08:00
. Command = > continue ,
else = > if ( param . hideResult = = false ) {
2022-11-26 20:29:23 -08:00
@field ( result , param . name ) = param . default orelse continue ;
2022-11-20 12:54:26 -08:00
} ,
}
}
return result ;
}
} ;
}
2022-11-26 20:29:23 -08:00
pub fn CommandResult ( comptime spec : anytype , comptime UserContext : type ) type {
2022-11-20 12:54:26 -08:00
comptime {
// not sure how to do this without iterating twice, so let's iterate
// twice
var outsize = 0 ;
for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
const ParamType = @TypeOf ( param ) ;
if ( ParamType . ContextType ! = UserContext ) {
@compileError ( " param \" " + + param . name + + " \" has wrong context type (wanted: " + + @typeName ( UserContext ) + + " , got: " + + @typeName ( ParamType . ContextType ) + + " ) " ) ;
}
switch ( ParamType . brand ) {
. Argument , . Option = > {
if ( param . handler = = null ) {
@compileError ( " param \" " + + param . name + + " \" does not have a handler " ) ;
}
} ,
else = > { } ,
}
switch ( ParamType . brand ) {
2022-11-20 12:54:26 -08:00
. Command = > continue ,
2022-11-26 20:29:23 -08:00
else = > {
if ( param . hideResult = = false ) {
outsize + = 1 ;
}
2022-11-20 12:54:26 -08:00
} ,
}
}
var fields : [ outsize ] StructField = undefined ;
var idx = 0 ;
for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
const ParamType = @TypeOf ( param ) ;
switch ( ParamType . brand ) {
2022-11-20 12:54:26 -08:00
. Command = > continue ,
else = > if ( param . hideResult = = true ) continue ,
}
2022-11-26 20:29:23 -08:00
const FieldType = ParamType . ResultType ;
2022-11-20 12:54:26 -08:00
fields [ idx ] = . {
. name = param . name ,
2023-03-20 23:02:13 -07:00
. type = FieldType ,
2022-11-26 20:29:23 -08:00
. default_value = @ptrCast ( ? * const anyopaque , & param . default ) ,
2022-11-20 12:54:26 -08:00
. is_comptime = false ,
2022-11-26 20:29:23 -08:00
. alignment = @alignOf ( FieldType ) ,
2022-11-20 12:54:26 -08:00
} ;
idx + = 1 ;
}
return @Type ( . { . Struct = . {
. layout = . Auto ,
. fields = & fields ,
. decls = & . { } ,
. is_tuple = false ,
} } ) ;
}
}
fn RequiredTracker ( comptime spec : anytype ) type {
comptime {
// not sure how to do this without iterating twice, so let's iterate
// twice
var outsize = 0 ;
for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
const ParamType = @TypeOf ( param ) ;
switch ( ParamType . brand ) {
// flags are always optional, and commands don't map into the
// output type.
. Flag , . Command = > continue ,
. Argument , . Option = > if ( param . required ( ) ) {
// if mayBeOptional is false, then the argument/option is
// required. Otherwise, we have to check if a default has
// been provided.
outsize + = 1 ;
2022-11-20 12:54:26 -08:00
} ,
}
}
var fields : [ outsize ] StructField = undefined ;
var idx = 0 ;
for ( spec ) | param | {
2022-11-26 20:29:23 -08:00
const ParamType = @TypeOf ( param ) ;
switch ( ParamType . brand ) {
. Flag , . Command = > continue ,
. Argument , . Option = > if ( param . required ( ) ) {
2022-11-20 12:54:26 -08:00
fields [ idx ] = . {
. name = param . name ,
2023-03-20 23:02:13 -07:00
. type = bool ,
2022-11-20 12:54:26 -08:00
. default_value = & false ,
. is_comptime = false ,
. alignment = @alignOf ( bool ) ,
} ;
idx + = 1 ;
} ,
}
}
return @Type ( . { . Struct = . {
. layout = . Auto ,
. fields = & fields ,
. decls = & . { } ,
. is_tuple = false ,
} } ) ;
}
}
2022-11-26 20:29:23 -08:00
test {
_ = meta ;
}