use crate::shell::Shell;
pub fn cd(shell: &mut Shell, arguments: &[&str]) {
let Some(path) = arguments.first() else {
shell.complain("Usage: .cd DIRECTORY");
return;
};
if std::env::set_current_dir(path).is_err() {
shell.complain(&format!("Cannot change to directory \"{path}\""));
}
}
pub fn system(shell: &mut Shell, arguments: &[&str]) {
if arguments.is_empty() {
shell.complain("Usage: .system COMMAND");
return;
}
let line = arguments.join(" ");
let status = if cfg!(windows) {
std::process::Command::new("cmd")
.arg("/C")
.arg(&line)
.status()
} else {
std::process::Command::new("/bin/sh")
.arg("-c")
.arg(&line)
.status()
};
if let Ok(status) = status {
if !status.success() {
shell.failed = true;
}
} else {
shell.complain("Error: cannot run the system shell");
}
}
pub fn crlf(shell: &mut Shell, arguments: &[&str]) {
if let Some(word) = arguments.first() {
shell.crlf = cfg!(windows) && crate::dot::truthy(Some(word));
}
let state = if shell.crlf { "ON" } else { "OFF" };
shell.say(&format!("crlf is {state}"));
}
pub fn prompt(shell: &mut Shell, arguments: &[&str]) {
if let Some(main) = arguments.first() {
shell.prompt_main = (*main).to_string();
}
if let Some(more) = arguments.get(1) {
shell.prompt_continue = (*more).to_string();
}
}
pub fn explain(shell: &mut Shell, arguments: &[&str]) {
shell.explain_mode = match arguments.first().map(|word| word.to_ascii_lowercase()) {
None => ExplainMode::On,
Some(word) if word == "auto" => ExplainMode::Auto,
Some(word) if crate::dot::truthy(Some(&word)) => ExplainMode::On,
Some(_) => ExplainMode::Off,
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExplainMode {
Auto,
On,
Off,
}
impl ExplainMode {
pub fn name(self) -> &'static str {
match self {
ExplainMode::Auto => "auto",
ExplainMode::On => "on",
ExplainMode::Off => "off",
}
}
}
pub fn nonce(shell: &mut Shell, arguments: &[&str]) {
let Some(value) = arguments.first() else {
shell.complain("Usage: .nonce NONCE");
return;
};
shell.nonce = Some((*value).to_string());
}
pub fn testcase(shell: &mut Shell, arguments: &[&str]) {
shell.testcase = Some(
arguments
.first()
.map(|name| (*name).to_string())
.unwrap_or_default(),
);
shell.captured.clear();
}
pub fn check(shell: &mut Shell, arguments: &[&str]) {
let line = shell.line;
let typed = if arguments.is_empty() {
".check".to_string()
} else {
format!(".check {}", arguments.join(" "))
};
let Some(name) = shell.testcase.take() else {
shell.complain(&format!("line {line}: {typed}"));
shell.complain(&format!("line {line}: ^--- no .testcase is active"));
return;
};
shell.tests_run = shell.tests_run.saturating_add(1);
let wanted = arguments.join(" ");
let got = shell.captured.clone();
if got.trim_end_matches('\n') == wanted {
shell.captured.clear();
return;
}
shell.tests_failed = shell.tests_failed.saturating_add(1);
shell.complain(&format!(
"<stdin>:{line}: .check failed for testcase {name}"
));
shell.complain(&format!("Expected: [{wanted}]"));
shell.complain(&format!("Got: [{got}]"));
shell.captured.clear();
}
pub fn report_tests(shell: &mut Shell) {
if shell.tests_run == 0 {
return;
}
let run = shell.tests_run;
let failed = shell.tests_failed;
let plural = if failed == 1 { "error" } else { "errors" };
shell.say(&format!("{run} tests run with {failed} {plural}"));
}
pub fn viewer(shell: &mut Shell, html: bool) {
let suffix = if html { "html" } else { "csv" };
let path = std::env::temp_dir().join(format!(
"inillucent-{}.{suffix}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|held| held.as_nanos())
.unwrap_or(0)
));
shell.layout.mode = if html {
crate::render::Mode::Html
} else {
crate::render::Mode::Csv
};
shell.layout.headers = true;
if let Err(reason) = shell.redirect(Some(&path.to_string_lossy()), true) {
shell.complain(&format!("Error: {reason}"));
return;
}
shell.viewer = Some(path);
}
pub fn open_viewer(path: &std::path::Path) {
let opened = if cfg!(windows) {
std::process::Command::new("cmd")
.arg("/C")
.arg("start")
.arg("")
.arg(path)
.status()
} else if cfg!(target_os = "macos") {
std::process::Command::new("open").arg(path).status()
} else {
std::process::Command::new("xdg-open").arg(path).status()
};
let _ = opened;
}
pub fn scanstats(shell: &mut Shell, arguments: &[&str]) {
let Some(word) = arguments.first() else {
shell.complain("Usage: .scanstats on|off|est");
return;
};
let folded = word.to_ascii_lowercase();
if !matches!(folded.as_str(), "on" | "off" | "est") {
shell.complain("Usage: .scanstats on|off|est");
return;
}
shell.scanstats = folded;
}
pub fn trace(shell: &mut Shell, arguments: &[&str]) {
let Some(word) = arguments.first() else {
shell.trace = None;
return;
};
if word.eq_ignore_ascii_case("off") {
shell.trace = None;
return;
}
shell.trace = Some((*word).to_string());
}
pub fn auth(shell: &mut Shell, arguments: &[&str]) {
let Some(word) = arguments.first() else {
shell.complain("Usage: .auth ON|OFF");
return;
};
let on = crate::dot::truthy(Some(word));
shell.auth = on;
shell.set_authorizer(on);
}
pub struct Watching {
pub seen: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
}
impl inillucent_driver::Authorizer for Watching {
fn authorize(
&self,
action: inillucent_driver::AuthAction<'_>,
) -> inillucent_driver::Authorization {
let line = match action {
inillucent_driver::AuthAction::Select => "SELECT NULL NULL NULL NULL".to_string(),
inillucent_driver::AuthAction::Read {
database,
table,
column,
} => format!(
"READ {} {} {} NULL",
quoted(table),
quoted(column),
quoted(database)
),
inillucent_driver::AuthAction::Function { name } => {
format!("FUNCTION NULL {} NULL NULL", quoted(name))
}
};
self.seen.borrow_mut().push(format!("authorizer: {line}"));
inillucent_driver::Authorization::Allow
}
}
fn quoted(bytes: &[u8]) -> String {
format!("\"{}\"", String::from_utf8_lossy(bytes))
}
pub fn connection(shell: &mut Shell, arguments: &[&str]) {
const MARK: usize = 6;
match arguments.first().map(|word| word.to_ascii_lowercase()) {
None => {
let active = shell.active();
for (slot, held) in shell.slots().into_iter().enumerate() {
let Some(path) = held else {
continue;
};
let mark = if slot == active { "ACTIVE" } else { "" };
let name = if path == ":memory:" {
"(memory)".to_string()
} else {
path
};
let line = format!("{mark:<MARK$} {slot}: {name}");
shell.say(&line);
}
}
Some(word) if word == "close" => {
if let Some(slot) = arguments.get(1).and_then(|text| text.parse::<usize>().ok()) {
shell.close_slot(slot);
}
}
Some(word) => {
if let Ok(slot) = word.parse::<usize>() {
if let Err(reason) = shell.use_slot(slot) {
shell.complain(&format!("Error: {reason}"));
}
}
}
}
}
pub fn imposter(shell: &mut Shell, arguments: &[&str]) {
match (arguments.first().copied(), arguments.get(1).copied()) {
(Some(word), None) if word.eq_ignore_ascii_case("off") => {
if let Err(reason) = shell.connection().imposter(None, b"") {
shell.complain(&format!("Error: {}", reason.message()));
}
}
(Some(index), Some(name)) => {
match shell
.connection()
.imposter(Some(index.as_bytes()), name.as_bytes())
{
Ok(Some(sql)) => shell.say(&sql),
Ok(None) => {}
Err(reason) => shell.complain(reason.message()),
}
}
_ => {
shell.complain("Usage: .imposter INDEX IMPOSTER");
shell.complain(" .imposter off");
}
}
}