8000 support option type by Roger-luo · Pull Request #222 · comonicon/Comonicon.jl · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

support option type #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 22, 2022
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ Manifest.toml

/node_modules/
package-lock.json
config.toml

*.cmd
/bin/
/test/bin/
/test/completions/
/test/config.toml

build
*.tar.gz
Expand Down
144 changes: 122 additions & 22 deletions src/codegen/julia.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module JuliaExpr
using ..AST
using ..Comonicon: CommandException, CommandExit, _sprint
using ExproniconLite
using Configurations: Configurations

function help_str(x; color = true, width::Int)
_sprint(print_cmd, x; color, displaysize = (24, width))
Expand Down Expand Up @@ -163,11 +164,14 @@ function emit_body(cmd::LeafCommand, ctx::EmitContext, ptr::Int = 1)
end
end

"""
emit dash parsing code (contains `--`).
"""
function emit_dash_body(cmd::LeafCommand, ctx::EmitContext, idx::Symbol, ptr::Int = 1)
@gensym token token_ptr args kwargs

quote # parse option/flag
$kwargs = []
$kwargs = Dict{Symbol, Any}()
$token_ptr = $ptr
while $token_ptr ≤ $idx - 1
$token = ARGS[$token_ptr]
Expand All @@ -184,12 +188,15 @@ function emit_dash_body(cmd::LeafCommand, ctx::EmitContext, idx::Symbol, ptr::In
end
end

"""
emit the normal parsing code (do not contain dash `--`)
"""
function emit_norm_body(cmd::LeafCommand, ctx::EmitContext, ptr::Int = 1)
@gensym token_ptr args kwargs token

quote
$args = []
$kwargs = []
$kwargs = Dict{Symbol, Any}()
sizehint!($args, $(cmd.nrequire))
$token_ptr = $ptr
while $token_ptr ≤ length(ARGS)
Expand Down Expand Up @@ -239,8 +246,8 @@ function emit_leaf_call(cmd::LeafCommand, ctx::EmitContext, args::Symbol, kwargs
)
end

# check if all required options are filled
if !isempty(cmd.options)
# check if all required options are filled
for each in values(cmd.options)
each.require || continue
err_hint = "--" * each.name
Expand All @@ -253,12 +260,23 @@ function emit_leaf_call(cmd::LeafCommand, ctx::EmitContext, args::Symbol, kwargs

@gensym idx
push!(ret.args, quote
$idx = findfirst(x -> x.first === $(QuoteNode(each.sym)), $kwargs)
if $idx === nothing
if !($(QuoteNode(each.sym)) in Base.keys($kwargs))
$(emit_error(cmd, ctx, err_hint))
end
end)
end

# convert dict to option types
# NOTE: since the types must be an option type
# thus the corresponding program depends on Configurations
# already, we are safe to depend on it in the generated code
for option in values(cmd.options)
Configurations.is_option(option.type) || continue
push!(ret.args, quote
$kwargs[$(QuoteNode(option.sym))] =
$Configurations.from_dict($(option.type), $kwargs[$(QuoteNode(option.sym))])
end)
end
end

call = Expr(:call, cmd.fn)
Expand Down Expand Up @@ -348,37 +366,33 @@ function emit_kwarg(cmd::LeafCommand, ctx::EmitContext, token::Symbol, kwargs::S
@gensym sym key value

ifelse = JLIfElse()
# short flag
ifelse[:(length($token) == 2)] = quote
$key = $token[2:2]
$(emit_short_flag(cmd, ctx, token, sym, key, value))
end

# long option/flag
ifelse[:(startswith($token, "--"))] = quote
$key = lstrip(split($token, '=')[1], '-')
$(emit_long_option_or_flag(cmd, ctx, token, sym, key, value, token_ptr))
$key = lstrip(split($token, '='; limit=2)[1], '-')
$key = split($key, '.'; limit=2)[1]
$(emit_long_option_or_flag(cmd, ctx, token, kwargs, sym, key, value, token_ptr))
end

# short option
ifelse.otherwise = quote
$key = $token[2:2]
$(emit_short_option(cmd, ctx, token, sym, key, value, token_ptr))
$(emit_short_flag_or_option(cmd, ctx, token, sym, key, value, token_ptr))
end

return quote
$(codegen_ast(ifelse))
push!($kwargs, $sym => $value)
$kwargs[$sym] = $value
end
end

function emit_short_flag(
function emit_short_flag_or_option(
cmd::LeafCommand,
ctx::EmitContext,
token::Symbol,
sym::Symbol,
key::Symbol,
value::Symbol,
token_ptr,
)
ifelse = JLIfElse()
for (_, flag) in cmd.flags
Expand All @@ -390,7 +404,7 @@ function emit_short_flag(
end
end
end
ifelse.otherwise = emit_error(cmd, ctx, :("cannot find flag: $($token)"))
ifelse.otherwise = emit_short_option(cmd, ctx, token, sym, key, value, token_ptr)
return codegen_ast(ifelse)
end

Expand All @@ -409,7 +423,7 @@ function emit_short_option(
name = option.name[1:1]
ifelse[:($key == $name)] = quote
if occursin('=', $token)
_, $value = split($token, '=')
_, $value = split($token, '='; limit=2)
elseif length($token) == 2 # read next
$token_ptr += 1
if $token_ptr > length(ARGS)
Expand All @@ -424,19 +438,23 @@ function emit_short_option(
end
end
end
ifelse.otherwise = emit_error(cmd, ctx, :("cannot find $($token)"))
ifelse.otherwise = emit_error(
cmd, ctx, :("cannot find short flag or option: $($token)")
)
return codegen_ast(ifelse)
end

function emit_long_option_or_flag(
cmd::LeafCommand,
ctx::EmitContext,
token::Symbol,
kwargs::Symbol,
sym::Symbol,
key::Symbol,
value::Symbol,
token_ptr,
)

ifelse = JLIfElse()
for (name, flag) in cmd.flags
ifelse[:($key == $name)] = quote
Expand All @@ -448,7 +466,7 @@ function emit_long_option_or_flag(
for (name, option) in cmd.options
ifelse[:($key == $name)] = quote
$sym = $(QuoteNode(option.sym))
$(emit_option(option, ctx, token, value, token_ptr))
$(emit_option(option, ctx, token, kwargs, value, token_ptr))
end
end

Expand All @@ -461,10 +479,14 @@ function emit_parse_value(cmd, ctx::EmitContext, type, value)
return value
elseif type === String # we need to convert SubString to String
return :(String($value))
elseif Configurations.is_option(type)
return quote
$Configurations.from_toml($type, $value)
end
else
@gensym ret
return quote
$ret = tryparse($type, $value)
$ret = tryparse($type, $value) # this may calls the runtime in Comonicon.Arg
if isnothing($ret)
$(emit_error(cmd, ctx, "expect value of type: $(type)"))
end
Expand All @@ -477,12 +499,17 @@ function emit_option(
option::Option,
ctx::EmitContext,
token::Symbol,
kwargs::Symbol,
value::Symbol,
token_ptr::Symbol,
)
if Configurations.is_option(option.type)
return emit_option_with_option_type(option, ctx, token, kwargs, value, token_ptr)
end

return quote
if occursin('=', $token)
_, $value = split($token, '=')
_, $value = split($token, '='; limit=2)
else # read next token
$token_ptr += 1
if $token_ptr > length(ARGS)
Expand All @@ -494,4 +521,77 @@ function emit_option(
end
end

function emit_option_with_option_type(
option::Option,
ctx::EmitContext,
token::Symbol,
kwargs::Symbol,
value::Symbol,
token_ptr::Symbol,
)

@gensym key field_value fields
assign_field_to_dict = foreach_leaf_field(option.type, value, fields) do dict_ex, type
quote
$field_value = $(emit_parse_value(option, ctx, type, field_value))
$dict_ex = $field_value
end
end
assign_field_to_dict.otherwise = emit_error(option, ctx, :("cannot find field $($fields)"))

return quote
if occursin('=', $token)
$key, $field_value = split($token, '='; limit=2)
if occursin('.', $key)
_, $fields = split($key, '.'; limit=2)
else
$fields = nothing
end
else # read next token
if occursin('.', $token)
_, $fields = split($token, '.'; limit=2)
else
$fields = nothing
end

$token_ptr += 1
if $token_ptr > length(ARGS)
$(emit_error(option, ctx, "expect a value"))
end
$field_value = ARGS[$token_ptr]
end

if isnothing($fields) # path to the config file
$value = $(emit_parse_value(option, ctx, option.type, field_value))
else
$value = get(Dict{String, Any}, $kwargs, $(QuoteNode(option.sym)))
$(codegen_ast(assign_field_to_dict))
end
end
end

function foreach_leaf_field!(f, jl::JLIfElse, ::Type{T}, dict, fields::Symbol, prefix::String="") where T
Configurations.foreach_keywords(T) do name, type
key = isempty(prefix) ? string(name) : join((prefix, name), '.')
dict_ex = :($dict[$(string(name))])
if Configurations.is_option(type)
foreach_leaf_field!(jl, type, dict_ex, fields, key) do dict_ex, type
quote
Base.get!(Dict{String, Any}, $dict, $(string(name)))
$(f(dict_ex, type))
end
end
else
jl[:($fields == $key)] = quote
$(f(dict_ex, type))
end
end
end
return jl
end

function foreach_leaf_field(f, ::Type{T}, dict::Symbol, fields::Symbol) where T
return foreach_leaf_field!(f, JLIfElse(), T, dict, fields)
end

end # JuliaExpr
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[deps]
ComoniconTestUtils = "53b67e40-53a7-4335-973f-e3e901fe4865"
Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d"
ExproniconLite = "55351af7-c7e9-48d6-89ff-24e801d99491"
Faker = "0efc519c-db33-5916-ab87-703215c3906f"
FromFile = "ff7dd447-1dcb-4ce3-b8ac-22a812192de7"
Expand Down
3 changes: 2 additions & 1 deletion test/builder/install.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ end
end

@testset "detect_rcfile" begin
answer = joinpath((haskey(ENV, "ZDOTDIR") ? ENV["ZDOTDIR"] : homedir()), ".zshrc")
withenv("SHELL" => "zsh") do
@test detect_rcfile("zsh") == joinpath(homedir(), ".zshrc")
@test detect_rcfile("zsh") == answer
end

withenv("SHELL" => "zsh", "ZDOTDIR" => "zsh_dir") do
Expand Down
1 change: 1 addition & 0 deletions test/codegen/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ using Test
include("julia/exception.jl")
include("julia/plugin.jl")
include("julia/print.jl")
include("julia/config.jl")
end

@testset "ZSHCompletions.emit" begin
Expand Down
35 changes: 35 additions & 0 deletions test/codegen/julia/config.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
module TestConfigOption

using Test
using Comonicon
using Configurations

@option struct OptionA
a::Int
b::Int
end

@option struct OptionB
option::OptionA
c::Int
end

"""
# Options

- `-c, --config <path/to/option/or/specific field>`: config.
"""
@main function run(;config::OptionB)
@test config == OptionB(OptionA(1, 1), 1)
end

@testset "config options" begin
TestConfigOption.command_main(["--config.c=1", "--config.option.a=1", "--config.option.b=1"])

opt = TestConfigOption.OptionB(TestConfigOption.OptionA(1, 1), 1)
to_toml("config.toml", opt)
TestConfigOption.command_main(["--config", "config.toml"])
TestConfigOption.command_main(["-c", "config.toml"])
end

end
8 changes: 4 additions & 4 deletions test/codegen/julia/dash.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ using Comonicon.JuliaExpr: emit, emit_body, emit_norm_body, emit_dash_body
using Test

const test_args = Ref{Vector{Any}}()
const test_kwargs = Ref{Vector{Any}}()
const test_kwargs = Ref{Dict{Symbol, Any}}()

function foo(a, b = 2, c...; kwargs...)
test_args[] = [a, b, c...]
test_kwargs[] = [kwargs...]
test_kwargs[] = Dict{Symbol, Any}(kwargs)
end

cmd = Entry(;
Expand Down Expand Up @@ -39,11 +39,11 @@ eval(emit(cmd))
@testset "test leaf optional argument" begin
@test command_main(["3", "2", "5", "6", "7", "--option-a=2", "--option-b", "2.3"]) == 0
@test test_args[] == Any[3, 2, 5, 6, 7]
@test test_kwargs[] == [:option_a => 2, :option_b => 2.3]
@test test_kwargs[] == Dict{Symbol, Any}(:option_a => 2, :option_b => 2.3)

@test command_main(["--option-a=2", "--option-b=2.3", "--", "3", "2"]) == 0
@test test_args[] == Any[3, 2]
@test test_kwargs[] == [:option_a => 2, :option_b => 2.3]
@test test_kwargs[] == Dict{Symbol, Any}(:option_a => 2, :option_b => 2.3)

@test command_main(["3", "2"]) == 0
@test test_args[] == Any[3, 2]
Expand Down
Loading
0