shuck-cli 0.0.16

A fast shell script linter
Documentation
use std::collections::BTreeSet;
use std::fs;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::path::PathBuf;

use anyhow::{Context, Result};
use shuck_cache::{legacy_cache_dir, read_project_root_from_cache_file};

use crate::ExitStatus;
use crate::args::CleanCommand;
use crate::cache::resolve_cache_root;
use crate::config::{ConfigArguments, resolve_project_root_for_input};

pub(crate) fn clean(
    args: CleanCommand,
    config_arguments: &ConfigArguments,
    cache_dir: Option<&Path>,
) -> Result<ExitStatus> {
    let cwd = std::env::current_dir()?;
    let cache_root = resolve_cache_root(&cwd, cache_dir)?;
    let inputs = if args.paths.is_empty() {
        vec![cwd.clone()]
    } else {
        args.paths
            .iter()
            .map(|path| {
                if path.is_absolute() {
                    path.clone()
                } else {
                    cwd.join(path)
                }
            })
            .collect::<Vec<PathBuf>>()
    };

    let mut roots = BTreeSet::new();
    let mut canonical_roots = BTreeSet::new();
    for input in inputs {
        let root = resolve_project_root_for_input(&input, config_arguments.use_config_roots())?;
        let canonical_root =
            fs::canonicalize(&root).with_context(|| format!("canonicalize {}", root.display()))?;
        canonical_roots.insert(canonical_root);
        roots.insert(root);
    }

    remove_shared_cache_entries(&cache_root, &canonical_roots)?;

    for root in roots {
        match fs::remove_dir_all(legacy_cache_dir(&root)) {
            Ok(_) => {}
            Err(err) if err.kind() == io::ErrorKind::NotFound => {}
            Err(err) => return Err(err.into()),
        }
    }

    let mut stdout = BufWriter::new(io::stdout().lock());
    writeln!(stdout, "cache cleared")?;

    Ok(ExitStatus::Success)
}

fn remove_shared_cache_entries(
    cache_root: &Path,
    canonical_roots: &BTreeSet<PathBuf>,
) -> Result<()> {
    let version_dirs = match fs::read_dir(cache_root) {
        Ok(entries) => entries,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(err) => return Err(err.into()),
    };

    for version_dir in version_dirs {
        let version_dir = version_dir?;
        let version_dir_path = version_dir.path();
        if !version_dir.file_type()?.is_dir() {
            continue;
        }

        for entry in fs::read_dir(&version_dir_path)? {
            let entry = entry?;
            let path = entry.path();
            if !entry.file_type()?.is_file() {
                continue;
            }

            let Some(project_root) = read_project_root_from_cache_file(&path)? else {
                continue;
            };
            if canonical_roots.contains(&project_root) {
                fs::remove_file(&path)?;
            }
        }

        remove_dir_if_empty(&version_dir_path)?;
    }

    remove_dir_if_empty(cache_root)?;
    Ok(())
}

fn remove_dir_if_empty(path: &Path) -> io::Result<()> {
    let mut entries = match fs::read_dir(path) {
        Ok(entries) => entries,
        Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
        Err(err) => return Err(err),
    };

    if entries.next().is_none() {
        fs::remove_dir(path)?;
    }

    Ok(())
}