1use std::path::PathBuf;
9
10use crate::format::Format;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Command {
15 Sql(String),
17 File(PathBuf),
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Action {
24 Run(Box<Options>),
26 Version,
28 Help,
30 Config,
32 Wrong(String),
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Options {
39 pub database: String,
41 pub commands: Vec<Command>,
43 pub stop_after_commands: bool,
45 pub interactive: Option<bool>,
48 pub echo: bool,
50 pub bail: bool,
52 pub readonly: bool,
54 pub settings: crate::format::Settings,
56}
57
58impl Default for Options {
59 fn default() -> Self {
60 Self {
61 database: ":memory:".to_string(),
62 commands: Vec::new(),
63 stop_after_commands: false,
64 interactive: None,
65 echo: false,
66 bail: false,
67 readonly: false,
68 settings: crate::format::Settings::default(),
69 }
70 }
71}
72
73pub fn parse(arguments: &[String]) -> Action {
79 let mut options = Options::default();
80 let mut positional = 0;
81 let mut at = 0;
82 while at < arguments.len() {
83 let argument = arguments[at].as_str();
84 at += 1;
85 let mut next = |name: &str| -> Result<String, String> {
86 if at < arguments.len() {
87 let value = arguments[at].clone();
88 at += 1;
89 Ok(value)
90 } else {
91 Err(format!("{name} wants a value"))
92 }
93 };
94 match argument {
95 "-version" | "--version" | "-V" => return Action::Version,
96 "-h" | "-help" | "--help" => return Action::Help,
97 "--print-config" => return Action::Config,
98 "-c" | "-s" | "--command" => match next(argument) {
99 Ok(sql) => {
100 options.commands.push(Command::Sql(sql));
101 options.stop_after_commands = true;
102 }
103 Err(why) => return Action::Wrong(why),
104 },
105 "-cmd" => match next(argument) {
106 Ok(sql) => options.commands.push(Command::Sql(sql)),
107 Err(why) => return Action::Wrong(why),
108 },
109 "-f" | "-file" => match next(argument) {
110 Ok(path) => {
111 options.commands.push(Command::File(PathBuf::from(path)));
112 options.stop_after_commands = true;
113 }
114 Err(why) => return Action::Wrong(why),
115 },
116 "-init" => match next(argument) {
117 Ok(path) => options.commands.push(Command::File(PathBuf::from(path))),
118 Err(why) => return Action::Wrong(why),
119 },
120 "-separator" => match next(argument) {
121 Ok(value) => options.settings.separator = value,
122 Err(why) => return Action::Wrong(why),
123 },
124 "-newline" => match next(argument) {
125 Ok(value) => options.settings.newline = value,
126 Err(why) => return Action::Wrong(why),
127 },
128 "-nullvalue" => match next(argument) {
129 Ok(value) => options.settings.nullvalue = value,
130 Err(why) => return Action::Wrong(why),
131 },
132 "-header" => options.settings.header = true,
133 "-noheader" => options.settings.header = false,
134 "-echo" => options.echo = true,
135 "-bail" => options.bail = true,
136 "-readonly" => options.readonly = true,
137 "-interactive" => options.interactive = Some(true),
138 "-batch" => options.interactive = Some(false),
139 "-no-stdin" => options.stop_after_commands = true,
140 "-no-init" | "-unsigned" | "-unredacted" | "-safe" => {}
141 other if other.starts_with('-') => {
142 match Format::from_name(other.trim_start_matches('-')) {
143 Some(format) => options.settings.set_format_flag(format),
144 None => return Action::Wrong(format!("unknown option {other}")),
145 }
146 }
147 other => {
148 positional += 1;
149 match positional {
150 1 => options.database = other.to_string(),
151 2 => {
152 options.commands.push(Command::Sql(other.to_string()));
153 options.stop_after_commands = true;
154 }
155 _ => return Action::Wrong(format!("too many arguments, starting at {other}")),
156 }
157 }
158 }
159 }
160 Action::Run(Box::new(options))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::{Action, Command, parse};
166 use crate::format::Format;
167
168 fn options(arguments: &[&str]) -> super::Options {
169 let owned: Vec<String> = arguments.iter().map(|text| (*text).to_string()).collect();
170 match parse(&owned) {
171 Action::Run(options) => *options,
172 other => panic!("expected a run, got {other:?}"),
173 }
174 }
175
176 #[test]
177 fn nothing_means_an_interactive_memory_database() {
178 let parsed = options(&[]);
179 assert_eq!(parsed.database, ":memory:");
180 assert!(parsed.commands.is_empty());
181 assert!(!parsed.stop_after_commands);
182 }
183
184 #[test]
185 fn a_command_runs_and_stops() {
186 let parsed = options(&["-c", "SELECT 1"]);
187 assert_eq!(parsed.commands, vec![Command::Sql("SELECT 1".to_string())]);
188 assert!(parsed.stop_after_commands);
189 }
190
191 #[test]
192 fn commands_keep_their_order() {
193 let parsed = options(&["-c", "one", "-c", "two"]);
194 assert_eq!(
195 parsed.commands,
196 vec![Command::Sql("one".to_string()), Command::Sql("two".to_string())]
197 );
198 }
199
200 #[test]
201 fn cmd_runs_first_and_does_not_stop() {
202 let parsed = options(&["-cmd", ".mode csv"]);
203 assert!(!parsed.stop_after_commands);
204 }
205
206 #[test]
207 fn the_first_positional_is_the_database_and_the_second_is_sql() {
208 let parsed = options(&["shop.db", "SELECT 1"]);
209 assert_eq!(parsed.database, "shop.db");
210 assert_eq!(parsed.commands, vec![Command::Sql("SELECT 1".to_string())]);
211 assert!(parsed.stop_after_commands);
212 }
213
214 #[test]
215 fn a_mode_flag_sets_the_mode_and_its_separator() {
216 let parsed = options(&["-csv"]);
217 assert_eq!(parsed.settings.format, Format::Csv);
218 assert_eq!(parsed.settings.separator, ",");
219 }
220
221 #[test]
227 fn a_mode_flag_leaves_the_row_separator_where_it_was_and_the_dot_command_does_not() {
228 assert_eq!(options(&["-csv"]).settings.newline, "\n");
229 assert_eq!(options(&["-ascii"]).settings.newline, "\n");
230 assert_eq!(options(&["-csv", "-newline", ";"]).settings.newline, ";");
231 }
232
233 #[test]
234 fn a_separator_given_after_the_mode_wins() {
235 let parsed = options(&["-csv", "-separator", ";"]);
236 assert_eq!(parsed.settings.separator, ";");
237 }
238
239 #[test]
240 fn an_unknown_option_is_an_error_rather_than_a_filename() {
241 assert!(matches!(parse(&["-csvv".to_string()]), Action::Wrong(_)));
242 }
243
244 #[test]
245 fn an_option_missing_its_value_says_so() {
246 assert!(matches!(parse(&["-c".to_string()]), Action::Wrong(_)));
247 }
248
249 #[test]
250 fn version_and_help_win_wherever_they_appear() {
251 assert!(matches!(parse(&["-csv".to_string(), "-version".to_string()]), Action::Version));
252 assert!(matches!(parse(&["-help".to_string()]), Action::Help));
253 }
254}