1pub struct Command {
6 pub name: &'static str,
7 pub aliases: &'static [&'static str],
8 pub options: Option<&'static [&'static str]>,
10}
11
12const GET_UNIQUE: &[&str] = &[
13 "PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN", "INTO", "VARIABLE", "LAST", "SEGMENT", "SEGLENGTH", "OFFSET", "LOCKED", "LOCKCLASS", "MOVENEXT", "GETFIRST", "SET",
14 "SETCOND", "SETZERO", "SETPARENT", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH",
15];
16
17const GET_NEXT: &[&str] = &[
18 "PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN", "INTO", "VARIABLE", "FIRST", "LAST", "CURRENT", "SEGMENT", "SEGLENGTH", "OFFSET", "LOCKED", "LOCKCLASS", "MOVENEXT",
19 "GETFIRST", "SET", "SETCOND", "SETZERO", "SETPARENT", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH",
20];
21
22pub const COMMANDS: &[Command] = &[
25 Command { name: "GU", aliases: &["GET UNIQUE"], options: Some(GET_UNIQUE) },
26 Command { name: "GHU", aliases: &[], options: Some(GET_UNIQUE) },
27 Command { name: "GN", aliases: &["GET NEXT"], options: Some(GET_NEXT) },
28 Command { name: "GHN", aliases: &[], options: Some(GET_NEXT) },
29 Command { name: "GNP", aliases: &["GET NEXT IN PARENT"], options: Some(GET_NEXT) },
30 Command { name: "GHNP", aliases: &[], options: Some(GET_NEXT) },
31 Command { name: "ISRT", aliases: &["INSERT"], options: Some(&["PCB", "AIB", "VARIABLE", "FIRST", "LAST", "CURRENT", "SEGMENT", "SEGLENGTH", "FROM", "OFFSET", "MOVENEXT", "GETFIRST", "SET", "SETCOND", "SETZERO", "WHERE", "FIELDLENGTH", "KEYS", "KEYLENGTH"]) },
32 Command { name: "DLET", aliases: &["DELETE"], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "FROM", "SETZERO"]) },
33 Command { name: "REPL", aliases: &["REPLACE"], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "OFFSET", "FROM", "MOVENEXT", "SET", "SETCOND", "SETZERO"]) },
34 Command { name: "POS", aliases: &["POSITION"], options: Some(&["PCB", "AIB", "INTO", "KEYFEEDBACK", "FEEDBACKLEN", "SEGMENT", "WHERE", "FIELDLENGTH"]) },
35 Command { name: "RETRIEVE", aliases: &[], options: Some(&["PCB", "AIB", "KEYFEEDBACK", "FEEDBACKLEN"]) },
36 Command { name: "LOAD", aliases: &[], options: Some(&["PCB", "AIB", "VARIABLE", "SEGMENT", "SEGLENGTH", "FROM"]) },
37 Command { name: "SCHD", aliases: &["SCHEDULE"], options: Some(&["PSB", "SYSSERVE", "NODHABEND"]) },
38 Command { name: "TERM", aliases: &["TERMINATE"], options: Some(&[]) },
39 Command { name: "ACCEPT", aliases: &[], options: Some(&["STATUSGROUP", "AIB"]) },
40 Command { name: "CHKP", aliases: &["CHECKPOINT"], options: Some(&["ID", "AIB"]) },
41 Command { name: "DEQ", aliases: &[], options: Some(&["LOCKCLASS", "AIB"]) },
42 Command { name: "LOG", aliases: &[], options: Some(&["FROM", "LENGTH", "AIB"]) },
43 Command { name: "QUERY", aliases: &[], options: Some(&["PCB", "AIB"]) },
44 Command { name: "REFRESH", aliases: &[], options: Some(&["DBQUERY", "AIB"]) },
45 Command { name: "ROLB", aliases: &[], options: Some(&[]) },
46 Command { name: "ROLL", aliases: &[], options: Some(&[]) },
47 Command { name: "ROLS", aliases: &[], options: Some(&["PCB", "TOKEN", "AREA", "AIB"]) },
48 Command { name: "SETS", aliases: &[], options: Some(&["TOKEN", "AREA", "AIB"]) },
49 Command { name: "SETU", aliases: &[], options: Some(&["TOKEN", "AREA"]) },
50 Command { name: "STAT", aliases: &["STATISTICS"], options: Some(&["PCB", "INTO", "LENGTH", "VSAM", "NONVSAM", "FORMATTED", "UNFORMATTED", "SUMMARY", "AIB"]) },
51 Command { name: "SYMCHKP", aliases: &[], options: Some(&["ID", "AREA1", "AREA2", "AREA3", "AREA4", "AREA5", "AREA6", "AREA7", "LENGTH1", "LENGTH2", "LENGTH3", "LENGTH4", "LENGTH5", "LENGTH6", "LENGTH7"]) },
52 Command { name: "XRST", aliases: &[], options: Some(&["MAXLENGTH", "ID", "AREA1", "AREA2", "AREA3", "AREA4", "AREA5", "AREA6", "AREA7", "LENGTH1", "LENGTH2", "LENGTH3", "LENGTH4", "LENGTH5", "LENGTH6", "LENGTH7"]) },
53 Command { name: "GMSG", aliases: &[], options: None },
54 Command { name: "ICMD", aliases: &[], options: None },
55 Command { name: "RCMD", aliases: &[], options: None },
56];
57
58pub const NAMED: &[&str] = &["SEGMENT", "PSB"];
61
62pub fn command(words: &[&str]) -> Option<(&'static Command, usize)> {
64 let upper: Vec<String> = words.iter().map(|w| w.to_ascii_uppercase()).collect();
65 let spelled = |s: &str| {
66 let n = s.split(' ').count();
67 (upper.len() >= n && upper[..n].join(" ") == s).then_some(n)
68 };
69 COMMANDS
70 .iter()
71 .flat_map(|c| c.aliases.iter().chain([&c.name]).filter_map(move |s| Some((c, spelled(s)?))))
72 .max_by_key(|&(_, n)| n)
73}
74
75pub fn find(name: &str) -> Option<&'static Command> {
76 COMMANDS.iter().find(|c| c.name == name)
77}
78
79const RELATIONS: &[&str] = &[">=", "<=", "¬=", "=>", "=<", "=", ">", "<", "EQ", "NE", "GT", "GE", "LT", "LE"];
80const CONNECTORS: &[&str] = &["AND", "OR", "&", "|", "*", "+"];
81
82pub fn qualification(text: &str) -> Result<Vec<(String, String, String)>, String> {
85 let mut out = Vec::new();
86 let mut rest = text.trim();
87 while !rest.is_empty() {
88 let field_end = rest.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '#' | '@' | '$'))).unwrap_or(rest.len());
89 let field = &rest[..field_end];
90 if field.is_empty() {
91 return Err(format!("a segment field name, not {rest}"));
92 }
93 rest = rest[field_end..].trim_start();
94 let relation = RELATIONS
95 .iter()
96 .find(|r| rest.get(..r.len()).is_some_and(|p| p.eq_ignore_ascii_case(r)) && (r.chars().all(|c| !c.is_ascii_alphabetic()) || rest[r.len()..].starts_with(' ')))
97 .ok_or_else(|| format!("a relational operator after {field}"))?;
98 rest = rest[relation.len()..].trim_start();
99 let (value, after) = value(rest);
100 if value.is_empty() {
101 return Err(format!("a value after {field} {relation}"));
102 }
103 out.push((field.to_ascii_uppercase(), relation.to_ascii_uppercase(), value.to_owned()));
104 rest = after.trim_start();
105 if let Some(c) = CONNECTORS.iter().find(|c| rest.get(..c.len()).is_some_and(|p| p.eq_ignore_ascii_case(c)) && rest[c.len()..].starts_with([' ', '\t'])) {
106 rest = rest[c.len()..].trim_start();
107 }
108 }
109 Ok(out)
110}
111
112fn value(text: &str) -> (&str, &str) {
114 if let Some(q @ ('\'' | '"')) = text.chars().next() {
115 let mut end = 1;
116 let bytes = text.as_bytes();
117 while end < bytes.len() {
118 if bytes[end] == q as u8 {
119 if bytes.get(end + 1) == Some(&(q as u8)) {
120 end += 2;
121 continue;
122 }
123 return (&text[..=end], &text[end + 1..]);
124 }
125 end += 1;
126 }
127 return (text, "");
128 }
129 let end = text.find(char::is_whitespace).unwrap_or(text.len());
130 (&text[..end], &text[end..])
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn long_spellings_name_their_command() {
139 let words = |s: &'static str| s.split_whitespace().collect::<Vec<_>>();
140 assert_eq!(command(&words("GET NEXT IN PARENT USING PCB(1)")).map(|(c, n)| (c.name, n)), Some(("GNP", 4)));
141 assert_eq!(command(&words("gn using pcb(1)")).map(|(c, n)| (c.name, n)), Some(("GN", 1)));
142 assert!(command(&words("FETCH X")).is_none());
143 }
144
145 #[test]
146 fn a_qualification_is_comparisons_joined_by_and_or_or() {
147 assert_eq!(qualification("ACCNTID = PA-ACCT-ID").unwrap(), [("ACCNTID".into(), "=".into(), "PA-ACCT-ID".into())]);
148 let q = qualification("KEYA > SEGKEY1 AND KEYA < 'A 350' OR KEYA>=X").unwrap();
149 assert_eq!(q.iter().map(|(f, r, v)| format!("{f}{r}{v}")).collect::<Vec<_>>(), ["KEYA>SEGKEY1", "KEYA<'A 350'", "KEYA>=X"]);
150 assert!(qualification("= X").is_err());
151 assert!(qualification("KEYA X").is_err());
152 }
153}