cnf 0.6.0

Distribution-agnostic 'command not found'-handler
Documentation
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: (C) 2023 Andreas Hartmann <hartan@7x.de>
// This file is part of cnf, available at <https://gitlab.com/hartang/rust/cnf>

//! # Command aliases
//!
//! Aliases can be thought of as a way to skip interactions with the application TUI for repeated
//! use of a specific command. In the UI, there is an option to create an alias for any executable
//! command. When selected, `cnf` writes a shell-script named after the command `cnf` is supposed
//! to search into a special directory. This script will then perform the same steps as the
//! `execute` action for the selected entry.
//!
//! When the application hooks are set up, this special directory is automatically appended to the
//! system `$PATH` and aliases are immediately available as CLI commands. This is a convenient
//! method to call e.g. `podman` from inside a toolbx/distrobox container.
//!
//!
//! ## Additional comfort
//!
//! Once aliases are set up, one can extend their shells completions with the command completions
//! for the aliased commands taken from the environment the command is run in. The following
//! snippet adds shell completions for commands installed on the host to `zsh` running in a toolbx
//! container:
//!
//! ```shell
//! # add this to your `~/.zshrc`, replacing/extending any previous call to `compinit`!
//! HOST_COMPLETIONS="/run/host/usr/share/zsh/site-functions"
//! [[ -d "$HOST_COMPLETIONS" ]] && fpath+="$HOST_COMPLETIONS"
//! compinit -u
//! ```
//!
//! This works because, by default, toolbx containers map the hosts entire filesystem beneath
//! `/run/host`. The `zsh` completion files for system utilities are found in
//! `/usr/share/zsh/site-functions`, which is added to the zsh `fpath` in case the directory exists
//! (which is only the case inside a toolbx container, not on the host). Then we call `compinit` to
//! initialize the zsh command completions, passing the `-u` option so it ignores
//! permission/ownership issues on the host completions folder (this bit seems unavoidable).
//!
//! Once this is done, command aliases for system utilities installed on the host should have full
//! tab completion (e.g. container names for `podman`, subcommands/network names for `nmcli` and
//! more)!

use anyhow::{Context, Result};
use cnf_lib::util::CommandLine;
use serde_derive::{Deserialize, Serialize};
use std::{io::Write, os::unix::fs::PermissionsExt};

/// Definition for a single command alias.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct Alias {
    /// Source environment this alias is valid in.
    pub source_env: String,
    /// Target environment this alias is valid for.
    pub target_env: String,
    /// Command this alias applies to.
    pub command: String,
    /// The real commandline to alias `command` to.
    pub alias: CommandLine,
}

impl Alias {
    /// Turn this alias into a shell wrapper, interacting with `cnf` through the command line
    /// `--alias-*` flags.
    pub(crate) fn to_shell_wrapper(&self) -> Result<()> {
        let mut alias_path = crate::directories::get().aliases();
        // get basename of command, else the `alias_path` below is overwritten with `self.command`
        // if it is an absolute path!
        let command_name = std::path::Path::new(&self.command)
            .file_name()
            .unwrap_or(std::ffi::OsStr::new(&self.command));
        alias_path.push(command_name);
        let alias_path_str = alias_path.display().to_string();
        tracing::info!(alias = ?self, "writing new alias to '{}'", alias_path_str);

        std::fs::File::create(alias_path)
            .with_context(|| format!("failed to write alias script to '{}'", alias_path_str))
            .and_then(|mut fd| {
                writeln!(fd, "#!/bin/sh")?;
                writeln!(
                    fd,
                    "${{{cnf_environment_exe}:-\"{cnf_current_exe}\"}} \\",
                    cnf_environment_exe = crate::Env::AliasExecutable,
                    cnf_current_exe = std::env::current_exe()
                        .context("cannot determine current execution environment")?
                        .display()
                )?;
                writeln!(
                    fd,
                    "    --alias-target-env {} \\",
                    shlex::quote(&self.target_env)
                )?;
                if self.alias.get_privileged() {
                    writeln!(fd, "    --alias-privileged \\")?;
                }
                if self.alias.get_interactive() {
                    writeln!(fd, "    --alias-interactive \\")?;
                }
                writeln!(fd, "    {} \\", shlex::quote(&self.alias.command()))?;
                for arg in self.alias.args() {
                    writeln!(fd, "    {} \\", shlex::quote(arg))?;
                }
                writeln!(fd, "    \"$@\"")?;
                Ok(fd)
            })
            .context("failed to write contents of alias script")
            .and_then(|fd| {
                let mut permissions = fd
                    .metadata()
                    .context("cannot determine metadata of alias script")?
                    .permissions();
                permissions.set_mode(0o755);
                fd.set_permissions(permissions)?;
                Ok(())
            })
            .context("failed to mark alias script as executable")
    }
}