openfunctions_rs/parser/
bash.rs1use crate::models::{EnvVarDefinition, ParameterDefinition, ParameterType, ToolDefinition};
20use anyhow::Result;
21use regex::Regex;
22use std::collections::HashMap;
23
24pub fn parse(source: &str) -> Result<ToolDefinition> {
29 let mut description = String::new();
30 let mut parameters = Vec::new();
31 let mut env_vars = Vec::new();
32 let mut required_tools = Vec::new();
33 let metadata = HashMap::new();
34
35 let describe_re = Regex::new(r"^#\s*@describe\s+(.+)$")?;
36 let option_re =
37 Regex::new(r"^#\s*@option\s+--([a-z0-9-]+)(!?)(?:\[([^\]]+)\])?(?:<([^>]+)>)?\s+(.*)$")?;
38 let flag_re = Regex::new(r"^#\s*@flag\s+--([a-z0-9-]+)\s+(.*)$")?;
39 let env_re = Regex::new(r"^#\s*@env\s+([A-Z0-9_]+)(!?)(?:=([^\s]+))?\s+(.*)$")?;
40 let meta_re = Regex::new(r"^#\s*@meta\s+require-tools\s+(.+)$")?;
41
42 for line in source.lines() {
43 let line = line.trim();
44
45 if let Some(caps) = describe_re.captures(line) {
46 description = caps.get(1).unwrap().as_str().to_string();
47 } else if let Some(caps) = option_re.captures(line) {
48 let name = caps.get(1).unwrap().as_str().replace('-', "_");
49 let required = !caps.get(2).unwrap().as_str().is_empty();
50 let enum_values = caps.get(3).map(|m| {
51 m.as_str()
52 .split('|')
53 .map(|s| s.to_string())
54 .collect::<Vec<_>>()
55 });
56 let type_hint = caps.get(4).map(|m| m.as_str());
57 let desc = caps.get(5).unwrap().as_str().to_string();
58
59 let (param_type, enum_values_for_def) = if let Some(values) = enum_values {
60 (ParameterType::Enum(values.clone()), Some(values))
61 } else if let Some(hint) = type_hint {
62 let p_type = match hint.to_uppercase().as_str() {
63 "INT" => ParameterType::Integer,
64 "NUM" => ParameterType::Number,
65 _ => ParameterType::String,
66 };
67 (p_type, None)
68 } else {
69 (ParameterType::String, None)
70 };
71
72 parameters.push(ParameterDefinition {
73 name,
74 param_type,
75 description: desc,
76 required,
77 default: None,
78 enum_values: enum_values_for_def,
79 });
80 } else if let Some(caps) = flag_re.captures(line) {
81 let name = caps.get(1).unwrap().as_str().replace('-', "_");
82 let desc = caps.get(2).unwrap().as_str().to_string();
83
84 parameters.push(ParameterDefinition {
85 name,
86 param_type: ParameterType::Boolean,
87 description: desc,
88 required: false,
89 default: Some(serde_json::Value::Bool(false)),
90 enum_values: None,
91 });
92 } else if let Some(caps) = env_re.captures(line) {
93 let name = caps.get(1).unwrap().as_str().to_string();
94 let required = !caps.get(2).unwrap().as_str().is_empty();
95 let default = caps.get(3).map(|m| m.as_str().to_string());
96 let desc = caps.get(4).unwrap().as_str().to_string();
97
98 env_vars.push(EnvVarDefinition {
99 name,
100 description: desc,
101 required,
102 default,
103 });
104 } else if let Some(caps) = meta_re.captures(line) {
105 required_tools = caps
106 .get(1)
107 .unwrap()
108 .as_str()
109 .split_whitespace()
110 .map(|s| s.to_string())
111 .collect();
112 }
113 }
114
115 if description.is_empty() {
116 anyhow::bail!("No @describe annotation found in bash script. A description is required.");
117 }
118
119 Ok(ToolDefinition {
120 description,
121 parameters,
122 env_vars,
123 required_tools,
124 metadata,
125 })
126}