From 3703a776a58178d345812142a9694c8c17913f13 Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 23 Mar 2023 15:37:22 -0400 Subject: [PATCH] added arg crate source for potential argument parsing --- src/env/derive/mod.rs | 799 ++++++++++++++++++++++++++++++++++++++++ src/env/derive/utils.rs | 76 ++++ src/env/mod.rs | 157 ++++++++ src/env/split.rs | 88 +++++ tests/args.rs | 222 +++++++++++ 5 files changed, 1342 insertions(+) create mode 100644 src/env/derive/mod.rs create mode 100644 src/env/derive/utils.rs create mode 100644 src/env/mod.rs create mode 100644 src/env/split.rs create mode 100644 tests/args.rs diff --git a/src/env/derive/mod.rs b/src/env/derive/mod.rs new file mode 100644 index 0000000..28876a4 --- /dev/null +++ b/src/env/derive/mod.rs @@ -0,0 +1,799 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of SPD. + * + * SPD is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * SPD is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SPD. If not, see . + * + * This file incorporates work covered by the following copyright and permission + * notice: + * + * Copyright 2017 Douman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +//! Command line argument parser derive + +extern crate alloc; +extern crate proc_macro; + +mod utils; +use utils::*; + +use crate::println; + +use proc_macro::TokenStream; +use quote::quote; + +use alloc::{ + string::String, + borrow::ToOwned, + vec, + vec::*, +}; +use core::fmt::Write; + +struct Argument { + field_name: String, + name: String, + desc: String, + required: bool, + is_optional: bool, + default: Option, +} + +#[derive(PartialEq, Eq, Debug)] +enum OptValueType { + Help, + Bool, + Value, + MultiValue, +} + +struct Opt { + arg: Argument, + long: String, + short: Option, + typ: OptValueType, +} + +struct Command { + variant_name: String, + command_name: String, + desc: String, +} + +const FROM_FN: &str = "core::str::FromStr::from_str"; +const TAB: &str = " "; +const PARSER_TRAIT: &str = "arg::Args"; +const DEFAULT_INIT: &str = "Default::default()"; +const INVALID_ARG_TYPE_STRING: &str = "Attribute accepts only str"; +const INVALID_REQUIRED_BOOL: &str = "Attribute required cannot be applied to bool switch"; +const UNKNOWN_ARG_ATTR: &str = "Unknown attribute is used"; +const ARG_INVALID_CHARS: &[char] = &[' ', '\t']; +const ARG_NAME_SPACE_ERROR: &str = "Name contains space character"; + +fn parse_segment(segment: &syn::PathSegment) -> OptValueType { + if segment.ident == "bool" { + OptValueType::Bool + } else if segment.ident == "Vec" { + OptValueType::MultiValue + } else { + OptValueType::Value + } +} + +fn from_enum(ast: &syn::DeriveInput, payload: &syn::DataEnum) -> TokenStream { + let mut about_prog = String::new(); + for attr in ast.attrs.iter() { + let attr = match attr.parse_meta() { + Ok(attr) => attr, + Err(error) => { + return syn::Error::new_spanned(attr, format!("cannot parse attribute: {error}")).to_compile_error().into() + } + }; + + match attr { + syn::Meta::NameValue(value) => if value.path.is_ident("doc") { + if let syn::Lit::Str(ref text) = value.lit { + about_prog.push_str(&text.value()); + about_prog.push_str("\n"); + } + } else { + }, + _ => (), + } + } + about_prog.pop(); + + let mut commands = Vec::new(); + for variant in payload.variants.iter() { + let mut desc = String::new(); + let variant_name = variant.ident.to_string(); + if variant_name.is_empty() { + return syn::Error::new_spanned(&variant.ident, "Oi, mate, You cannot have enum variant without name").to_compile_error().into() + } + let command_name = to_hyphenated_lower_case(&variant_name); + if command_name.eq_ignore_ascii_case("help") { + return syn::Error::new_spanned(&variant.ident, "Oi, mate, You cannot use variant 'Help'").to_compile_error().into() + } + + for attr in variant.attrs.iter() { + let attr = match attr.parse_meta() { + Ok(attr) => attr, + Err(error) => { + return syn::Error::new_spanned(attr, format!("cannot parse attribute: {error}")).to_compile_error().into() + } + }; + + match attr { + syn::Meta::NameValue(value) => if value.path.is_ident("doc") { + if let syn::Lit::Str(ref text) = value.lit { + desc.push_str(&text.value()); + desc.push_str(" "); + } + }, + _ => continue + } + } + + let field = match &variant.fields { + syn::Fields::Unit => return syn::Error::new_spanned(&variant.fields, "Unit variant cannot be used").to_compile_error().into(), + syn::Fields::Named(_) => return syn::Error::new_spanned(&variant.fields, "I'm too lazy to support named variant").to_compile_error().into(), + syn::Fields::Unnamed(fields) => { + if fields.unnamed.empty_or_trailing() { + return syn::Error::new_spanned(&fields, "MUST specify single field").to_compile_error().into(); + } else if fields.unnamed.len() > 1 { + return syn::Error::new_spanned(&fields, "MUST not specify more than 1 field").to_compile_error().into(); + } else { + fields.unnamed.first().unwrap() + } + }, + }; + + match &field.ty { + syn::Type::Path(ref ty) => { + let ty = ty.path.segments.last().expect("To have at least one segment"); + if ty.ident == "Option" { + return syn::Error::new_spanned(&ty, "Command cannot be optional").to_compile_error().into() + } else { + match parse_segment(ty) { + OptValueType::Bool => return syn::Error::new_spanned(&ty, "Command value cannot be boolean").to_compile_error().into(), + OptValueType::MultiValue => return syn::Error::new_spanned(&ty, "Command value Vec<_>").to_compile_error().into(), + _ => (), + } + } + }, + ty => { + return syn::Error::new_spanned(&ty, "Expected simple ident or path").to_compile_error().into() + } + } + + commands.push(Command { + command_name, + variant_name, + desc + }) + } + + if commands.is_empty() { + return syn::Error::new_spanned(&ast, "Enum must have at least one variant").to_compile_error().into() + } + + let (impl_gen, type_gen, where_clause) = ast.generics.split_for_impl(); + + let help_msg = { + use std::io::Write; + use tabwriter::TabWriter; + + let mut tw = TabWriter::new(vec![]); + + let _ = write!(tw, "COMMANDS:\n"); + for command in commands.iter() { + let _ = write!(tw, "\t{}\t{}\n", command.command_name, command.desc); + } + + let _ = tw.flush(); + + String::from_utf8(tw.into_inner().unwrap()).unwrap() + }; + + let mut result = String::new(); + let _ = writeln!(result, "{} {} for {}{} {{", quote!(impl #impl_gen), PARSER_TRAIT, ast.ident, quote!(#type_gen #where_clause)); + + let _ = writeln!(result, "{}const HELP: &'static str = \"{}\";", TAB, help_msg); + + //from_args START + let _ = writeln!(result, "{}fn from_args<'a, T: IntoIterator>(_args_: T) -> Result> {{", TAB); + + let _ = writeln!(result, "{0}{0}let mut _args_ = _args_.into_iter();\n", TAB); + + //args START + let _ = writeln!(result, "{0}{0}while let Some(_arg_) = _args_.next() {{", TAB); + + //help + let _ = writeln!(result, "{0}{0}{0}if _arg_.eq_ignore_ascii_case(\"help\") {{", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}return Err(arg::ParseKind::Top(arg::ParseError::HelpRequested(Self::HELP)));", TAB); + let _ = write!(result, "{0}{0}{0}}}", TAB); + + for command in commands.iter() { + //arg START + let _ = writeln!(result, " else if _arg_.eq_ignore_ascii_case(\"{}\") {{", command.command_name); + + let _ = writeln!(result, "{0}{0}{0}{0}match {1}::from_args(_args_) {{", TAB, PARSER_TRAIT); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Ok(res) => return Ok(Self::{1}(res)),", TAB, command.variant_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(arg::ParseKind::Top(error)) => return Err(arg::ParseKind::Sub(\"{1}\", error)),", TAB, command.command_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(arg::ParseKind::Sub(name, error)) => return Err(arg::ParseKind::Sub(name, error)),", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}}}", TAB); + + //arg END + let _ = write!(result, "{0}{0}{0}}}", TAB); + } + + //args END + let _ = writeln!(result, "\n{0}{0}}}", TAB); + + let _ = writeln!(result, "{0}{0}Err(arg::ParseKind::Top(arg::ParseError::RequiredArgMissing(\"command\")))", TAB); + + //from_args END + let _ = writeln!(result, "{}}}", TAB); + + let _ = writeln!(result, "}}"); + + if let Ok(val) = std::env::var("ARG_RS_PRINT_PARSER") { + match val.trim() { + "0" | "false" => (), + _ => println!("{result}"), + } + } + result.parse().expect("To parse generated code") +} + +fn from_struct(ast: &syn::DeriveInput, payload: &syn::DataStruct) -> TokenStream { + let mut about_prog = String::new(); + for attr in ast.attrs.iter() { + let attr = match attr.parse_meta() { + Ok(attr) => attr, + Err(error) => { + return syn::Error::new_spanned(attr, format!("cannot parse attribute: {error}")).to_compile_error().into() + } + }; + + match attr { + syn::Meta::NameValue(value) => if value.path.is_ident("doc") { + if let syn::Lit::Str(ref text) = value.lit { + about_prog.push_str(&text.value()); + about_prog.push_str("\n"); + } + } else { + }, + _ => (), + } + } + + about_prog.pop(); + + let mut options = Vec::new(); + let mut arguments = Vec::new(); + + options.push(Opt { + arg: Argument { + field_name: "_".to_owned(), + name: "help".to_owned(), + desc: "Prints this help information".to_owned(), + required: false, + is_optional: false, + default: None, + }, + short: Some("h".to_owned()), + long: "help".to_owned(), + typ: OptValueType::Help, + }); + + let mut sub_command = None; + let mut multi_argument = None; + + for field in payload.fields.iter() { + let field_name = field.ident.as_ref().unwrap().to_string(); + let name = field.ident.as_ref().unwrap().to_string().trim_matches(|ch| !char::is_alphanumeric(ch)).to_owned(); + let mut desc = String::new(); + let mut short = None; + let mut long = None; + let mut required = false; + let mut is_sub = false; + + let (is_optional, typ) = match field.ty { + syn::Type::Path(ref ty) => { + let ty = ty.path.segments.last().expect("To have at least one segment"); + + if ty.ident == "Option" { + let ty = match &ty.arguments { + syn::PathArguments::AngleBracketed(ref args) => match args.args.len() { + 0 => return syn::Error::new_spanned(&ty.ident, "Oi, mate, Option is without type arguments. Fix it").to_compile_error().into(), + 1 => match args.args.first().unwrap() { + syn::GenericArgument::Type(syn::Type::Path(ty)) => parse_segment(ty.path.segments.last().expect("To have at least one segment")), + _ => return syn::Error::new_spanned(&ty.ident, "Oi, mate, Option should have type argument, but got some other shite. Fix it").to_compile_error().into(), + }, + _ => return syn::Error::new_spanned(&ty.ident, "Oi, mate, Option has too many type arguments. Fix it").to_compile_error().into() + }, + syn::PathArguments::None => return syn::Error::new_spanned(&ty.ident, "Oi, mate, Option is without type arguments. Fix it").to_compile_error().into(), + syn::PathArguments::Parenthesized(_) => return syn::Error::new_spanned(&ty.ident, "Oi, mate, you got wrong brackets for your Option . Fix it").to_compile_error().into(), + }; + + (true, ty) + } else { + (false, parse_segment(ty)) + } + }, + _ => (false, OptValueType::Value), + }; + + if is_optional && typ == OptValueType::MultiValue { + return syn::Error::new_spanned(field, "Option> makes no sense. Just use plain Vec<_>").to_compile_error().into(); + } + + let mut default = None; + + for attr in field.attrs.iter() { + let attr = match attr.parse_meta() { + Ok(attr) => attr, + Err(error) => { + return syn::Error::new_spanned(attr, format!("cannot parse attribute: {error}")).to_compile_error().into() + } + }; + + match attr { + syn::Meta::NameValue(value) => if value.path.is_ident("doc") { + if let syn::Lit::Str(ref text) = value.lit { + desc.push_str(&text.value()); + desc.push_str(" "); + } + }, + syn::Meta::List(value) => if value.path.is_ident("arg") { + for value_attr in value.nested.iter() { + match value_attr { + syn::NestedMeta::Meta(value_attr) => { + match value_attr { + syn::Meta::Path(value_attr) => if value_attr.is_ident("short") { + short = Some(format!("{}", name.chars().next().unwrap()).to_lowercase()); + } else if value_attr.is_ident("long") { + long = Some(name.to_lowercase()); + } else if value_attr.is_ident("default_value") { + default = Some(DEFAULT_INIT.to_owned()); + } else if value_attr.is_ident("required") { + if typ != OptValueType::Bool { + required = true + } else { + return syn::Error::new_spanned(value_attr, INVALID_REQUIRED_BOOL).to_compile_error().into(); + } + } else if value_attr.is_ident("sub") { + if typ == OptValueType::Value { + is_sub = true; + } else { + return syn::Error::new_spanned(value_attr, "Sub-command must be simple value").to_compile_error().into(); + } + } + syn::Meta::NameValue(value_attr) => if value_attr.path.is_ident("short") { + if let syn::Lit::Str(ref text) = value_attr.lit { + let value_attr_text = text.value(); + + if value_attr_text.contains(ARG_INVALID_CHARS) { + return syn::Error::new_spanned(value_attr.lit.clone(), ARG_NAME_SPACE_ERROR).to_compile_error().into(); + } + + short = Some(value_attr_text); + } else { + return syn::Error::new_spanned(value_attr.path.clone(), INVALID_ARG_TYPE_STRING).to_compile_error().into(); + } + } else if value_attr.path.is_ident("long") { + if let syn::Lit::Str(ref text) = value_attr.lit { + let value_attr_text = text.value(); + + if value_attr_text.contains(ARG_INVALID_CHARS) { + return syn::Error::new_spanned(value_attr.lit.clone(), ARG_NAME_SPACE_ERROR).to_compile_error().into(); + } + + long = Some(value_attr_text) + } else { + return syn::Error::new_spanned(value_attr.path.clone(), INVALID_ARG_TYPE_STRING).to_compile_error().into(); + } + } else if value_attr.path.is_ident("default_value") { + if let syn::Lit::Str(ref text) = value_attr.lit { + default = Some(text.value()); + } else { + return syn::Error::new_spanned(value_attr.path.clone(), INVALID_ARG_TYPE_STRING).to_compile_error().into(); + } + } else { + return syn::Error::new_spanned(value_attr.path.clone(), UNKNOWN_ARG_ATTR).to_compile_error().into(); + } + _ => { + }, + } + }, + syn::NestedMeta::Lit(_) => (), + } + } + }, + _ => (), + } + } + + desc.pop(); + + if required && default.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Marked as required, but default value is provided?").to_compile_error().into(); + } else if is_optional && default.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Optional, but default value is provided?").to_compile_error().into(); + } else if is_sub && is_optional { + return syn::Error::new_spanned(field.ident.clone(), "Sub-command cannot be optional").to_compile_error().into(); + } else if is_sub && default.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Sub-command cannot have default value").to_compile_error().into(); + } else if !required && !is_optional && default.is_none() { + default = Some(DEFAULT_INIT.to_owned()); + } + + if short.is_none() && long.is_none() { + if typ == OptValueType::MultiValue { + if multi_argument.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Second argument collection. There can be only one").to_compile_error().into(); + } else if sub_command.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Multi-argument collection and sub-command are mutually exclusive").to_compile_error().into(); + } + + multi_argument = Some(Argument { + field_name, + name, + desc, + required, + is_optional, + default, + }); + + } else if is_sub { + if sub_command.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Second sub-command. There can be only one").to_compile_error().into(); + } else if multi_argument.is_some() { + return syn::Error::new_spanned(field.ident.clone(), "Sub-command and multi-argument collection are mutually exclusive").to_compile_error().into(); + } + + sub_command = Some(Argument { + field_name, + name, + desc, + required: true, + is_optional: false, + default: None, + }); + } else { + arguments.push(Argument { + field_name, + name, + desc, + required, + is_optional, + default, + }) + } + + } else { + let long = match long { + Some(long) => long, + None => name.clone() + }; + + options.push(Opt { + arg: Argument { + field_name, + name, + desc, + required, + is_optional, + default, + }, + short, + long, + typ + }) + } + } + + let (impl_gen, type_gen, where_clause) = ast.generics.split_for_impl(); + + let help_msg = { + use std::io::Write; + use tabwriter::TabWriter; + + let mut tw = TabWriter::new(vec![]); + + let _ = write!(tw, "{} +USAGE:", about_prog); + + if !options.is_empty() { + let _ = write!(tw, " [OPTIONS]"); + } + + for argument in arguments.iter() { + let _ = if argument.required { + write!(tw, " <{}>", argument.name) + } else { + write!(tw, " [{}]", argument.name) + }; + } + + if let Some(argument) = multi_argument.as_ref() { + let _ = if argument.required { + write!(tw, " <{}>...", argument.name) + } else { + write!(tw, " [{}]...", argument.name) + }; + } else if let Some(argument) = sub_command.as_ref() { + let _ = write!(tw, " <{}>", argument.name); + } + + if !options.is_empty() { + let _ = write!(tw, "\n\nOPTIONS:\n"); + } + + for option in options.iter() { + let _ = write!(tw, "\t"); + if let Some(short) = option.short.as_ref() { + let _ = write!(tw, "-{},", short); + } + let _ = write!(tw, "\t"); + + let _ = write!(tw, "--{}", option.long); + + let _ = match option.typ { + OptValueType::MultiValue => write!(tw, " <{}>...", option.arg.name), + OptValueType::Value => write!(tw, " <{}>", option.arg.name), + _ => Ok(()), + }; + + let _ = write!(tw, "\t{}\n", option.arg.desc); + } + + if !arguments.is_empty() || multi_argument.is_some() || sub_command.is_some() { + let _ = write!(tw, "\nARGS:\n"); + } + + for argument in arguments.iter() { + let _ = if argument.required { + writeln!(tw, "\t<{}>\t{}", argument.name, argument.desc) + } else { + writeln!(tw, "\t[{}]\t{}", argument.name, argument.desc) + }; + } + + if let Some(argument) = multi_argument.as_ref() { + let _ = writeln!(tw, "\t<{}>...\t{}", argument.name, argument.desc); + } else if let Some(command) = sub_command.as_ref() { + let _ = writeln!(tw, "\t<{}>\t{}", command.name, command.desc); + } + + let _ = tw.flush(); + + String::from_utf8(tw.into_inner().unwrap()).unwrap() + }; + + let mut result = String::new(); + let _ = writeln!(result, "{} {} for {}{} {{", quote!(impl #impl_gen), PARSER_TRAIT, ast.ident, quote!(#type_gen #where_clause)); + let _ = writeln!(result, "{}const HELP: &'static str = \"{}\";", TAB, help_msg); + + let _ = writeln!(result, "{}fn from_args<'a, T: IntoIterator>(_args_: T) -> Result> {{", TAB); + + for option in options.iter() { + if option.arg.field_name == "_" { + continue; + } + + let _ = match option.typ { + OptValueType::MultiValue => writeln!(result, "{0}{0}let mut {1} = Vec::new();", TAB, option.arg.field_name), + OptValueType::Bool => writeln!(result, "{0}{0}let mut {1} = false;", TAB, option.arg.field_name), + _ => writeln!(result, "{0}{0}let mut {1} = None;", TAB, option.arg.field_name), + }; + } + + for argument in arguments.iter() { + let _ = writeln!(result, "{0}{0}let mut {1} = None;", TAB, argument.field_name); + } + + if let Some(argument) = multi_argument.as_ref() { + let _ = writeln!(result, "{0}{0}let mut {1} = Vec::new();", TAB, argument.field_name); + } else if let Some(command) = sub_command.as_ref() { + let _ = writeln!(result, "{0}{0}let mut {1} = None;", TAB, command.field_name); + } + + let _ = writeln!(result, "{0}{0}let mut _args_ = _args_.into_iter();\n", TAB); + let _ = writeln!(result, "{0}{0}while let Some(_arg_) = _args_.next() {{", TAB); + + //options + let _ = writeln!(result, "{0}{0}{0}if let Some(_arg_) = _arg_.strip_prefix('-') {{", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}match _arg_ {{", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}{0}\"h\" | \"-help\" => return Err(arg::ParseKind::Top(arg::ParseError::HelpRequested(Self::HELP))),", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}{0}\"\" => (),", TAB); + + for option in options.iter() { + if option.arg.field_name == "_" { + continue; + } + + let _ = write!(result, "{0}{0}{0}{0}{0}", TAB); + + if let Some(short) = option.short.as_ref() { + let _ = write!(result, "\"{}\" | ", short); + } + + let _ = write!(result, "\"-{}\" => ", option.long); + + let _ = match option.typ { + OptValueType::Help => panic!("Option Help is invalid here. Bug report it"), + OptValueType::Bool => write!(result, "{{ {0} = !{0}; continue }},", option.arg.field_name), + OptValueType::Value => write!(result, "match _args_.next() {{ +{0}{0}{0}{0}{0}{0}Some(_next_arg_) => match {1}(_next_arg_) {{ +{0}{0}{0}{0}{0}{0}{0}Ok(value) => {{ {2} = Some(value); continue }}, +{0}{0}{0}{0}{0}{0}{0}Err(_) => return Err(arg::ParseKind::Top(arg::ParseError::InvalidFlagValue(\"{3}\", _next_arg_))), +{0}{0}{0}{0}{0}{0}}}, +{0}{0}{0}{0}{0}{0}None => return Err(arg::ParseKind::Top(arg::ParseError::MissingValue(\"{3}\"))), +{0}{0}{0}{0}{0}}}", TAB, FROM_FN, option.arg.field_name, option.arg.name), + OptValueType::MultiValue => write!(result, "match _args_.next() {{ +{0}{0}{0}{0}{0}{0}Some(_next_arg_) => match {1}(_next_arg_) {{ +{0}{0}{0}{0}{0}{0}{0}Ok(value) => {{ {2}.push(value); continue }}, +{0}{0}{0}{0}{0}{0}{0}Err(_) => return Err(arg::ParseKind::Top(arg::ParseError::InvalidFlagValue(\"{3}\", _next_arg_))), +{0}{0}{0}{0}{0}{0}}}, +{0}{0}{0}{0}{0}{0}None => return Err(arg::ParseKind::Top(arg::ParseError::MissingValue(\"{3}\"))), +{0}{0}{0}{0}{0}}}", TAB, FROM_FN, option.arg.field_name, option.arg.name), + }; + let _ = writeln!(result, ""); + } + let _ = writeln!(result, "{0}{0}{0}{0}{0}_ => return Err(arg::ParseKind::Top(arg::ParseError::UnknownFlag(_arg_))),", TAB); + + let _ = writeln!(result, "{0}{0}{0}{0}}}", TAB); + let _ = writeln!(result, "{0}{0}{0}}}", TAB); + //rest args + for (idx, arg) in arguments.iter().enumerate() { + if idx == 0 { + let _ = writeln!(result, "{0}{0}{0}if {1}.is_none() {{", TAB, arg.field_name); + } else { + let _ = writeln!(result, "{0}{0}{0}}} else if {1}.is_none() {{", TAB, arg.field_name); + } + let _ = writeln!(result, "{0}{0}{0}{0}match {1}(_arg_) {{", TAB, FROM_FN); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Ok(_res_) => {1} = Some(_res_),", TAB, arg.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(_) => return Err(arg::ParseKind::Top(arg::ParseError::InvalidArgValue(\"{1}\", _arg_))),", TAB, arg.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}}}", TAB); + } + //too many args? + if !arguments.is_empty() { + let _ = writeln!(result, "{0}{0}{0}}} else {{", TAB); + } + + if let Some(arg) = multi_argument.as_ref() { + let _ = writeln!(result, "{0}{0}{0}{0}match {1}(_arg_) {{", TAB, FROM_FN); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Ok(_res_) => {1}.push(_res_),", TAB, arg.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(_) => return Err(arg::ParseKind::Top(arg::ParseError::InvalidArgValue(\"{1}\", _arg_))),", TAB, arg.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}}}", TAB); + } else if let Some(command) = sub_command.as_ref() { + let _ = writeln!(result, "{0}{0}{0}{0}match {1}::from_args(core::iter::once(_arg_).chain(_args_)) {{", TAB, PARSER_TRAIT); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Ok(_res_) => {{ {1} = Some(_res_); break; }},", TAB, command.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(arg::ParseKind::Top(_)) => return Err(arg::ParseKind::Top(arg::ParseError::RequiredArgMissing(\"{1}\"))),", TAB, command.field_name); + let _ = writeln!(result, "{0}{0}{0}{0}{0}Err(arg::ParseKind::Sub(name, error)) => return Err(arg::ParseKind::Sub(name, error)),", TAB); + let _ = writeln!(result, "{0}{0}{0}{0}}}", TAB); + } else { + let _ = writeln!(result, "{0}{0}{0}{0} return Err(arg::ParseKind::Top(arg::ParseError::TooManyArgs));", TAB); + } + //exit args + if !arguments.is_empty() { + let _ = writeln!(result, "{0}{0}{0}}}", TAB); + let _ = writeln!(result, "{0}{0}}}", TAB); + } else { + let _ = writeln!(result, "{0}{0}}}", TAB); + } + + //Set defaults + for option in options.iter() { + if option.arg.field_name == "_" { + continue; + } + + let _ = match option.typ { + OptValueType::MultiValue => Ok(()), + OptValueType::Bool => Ok(()), + _ => match option.arg.default { + Some(ref default) => writeln!(result, "{0}{0}let {1} = if let Some(value) = {1} {{ value }} else {{ {2} }};", TAB, option.arg.field_name, default), + None => match option.arg.is_optional { + true => Ok(()), + false => writeln!(result, "{0}{0}let {1} = if let Some(value) = {1} {{ value }} else {{ return Err(arg::ParseKind::Top(arg::ParseError::RequiredArgMissing(\"{2}\"))) }};", TAB, option.arg.field_name, option.arg.name), + }, + }, + }; + } + + for arg in arguments.iter() { + let _ = match arg.default { + Some(ref default) => writeln!(result, "{0}{0}let {1} = if let Some(value) = {1} {{ value }} else {{ {2} }};", TAB, arg.field_name, default), + None => match arg.is_optional { + true => Ok(()), + false => writeln!(result, "{0}{0}let {1} = if let Some(value) = {1} {{ value }} else {{ return Err(arg::ParseKind::Top(arg::ParseError::RequiredArgMissing(\"{2}\"))) }};", TAB, arg.field_name, arg.name), + } + }; + } + + if let Some(command) = sub_command.as_ref() { + let _ = writeln!(result, "{0}{0}let {1} = if let Some(value) = {1} {{ value }} else {{ return Err(arg::ParseKind::Top(arg::ParseError::RequiredArgMissing(\"{2}\"))) }};", TAB, command.field_name, command.name); + } + + //Fill result + let _ = writeln!(result, "{0}{0}Ok(Self {{", TAB); + + for option in options.iter() { + if option.arg.field_name == "_" { + continue; + } + + let _ = if option.arg.is_optional && option.typ == OptValueType::Bool { + writeln!(result, "{0}{0}{0}{1}: Some({1}),", TAB, option.arg.field_name) + } else { + writeln!(result, "{0}{0}{0}{1},", TAB, option.arg.field_name) + }; + } + + for arg in arguments.iter() { + let _ = writeln!(result, "{0}{0}{0}{1},", TAB, arg.field_name); + } + + if let Some(arg) = multi_argument.as_ref() { + let _ = writeln!(result, "{0}{0}{0}{1},", TAB, arg.field_name); + } else if let Some(arg) = sub_command.as_ref() { + let _ = writeln!(result, "{0}{0}{0}{1},", TAB, arg.field_name); + } + + let _ = writeln!(result, "{0}{0}}})", TAB); + + //Exit fn + let _ = writeln!(result, "{}}}", TAB); + + let _ = writeln!(result, "}}"); + + if let Ok(val) = std::env::var("ARG_RS_PRINT_PARSER") { + match val.trim() { + "0" | "false" => (), + _ => println!("{result}"), + } + } + result.parse().expect("To parse generated code") +} + +#[proc_macro_derive(Args, attributes(parser, arg))] +pub fn parser_derive(input: TokenStream) -> TokenStream { + const INVALID_INPUT_TYPE: &str = "Unsupported parser input type. Expect: struct"; + let ast: syn::DeriveInput = syn::parse(input).unwrap(); + + match ast.data { + syn::Data::Struct(ref data) => from_struct(&ast, data), + syn::Data::Enum(ref data) => from_enum(&ast, data), + _ => syn::Error::new_spanned(ast.ident, INVALID_INPUT_TYPE).to_compile_error().into(), + } +} diff --git a/src/env/derive/utils.rs b/src/env/derive/utils.rs new file mode 100644 index 0000000..c8b62bf --- /dev/null +++ b/src/env/derive/utils.rs @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of SPD. + * + * SPD is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * SPD is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SPD. If not, see . + * + * This file incorporates work covered by the following copyright and permission + * notice: + * + * Copyright 2017 Douman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +extern crate alloc; +use alloc::string::String; + +pub fn to_hyphenated_lower_case(input: &str) -> String { + let mut output = String::with_capacity(input.len()); + + let mut chars = input.chars(); + let mut prev_upper = false; + if let Some(ch) = chars.next() { + if ch.is_uppercase() { + prev_upper = true; + for ch in ch.to_lowercase() { + output.push(ch); + } + } else { + output.push(ch); + } + } + + for ch in chars { + if ch.is_uppercase() { + if !prev_upper { + output.push('-'); + } + + for ch in ch.to_lowercase() { + output.push(ch); + } + + prev_upper = true; + } else { + output.push(ch); + + prev_upper = false; + } + } + + output +} diff --git a/src/env/mod.rs b/src/env/mod.rs new file mode 100644 index 0000000..f278e81 --- /dev/null +++ b/src/env/mod.rs @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of SPD. + * + * SPD is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * SPD is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SPD. If not, see . + * + * This file incorporates work covered by the following copyright and permission + * notice: + * + * Copyright 2017 Douman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#![no_std] +#![warn(missing_docs)] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::style))] +#![cfg_attr(feature = "cargo-clippy", allow(clippy::needless_lifetimes))] + +use core::fmt; + +mod derive; +mod split; + +use derive::*; +use split::Split; + + +#[derive(PartialEq, Eq, Debug)] +///Parse errors +pub enum ParseKind<'a> { + ///Main command result + Top(ParseError<'a>), + ///Sub-command name and result + Sub(&'static str, ParseError<'a>), +} + +impl<'a> ParseKind<'a> { + #[inline] + ///Returns whether help is requested + pub fn is_help(&self) -> bool { + match self { + Self::Top(err) => err.is_help(), + Self::Sub(_, err) => err.is_help(), + } + } +} + +impl<'a> PartialEq> for ParseKind<'a> { + #[inline(always)] + fn eq(&self, right: &ParseError<'a>) -> bool { + match self { + Self::Top(left) => PartialEq::eq(left, right), + Self::Sub(_, left) => PartialEq::eq(left, right), + } + } +} + +#[derive(PartialEq, Eq, Debug)] +///Parse errors +pub enum ParseError<'a> { + ///User requested help. + /// + ///Contains slice with `Args::HELP` + HelpRequested(&'static str), + ///Too many arguments are specified. + TooManyArgs, + ///Argument is required, but missing + /// + ///Contains name of argument + RequiredArgMissing(&'a str), + ///Flag is specified, but value is missing. + /// + ///Contains full flag name. + MissingValue(&'a str), + ///Flag is specified with invalid value + /// + ///Contains full flag name and provided value. + InvalidFlagValue(&'a str, &'a str), + ///Argument is supplied with invalid vlaue + /// + ///Contains argument name and provided value. + InvalidArgValue(&'a str, &'a str), + ///Unknown flag is specified. + UnknownFlag(&'a str) +} + +impl<'a> ParseError<'a> { + ///Returns whether help is requested + pub fn is_help(&self) -> bool { + match self { + ParseError::HelpRequested(_) => true, + _ => false, + } + } +} + +impl<'a> fmt::Display for ParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::HelpRequested(help) => f.write_str(help), + ParseError::TooManyArgs => f.write_str("Too many arguments are provided"), + ParseError::RequiredArgMissing(arg) => write!(f, "Argument '{}' is required, but not provided", arg), + ParseError::MissingValue(arg) => write!(f, "Flag '{}' is provided without value", arg), + ParseError::InvalidFlagValue(arg, value) => write!(f, "Flag '{}' is provided with '{}' which is invalid", arg, value), + ParseError::InvalidArgValue(arg, value) => write!(f, "Argument '{}' is provided with '{}' which is invalid", arg, value), + ParseError::UnknownFlag(flag) => write!(f, "Unknown flag '{}' is provided", flag), + } + } +} + +impl<'a> fmt::Display for ParseKind<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseKind::Top(res) => fmt::Display::fmt(res, f), + ParseKind::Sub(name, res) => write!(f, "{name}: {res}"), + } + } +} + + +///Describers command line argument parser +pub trait Args: Sized { + ///Help message for parser. + const HELP: &'static str; + + ///Parses arguments from iterator of strings + fn from_args<'a, T: IntoIterator>(args: T) -> Result>; + + ///Parses arguments from string, which gets tokenized and passed to from. + fn from_text<'a>(text: &'a str) -> Result> { + Self::from_args(Split::from_str(text)) + } +} diff --git a/src/env/split.rs b/src/env/split.rs new file mode 100644 index 0000000..491ce7c --- /dev/null +++ b/src/env/split.rs @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of SPD. + * + * SPD is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * SPD is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SPD. If not, see . + * + * This file incorporates work covered by the following copyright and permission + * notice: + * + * Copyright 2017 Douman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +///Simple split of string into arguments +pub struct Split<'a> { + string: &'a str, +} + +impl<'a> Split<'a> { + ///Creates new instance + pub const fn from_str(string: &'a str) -> Self { + Self { + string + } + } + + #[inline(always)] + ///Retrieves next argument + pub fn next_arg(&mut self) -> Option<&str> { + self.next() + } +} + +impl<'a> Iterator for Split<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + self.string = self.string.trim_start(); + + if self.string.is_empty() { + return None; + } + + let (end_char, from) = match self.string.get(0..=0) { + Some("'") => ('\'', 1), + Some("\"") => ('"', 1), + _ => (' ', 0), + }; + + match self.string[from..].find(end_char) { + Some(end_idx) => { + let end_idx = end_idx + from; + let result = &self.string[from..end_idx]; + self.string = &self.string[end_idx+1..]; + Some(result.trim_start()) + }, + None => { + let result = &self.string[from..]; + self.string = ""; + Some(result.trim()) + } + } + } +} diff --git a/tests/args.rs b/tests/args.rs new file mode 100644 index 0000000..686c270 --- /dev/null +++ b/tests/args.rs @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * This file is part of SPD. + * + * SPD is free software: you can redistribute it and/or modify it under the + * terms of the GNU Lesser General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) any + * later version. + * + * SPD is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SPD. If not, see . + * + * This file incorporates work covered by the following copyright and permission + * notice: + * + * Copyright 2017 Douman + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use crate::env::args::Args; + +#[allow(unused)] +#[derive(Args, Debug)] +struct Test4 { +} + +#[allow(unused)] +#[derive(Args, Debug)] +struct Test3 { + paths: Vec, +} + +#[allow(unused)] +#[derive(Args, Debug)] +struct Test2 { + #[arg(short = "u")] + u: bool, + + paths: Vec, +} + + +#[derive(Debug, Args)] +///my_exe 0.1.0 +///About my program +/// +///About my program +struct MyArgs { + #[arg(short, long, required)] + ///Required argument + required: u32, + + #[arg(short, long)] + ///Optional argument + optional: Option, + + #[arg(short, long)] + ///About this flag + flag: bool, + + #[arg(long = "verbose")] + ///Verbose mode + verbose: bool, + + #[arg(short = "v", long = "velocity", default_value = "42")] + ///This is felocity. Default value is 42. + speed: u32, + + #[arg(short = "g", long = "gps")] + ///GPS coordinates. + gps: Vec, + + #[arg(required)] + ///To store path + path: String, + + #[arg(required)] + ///To store path 2 + path2: String, + + ///To store rest of paths + remain_paths: Vec, +} + +#[derive(Debug, Args)] +///My subcommand +enum BigSubArgs { + ///my arguments + MyArgs(MyArgs), + ///test args + Test(Test2), +} + +#[derive(Debug, Args)] +struct BigArgs { + #[arg(long = "verbose")] + ///Verbose mode + verbose: bool, + #[arg(sub)] + cmd: BigSubArgs, +} + +#[test] +fn should_error_on_missing_args() { + let result = MyArgs::from_text("-f --verbose path1").unwrap_err(); + assert_eq!(result, arg::ParseError::RequiredArgMissing("required")); + + let result = MyArgs::from_text("-f -r 5 --verbose path1").unwrap_err(); + assert_eq!(result, arg::ParseError::RequiredArgMissing("path2")); + + let result = MyArgs::from_text("-f -r 5 --verbose").unwrap_err(); + assert_eq!(result, arg::ParseError::RequiredArgMissing("path")); +} + +#[test] +fn should_error_on_missing_flag_value() { + let result = MyArgs::from_text("-f -r").unwrap_err(); + assert_eq!(result, arg::ParseError::MissingValue("required")); +} + +#[test] +fn should_error_on_invalid_flag_value() { + let result = MyArgs::from_text("-f -r gg").unwrap_err(); + assert_eq!(result, arg::ParseError::InvalidFlagValue("required", "gg")); +} + + +#[test] +fn should_handle_all_flags() { + let result = MyArgs::from_text("-f -r 5 --verbose -v 32 -g 1 --gps 55 path1 path2 rest1 rest2").unwrap(); + assert!(result.flag); + assert!(result.verbose); + assert_eq!(result.optional, None); + assert_eq!(result.required, 5); + assert_eq!(result.speed, 32); + assert_eq!(result.gps, &[1, 55]); + assert_eq!(result.path, "path1"); + assert_eq!(result.path2, "path2"); + assert_eq!(result.remain_paths, &["rest1", "rest2"]); + + let result = MyArgs::from_text("-f -r 5 --verbose -o 13 -v 32 -g 1 --gps 55 path1 path2 rest1 rest2").unwrap(); + assert!(result.flag); + assert!(result.verbose); + assert_eq!(result.optional, Some(13)); + assert_eq!(result.required, 5); + assert_eq!(result.speed, 32); + assert_eq!(result.gps, &[1, 55]); + assert_eq!(result.path, "path1"); + assert_eq!(result.path2, "path2"); + assert_eq!(result.remain_paths, &["rest1", "rest2"]); +} + +#[test] +fn should_fail_invalid_sub_command() { + let result = BigArgs::from_text("--verbose my-invalid-args -f -r 5 --verbose -v 32 -g 1 --gps 55 path1 path2 rest1 rest2").unwrap_err(); + assert_eq!(result, arg::ParseKind::Top(arg::ParseError::RequiredArgMissing("cmd"))); +} + +#[test] +fn should_fail_sub_command_with_wrong_args() { + let result = BigArgs::from_text("--verbose my-args -f -r 5 --verbose -v 32 -g 1 --gps lolka path1 path2 rest1 rest2").unwrap_err(); + assert_eq!(result, arg::ParseKind::Sub("my-args", arg::ParseError::InvalidFlagValue("gps", "lolka"))); +} + +#[test] +fn should_handle_all_flags_as_sub_command() { + let result = BigArgs::from_text("--verbose my-args -f -r 5 --verbose -v 32 -g 1 --gps 55 path1 path2 rest1 rest2").unwrap(); + assert!(result.verbose); + let result = match result.cmd { + BigSubArgs::MyArgs(args) => args, + unexpected => panic!("invalid sub command result: {:?}", unexpected), + }; + assert!(result.flag); + assert!(result.verbose); + assert_eq!(result.optional, None); + assert_eq!(result.required, 5); + assert_eq!(result.speed, 32); + assert_eq!(result.gps, &[1, 55]); + assert_eq!(result.path, "path1"); + assert_eq!(result.path2, "path2"); + assert_eq!(result.remain_paths, &["rest1", "rest2"]); + + let result = BigArgs::from_text("--verbose test -u path1 path2 rest1 rest2").unwrap(); + assert!(result.verbose); + let result = match result.cmd { + BigSubArgs::Test(args) => args, + unexpected => panic!("invalid sub command result: {:?}", unexpected), + }; + assert!(result.u); + assert_eq!(result.paths, &["path1", "path2", "rest1", "rest2"]); +} + +#[test] +fn should_supply_default_value() { + let result = MyArgs::from_text("-f -r 5 --verbose -g 1 --gps 55 path1 path2 rest1 rest2").unwrap(); + assert_eq!(result.speed, 42); +} + +#[test] +fn shoukd_handle_dash() { + let result = MyArgs::from_text("-f -r 5 --verbose -g 1 --gps 55 path1 path2 rest1 -").unwrap(); + assert_eq!(result.remain_paths[1], "-"); +} +