use async_trait::async_trait;
use std::path::PathBuf;
use super::limits::DIRSTACK_MAX_SIZE as MAX_DIRSTACK_SIZE;
use super::{Builtin, Context};
use crate::error::Result;
use crate::fs::vfs_join;
use crate::interpreter::ExecResult;
fn normalize_path(base: &std::path::Path, target: &str) -> PathBuf {
let path = if target.starts_with('/') {
PathBuf::from(target)
} else {
vfs_join(base, target)
};
super::resolve_path(&PathBuf::from("/"), &path.to_string_lossy())
}
fn polite(dir: &str, home: Option<&str>) -> String {
if let Some(home) = home.filter(|h| h.len() > 1)
&& let Some(rest) = dir.strip_prefix(home)
&& (rest.is_empty() || rest.starts_with('/'))
{
return format!("~{rest}");
}
dir.to_string()
}
fn stack_entries(ctx: &Context<'_>, long: bool) -> Vec<String> {
let home = if long {
None
} else {
ctx.variables.get("HOME").map(String::as_str)
};
let cwd = ctx.cwd.to_string_lossy();
let mut parts = vec![polite(&cwd, home)];
if let Some(shell) = ctx.shell.as_ref() {
parts.extend(shell.dir_stack.iter().rev().map(|d| polite(d, home)));
}
parts
}
fn format_stack(ctx: &Context<'_>) -> String {
stack_entries(ctx, false).join(" ")
}
fn stack_operands<'a>(
args: &'a [String],
name: &str,
usage: &str,
) -> std::result::Result<Vec<&'a String>, Box<ExecResult>> {
let mut out = Vec::new();
let mut opts = true;
for arg in args {
if opts && arg == "--" {
opts = false;
continue;
}
if opts && arg.len() > 1 && arg.starts_with(['-', '+']) {
if arg == "-n" {
continue;
}
if !arg[1..].bytes().all(|b| b.is_ascii_digit()) {
return Err(Box::new(ExecResult::err(
format!("{name}: {arg}: invalid number\n{name}: usage: {usage}\n"),
2,
)));
}
}
opts = false;
out.push(arg);
}
Ok(out)
}
pub struct Pushd;
#[async_trait]
impl Builtin for Pushd {
async fn execute(&self, mut ctx: Context<'_>) -> Result<ExecResult> {
let operands = match stack_operands(ctx.args, "pushd", "pushd [-n] [+N | -N | dir]") {
Ok(o) => o,
Err(e) => return Ok(*e),
};
if operands.len() > 1 {
return Ok(ExecResult::err(
"pushd: too many arguments\n".to_string(),
1,
));
}
if operands.is_empty() {
let Some(top) = ctx.shell.as_ref().and_then(|s| s.dir_stack.last()).cloned() else {
return Ok(ExecResult::err(
"pushd: no other directory\n".to_string(),
1,
));
};
let new_path = normalize_path(ctx.cwd, &top);
if !ctx.fs.exists(&new_path).await.unwrap_or(false) {
return Ok(ExecResult::err(
format!("pushd: {}: No such file or directory\n", top),
1,
));
}
let old_cwd = ctx.cwd.to_string_lossy().to_string();
if let Some(shell) = ctx.shell.as_mut() {
shell.dir_stack.pop();
shell.dir_stack.push(old_cwd);
}
*ctx.cwd = new_path;
Ok(ExecResult::ok(format!("{}\n", format_stack(&ctx))))
} else {
let target = operands[0].clone();
let new_path = normalize_path(ctx.cwd, &target);
match ctx.fs.stat(&new_path).await {
Ok(meta) if meta.file_type.is_dir() => {}
Ok(_) => {
return Ok(ExecResult::err(
format!("pushd: {}: Not a directory\n", target),
1,
));
}
Err(_) => {
return Ok(ExecResult::err(
format!("pushd: {}: No such file or directory\n", target),
1,
));
}
}
let old_cwd = ctx.cwd.to_string_lossy().to_string();
if let Some(shell) = ctx.shell.as_mut() {
if shell.dir_stack.len() >= MAX_DIRSTACK_SIZE {
return Ok(ExecResult::err(
"pushd: directory stack full\n".to_string(),
1,
));
}
shell.dir_stack.push(old_cwd);
}
*ctx.cwd = new_path;
Ok(ExecResult::ok(format!("{}\n", format_stack(&ctx))))
}
}
}
pub struct Popd;
#[async_trait]
impl Builtin for Popd {
async fn execute(&self, mut ctx: Context<'_>) -> Result<ExecResult> {
let operands = match stack_operands(ctx.args, "popd", "popd [-n] [+N | -N]") {
Ok(o) => o,
Err(e) => return Ok(*e),
};
if let Some(arg) = operands.first() {
return Ok(ExecResult::err(
format!("popd: {arg}: invalid argument\npopd: usage: popd [-n] [+N | -N]\n"),
2,
));
}
let Some(dir) = ctx.shell.as_mut().and_then(|s| s.dir_stack.pop()) else {
return Ok(ExecResult::err(
"popd: directory stack empty\n".to_string(),
1,
));
};
*ctx.cwd = normalize_path(ctx.cwd, &dir);
Ok(ExecResult::ok(format!("{}\n", format_stack(&ctx))))
}
}
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;
let mut long = 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" => long = true,
a if !a.starts_with(['-', '+']) => {
return Ok(ExecResult::err(
format!("dirs: {a}: invalid option\ndirs: usage: dirs [-clpv] [+N] [-N]\n"),
2,
));
}
_ => {}
}
}
if clear {
if let Some(shell) = ctx.shell.as_mut() {
shell.dir_stack.clear();
}
return Ok(ExecResult::ok(String::new()));
}
let entries = stack_entries(&ctx, long);
let output = if verbose {
entries
.iter()
.enumerate()
.map(|(n, dir)| format!(" {n} {dir}\n"))
.collect()
} else if per_line {
entries.iter().map(|dir| format!("{dir}\n")).collect()
} else {
format!("{}\n", entries.join(" "))
};
Ok(ExecResult::ok(output))
}
}