use async_trait::async_trait;
use std::path::PathBuf;
use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
const MAX_DIRSTACK_SIZE: usize = 4096;
fn get_stack_size(ctx: &Context<'_>) -> usize {
ctx.variables
.get("_DIRSTACK_SIZE")
.and_then(|s| s.parse().ok())
.map(|size: usize| size.min(MAX_DIRSTACK_SIZE))
.unwrap_or(0)
}
fn get_stack_entry(ctx: &Context<'_>, idx: usize) -> Option<String> {
ctx.variables.get(&format!("_DIRSTACK_{}", idx)).cloned()
}
fn set_stack_size(ctx: &mut Context<'_>, size: usize) {
ctx.variables
.insert("_DIRSTACK_SIZE".to_string(), size.to_string());
}
fn push_stack(ctx: &mut Context<'_>, dir: &str) {
let size = get_stack_size(ctx);
ctx.variables
.insert(format!("_DIRSTACK_{}", size), dir.to_string());
set_stack_size(ctx, size + 1);
}
fn pop_stack(ctx: &mut Context<'_>) -> Option<String> {
let size = get_stack_size(ctx);
if size == 0 {
return None;
}
let entry = get_stack_entry(ctx, size - 1);
ctx.variables.remove(&format!("_DIRSTACK_{}", size - 1));
set_stack_size(ctx, size - 1);
entry
}
fn normalize_path(base: &std::path::Path, target: &str) -> PathBuf {
let path = if target.starts_with('/') {
PathBuf::from(target)
} else {
base.join(target)
};
super::resolve_path(&PathBuf::from("/"), &path.to_string_lossy())
}
pub struct Pushd;
#[async_trait]
impl Builtin for Pushd {
async fn execute(&self, mut ctx: Context<'_>) -> Result<ExecResult> {
if ctx.args.is_empty() {
let top = pop_stack(&mut ctx);
match top {
Some(dir) => {
let old_cwd = ctx.cwd.to_string_lossy().to_string();
let new_path = normalize_path(ctx.cwd, &dir);
if ctx.fs.exists(&new_path).await.unwrap_or(false) {
push_stack(&mut ctx, &old_cwd);
*ctx.cwd = new_path;
let output = format_stack(&ctx);
Ok(ExecResult::ok(format!("{}\n", output)))
} else {
push_stack(&mut ctx, &dir);
Ok(ExecResult::err(
format!("pushd: {}: No such file or directory\n", dir),
1,
))
}
}
None => Ok(ExecResult::err(
"pushd: no other directory\n".to_string(),
1,
)),
}
} else {
let target = &ctx.args[0].clone();
let new_path = normalize_path(ctx.cwd, target);
if ctx.fs.exists(&new_path).await.unwrap_or(false) {
let meta = ctx.fs.stat(&new_path).await;
if meta.map(|m| m.file_type.is_dir()).unwrap_or(false) {
let old_cwd = ctx.cwd.to_string_lossy().to_string();
push_stack(&mut ctx, &old_cwd);
*ctx.cwd = new_path;
let output = format_stack(&ctx);
Ok(ExecResult::ok(format!("{}\n", output)))
} else {
Ok(ExecResult::err(
format!("pushd: {}: Not a directory\n", target),
1,
))
}
} else {
Ok(ExecResult::err(
format!("pushd: {}: No such file or directory\n", target),
1,
))
}
}
}
}
pub struct Popd;
#[async_trait]
impl Builtin for Popd {
async fn execute(&self, mut ctx: Context<'_>) -> Result<ExecResult> {
match pop_stack(&mut ctx) {
Some(dir) => {
let new_path = normalize_path(ctx.cwd, &dir);
*ctx.cwd = new_path;
let output = format_stack(&ctx);
Ok(ExecResult::ok(format!("{}\n", output)))
}
None => Ok(ExecResult::err(
"popd: directory stack empty\n".to_string(),
1,
)),
}
}
}
pub struct Dirs;
#[async_trait]
impl Builtin for Dirs {
async fn execute(&self, mut ctx: Context<'_>) -> Result<ExecResult> {
let mut clear = false;
let mut per_line = false;
let mut verbose = false;
for arg in ctx.args.iter() {
match arg.as_str() {
"-c" => clear = true,
"-p" => per_line = true,
"-v" => {
verbose = true;
per_line = true;
}
"-l" => {} _ => {}
}
}
if clear {
let size = get_stack_size(&ctx);
for i in 0..size {
ctx.variables.remove(&format!("_DIRSTACK_{}", i));
}
set_stack_size(&mut ctx, 0);
return Ok(ExecResult::ok(String::new()));
}
let cwd = ctx.cwd.to_string_lossy().to_string();
let size = get_stack_size(&ctx);
if verbose {
let mut output = format!(" 0 {}\n", cwd);
for i in (0..size).rev() {
if let Some(dir) = get_stack_entry(&ctx, i) {
output.push_str(&format!(" {} {}\n", size - i, dir));
}
}
Ok(ExecResult::ok(output))
} else if per_line {
let mut output = format!("{}\n", cwd);
for i in (0..size).rev() {
if let Some(dir) = get_stack_entry(&ctx, i) {
output.push_str(&format!("{}\n", dir));
}
}
Ok(ExecResult::ok(output))
} else {
let output = format_stack(&ctx);
Ok(ExecResult::ok(format!("{}\n", output)))
}
}
}
fn format_stack(ctx: &Context<'_>) -> String {
let cwd = ctx.cwd.to_string_lossy().to_string();
let size = get_stack_size(ctx);
let mut parts = vec![cwd];
for i in (0..size).rev() {
if let Some(dir) = get_stack_entry(ctx, i) {
parts.push(dir);
}
}
parts.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use crate::fs::{FileSystem, InMemoryFs};
async fn setup() -> (Arc<InMemoryFs>, PathBuf, HashMap<String, String>) {
let fs = Arc::new(InMemoryFs::new());
let cwd = PathBuf::from("/home/user");
let variables = HashMap::new();
fs.mkdir(&cwd, true).await.unwrap();
fs.mkdir(Path::new("/tmp"), true).await.unwrap();
fs.mkdir(Path::new("/var"), true).await.unwrap();
(fs, cwd, variables)
}
#[tokio::test]
async fn pushd_to_directory() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Pushd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(cwd, PathBuf::from("/tmp"));
assert_eq!(variables.get("_DIRSTACK_0").unwrap(), "/home/user");
}
#[tokio::test]
async fn pushd_nonexistent_dir() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/nonexistent".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Pushd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("No such file or directory"));
assert_eq!(cwd, PathBuf::from("/home/user"));
}
#[tokio::test]
async fn pushd_file_not_dir() {
let (fs, mut cwd, mut variables) = setup().await;
fs.write_file(Path::new("/home/user/file.txt"), b"data")
.await
.unwrap();
let env = HashMap::new();
let args = vec!["file.txt".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Pushd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("Not a directory"));
}
#[tokio::test]
async fn pushd_no_args_empty_stack() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Pushd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("no other directory"));
}
#[tokio::test]
async fn pushd_no_args_swaps_top() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
assert_eq!(cwd, PathBuf::from("/tmp"));
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Pushd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(cwd, PathBuf::from("/home/user"));
}
#[tokio::test]
async fn popd_empty_stack() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Popd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains("directory stack empty"));
}
#[tokio::test]
async fn popd_after_pushd() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
assert_eq!(cwd, PathBuf::from("/tmp"));
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Popd.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(cwd, PathBuf::from("/home/user"));
}
#[tokio::test]
async fn pushd_popd_multiple() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
let args = vec!["/var".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
assert_eq!(cwd, PathBuf::from("/var"));
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Popd.execute(ctx).await.unwrap();
assert_eq!(cwd, PathBuf::from("/tmp"));
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Popd.execute(ctx).await.unwrap();
assert_eq!(cwd, PathBuf::from("/home/user"));
}
#[tokio::test]
async fn dirs_empty_stack() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("/home/user"));
}
#[tokio::test]
async fn dirs_after_pushd() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
let args: Vec<String> = vec![];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("/tmp"));
assert!(result.stdout.contains("/home/user"));
}
#[tokio::test]
async fn dirs_clear() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
let args = vec!["-c".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(get_stack_size_from_vars(&variables), 0);
}
#[tokio::test]
async fn dirs_per_line() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
let args = vec!["-p".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
let lines: Vec<&str> = result.stdout.lines().collect();
assert_eq!(lines.len(), 2);
}
#[tokio::test]
async fn dirs_verbose() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
let args = vec!["/tmp".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
Pushd.execute(ctx).await.unwrap();
let args = vec!["-v".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
assert!(result.stdout.contains(" 0 "));
assert!(result.stdout.contains(" 1 "));
}
#[tokio::test]
async fn dirs_limits_user_declared_size() {
let (fs, mut cwd, mut variables) = setup().await;
let env = HashMap::new();
variables.insert("_DIRSTACK_SIZE".to_string(), "999999999999".to_string());
variables.insert("_DIRSTACK_0".to_string(), "/tmp".to_string());
let args = vec!["-p".to_string()];
let ctx = Context::new_for_test(&args, &env, &mut variables, &mut cwd, fs.clone(), None);
let result = Dirs.execute(ctx).await.unwrap();
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "/home/user\n/tmp\n");
}
fn get_stack_size_from_vars(vars: &HashMap<String, String>) -> usize {
vars.get("_DIRSTACK_SIZE")
.and_then(|s| s.parse().ok())
.unwrap_or(0)
}
}