mirror of
https://github.com/odin-lang/Odin.git
synced 2025-12-28 17:04:34 +00:00
294 lines
8.3 KiB
Odin
294 lines
8.3 KiB
Odin
package flags
|
|
|
|
import "base:runtime"
|
|
import "core:fmt"
|
|
import "core:io"
|
|
import "core:reflect"
|
|
import "core:slice"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
|
|
/*
|
|
Write out the documentation for the command-line arguments to a stream.
|
|
|
|
Inputs:
|
|
- out: The stream to write to.
|
|
- data_type: The typeid of the data structure to describe.
|
|
- program: The name of the program, usually the first argument to `os.args`.
|
|
- style: The argument parsing style, required to show flags in the proper style.
|
|
*/
|
|
@(optimization_mode="favor_size")
|
|
write_usage :: proc(out: io.Writer, data_type: typeid, program: string = "", style: Parsing_Style = .Odin) {
|
|
// All flags get their tags parsed so they can be reasoned about later.
|
|
Flag :: struct {
|
|
name: string,
|
|
usage: string,
|
|
type_description: string,
|
|
full_length: int,
|
|
pos: int,
|
|
required_min, required_max: int,
|
|
is_positional: bool,
|
|
is_required: bool,
|
|
is_boolean: bool,
|
|
is_manifold: bool,
|
|
manifold_length: int,
|
|
}
|
|
|
|
//
|
|
// POSITIONAL+REQUIRED, POSITIONAL, REQUIRED, NON_REQUIRED+NON_POSITIONAL, ...
|
|
//
|
|
sort_flags :: proc(i, j: Flag) -> slice.Ordering {
|
|
// `overflow` goes to the end.
|
|
if i.name == INTERNAL_OVERFLOW_FLAG {
|
|
return .Greater
|
|
} else if j.name == INTERNAL_OVERFLOW_FLAG {
|
|
return .Less
|
|
}
|
|
|
|
// Handle positionals.
|
|
if i.is_positional {
|
|
if j.is_positional {
|
|
return slice.cmp(i.pos, j.pos)
|
|
} else {
|
|
return .Less
|
|
}
|
|
} else {
|
|
if j.is_positional {
|
|
return .Greater
|
|
}
|
|
}
|
|
|
|
// Then required flags.
|
|
if i.is_required {
|
|
if !j.is_required {
|
|
return .Less
|
|
}
|
|
} else if j.is_required {
|
|
return .Greater
|
|
}
|
|
|
|
// Finally, sort by name.
|
|
return slice.cmp(i.name, j.name)
|
|
}
|
|
|
|
describe_array_requirements :: proc(flag: Flag) -> (spec: string) {
|
|
if flag.is_required {
|
|
if flag.required_min == flag.required_max - 1 {
|
|
spec = fmt.tprintf(", exactly %i", flag.required_min)
|
|
} else if flag.required_min > 0 && flag.required_max == max(int) {
|
|
spec = fmt.tprintf(", at least %i", flag.required_min)
|
|
} else if flag.required_min == 0 && flag.required_max > 1 {
|
|
spec = fmt.tprintf(", at most %i", flag.required_max - 1)
|
|
} else if flag.required_min > 0 && flag.required_max > 1 {
|
|
spec = fmt.tprintf(", between %i and %i", flag.required_min, flag.required_max - 1)
|
|
} else {
|
|
spec = ", required"
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
|
|
flag_prefix, flag_assignment: string = ---, ---
|
|
switch style {
|
|
case .Odin: flag_prefix = "-"; flag_assignment = ":"
|
|
case .Unix: flag_prefix = "--"; flag_assignment = " "
|
|
}
|
|
|
|
visible_flags: [dynamic]Flag
|
|
defer delete(visible_flags)
|
|
|
|
longest_flag_length: int
|
|
|
|
for field in reflect.struct_fields_zipped(data_type) {
|
|
flag: Flag
|
|
|
|
if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok {
|
|
if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden {
|
|
// Hidden flags stay hidden.
|
|
continue
|
|
}
|
|
if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos {
|
|
flag.is_positional = true
|
|
if pos, parse_ok := strconv.parse_u64_of_base(pos_str, 10); parse_ok {
|
|
flag.pos = cast(int)pos
|
|
}
|
|
}
|
|
if requirement, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required {
|
|
flag.is_required = true
|
|
flag.required_min, flag.required_max, _ = parse_requirements(requirement)
|
|
}
|
|
if length_str, is_manifold := get_struct_subtag(args_tag, SUBTAG_MANIFOLD); is_manifold {
|
|
flag.is_manifold = true
|
|
if length, parse_ok := strconv.parse_u64_of_base(length_str, 10); parse_ok {
|
|
flag.manifold_length = cast(int)length
|
|
}
|
|
}
|
|
}
|
|
|
|
flag.name = get_field_name(field)
|
|
flag.is_boolean = reflect.is_boolean(field.type)
|
|
|
|
if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok {
|
|
flag.usage = usage
|
|
} else {
|
|
flag.usage = UNDOCUMENTED_FLAG
|
|
}
|
|
|
|
#partial switch specific_type_info in field.type.variant {
|
|
case runtime.Type_Info_Map:
|
|
flag.type_description = fmt.tprintf("<%v>=<%v>%s",
|
|
specific_type_info.key.id,
|
|
specific_type_info.value.id,
|
|
", required" if flag.is_required else "")
|
|
|
|
case runtime.Type_Info_Dynamic_Array:
|
|
requirement_spec := describe_array_requirements(flag)
|
|
|
|
if flag.is_manifold || flag.name == INTERNAL_OVERFLOW_FLAG {
|
|
if flag.manifold_length == 0 {
|
|
flag.type_description = fmt.tprintf("<%v, ...>%s",
|
|
specific_type_info.elem.id,
|
|
requirement_spec)
|
|
} else {
|
|
flag.type_description = fmt.tprintf("<%v, %i at once>%s",
|
|
specific_type_info.elem.id,
|
|
flag.manifold_length,
|
|
requirement_spec)
|
|
}
|
|
} else {
|
|
flag.type_description = fmt.tprintf("<%v>%s", specific_type_info.elem.id,
|
|
requirement_spec if len(requirement_spec) > 0 else ", multiple")
|
|
}
|
|
|
|
case:
|
|
if flag.is_boolean {
|
|
/*
|
|
if flag.is_required {
|
|
flag.type_description = ", required"
|
|
}
|
|
*/
|
|
} else {
|
|
flag.type_description = fmt.tprintf("<%v>%s",
|
|
field.type.id,
|
|
", required" if flag.is_required else "")
|
|
}
|
|
}
|
|
|
|
if flag.name == INTERNAL_OVERFLOW_FLAG {
|
|
flag.full_length = len(flag.type_description)
|
|
} else if flag.is_boolean {
|
|
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag.type_description)
|
|
} else {
|
|
flag.full_length = len(flag_prefix) + len(flag.name) + len(flag_assignment) + len(flag.type_description)
|
|
}
|
|
|
|
longest_flag_length = max(longest_flag_length, flag.full_length)
|
|
|
|
append(&visible_flags, flag)
|
|
}
|
|
|
|
slice.sort_by_cmp(visible_flags[:], sort_flags)
|
|
|
|
// All the flags have been figured out now.
|
|
|
|
if len(program) > 0 {
|
|
keep_it_short := len(visible_flags) >= ONE_LINE_FLAG_CUTOFF_COUNT
|
|
|
|
strings.write_string(&builder, "Usage:\n\t")
|
|
strings.write_string(&builder, program)
|
|
|
|
for flag in visible_flags {
|
|
if keep_it_short && !(flag.is_required || flag.is_positional || flag.name == INTERNAL_OVERFLOW_FLAG) {
|
|
continue
|
|
}
|
|
|
|
strings.write_byte(&builder, ' ')
|
|
|
|
if flag.name == INTERNAL_OVERFLOW_FLAG {
|
|
strings.write_string(&builder, "...")
|
|
continue
|
|
}
|
|
|
|
if !flag.is_required { strings.write_byte(&builder, '[') }
|
|
if !flag.is_positional { strings.write_string(&builder, flag_prefix) }
|
|
strings.write_string(&builder, flag.name)
|
|
if !flag.is_required { strings.write_byte(&builder, ']') }
|
|
}
|
|
|
|
strings.write_byte(&builder, '\n')
|
|
}
|
|
|
|
if len(visible_flags) == 0 {
|
|
// No visible flags. An unusual situation, but prevent any extra work.
|
|
fmt.wprint(out, strings.to_string(builder))
|
|
return
|
|
}
|
|
|
|
strings.write_string(&builder, "Flags:\n")
|
|
|
|
// Divide the positional/required arguments and the non-required arguments.
|
|
divider_index := -1
|
|
for flag, i in visible_flags {
|
|
if !flag.is_positional && !flag.is_required {
|
|
divider_index = i
|
|
break
|
|
}
|
|
}
|
|
if divider_index == 0 {
|
|
divider_index = -1
|
|
}
|
|
|
|
for flag, i in visible_flags {
|
|
if i == divider_index {
|
|
SPACING :: 2 // Number of spaces before the '|' from below.
|
|
strings.write_byte(&builder, '\t')
|
|
spacing := strings.repeat(" ", SPACING + longest_flag_length, context.temp_allocator)
|
|
strings.write_string(&builder, spacing)
|
|
strings.write_string(&builder, "|\n")
|
|
}
|
|
|
|
strings.write_byte(&builder, '\t')
|
|
|
|
if flag.name == INTERNAL_OVERFLOW_FLAG {
|
|
strings.write_string(&builder, flag.type_description)
|
|
} else {
|
|
strings.write_string(&builder, flag_prefix)
|
|
strings.write_string(&builder, flag.name)
|
|
if !flag.is_boolean {
|
|
strings.write_string(&builder, flag_assignment)
|
|
}
|
|
strings.write_string(&builder, flag.type_description)
|
|
}
|
|
|
|
if strings.contains_rune(flag.usage, '\n') {
|
|
// Multi-line usage documentation. Let's make it look nice.
|
|
usage_builder := strings.builder_make(context.temp_allocator)
|
|
|
|
strings.write_byte(&usage_builder, '\n')
|
|
iter := strings.trim_space(flag.usage)
|
|
for line in strings.split_lines_iterator(&iter) {
|
|
strings.write_string(&usage_builder, "\t\t")
|
|
strings.write_string(&usage_builder, strings.trim_left_space(line))
|
|
strings.write_byte(&usage_builder, '\n')
|
|
}
|
|
|
|
strings.write_string(&builder, strings.to_string(usage_builder))
|
|
} else {
|
|
// Single-line usage documentation.
|
|
spacing := strings.repeat(" ",
|
|
(longest_flag_length) - flag.full_length,
|
|
context.temp_allocator)
|
|
|
|
strings.write_string(&builder, spacing)
|
|
strings.write_string(&builder, " | ")
|
|
strings.write_string(&builder, flag.usage)
|
|
strings.write_byte(&builder, '\n')
|
|
}
|
|
}
|
|
|
|
fmt.wprint(out, strings.to_string(builder))
|
|
}
|