1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use crate::cli::PluginsCommand;
use anyhow::anyhow;
use clap::Parser;
use forc_tracing::println_warning;
use forc_util::ForcResult;
use std::{
    collections::HashMap,
    path::{Path, PathBuf},
};
use tracing::info;

forc_util::cli_examples! {
    crate::cli::Opt {
        [ List all plugins => "forc plugins" ]
        [ List all plugins with their paths => "forc plugins --paths" ]
        [ List all plugins with their descriptions => "forc plugins --describe" ]
        [ List all plugins with their paths and descriptions => "forc plugins --paths --describe" ]
    }
}

/// Find all forc plugins available via `PATH`.
///
/// Prints information about each discovered plugin.
#[derive(Debug, Parser)]
#[clap(name = "forc plugins", about = "List all forc plugins", version, after_help = help())]
pub struct Command {
    /// Prints the absolute path to each discovered plugin.
    #[clap(long = "paths", short = 'p')]
    print_full_path: bool,
    /// Prints the long description associated with each listed plugin
    #[clap(long = "describe", short = 'd')]
    describe: bool,
}

fn get_file_name(path: &Path) -> String {
    if let Some(path_str) = path.file_name().and_then(|path_str| path_str.to_str()) {
        path_str.to_owned()
    } else {
        path.display().to_string()
    }
}

pub(crate) fn exec(command: PluginsCommand) -> ForcResult<()> {
    let PluginsCommand {
        print_full_path,
        describe,
    } = command;

    let mut plugins = crate::cli::plugin::find_all()
        .map(|path| {
            get_plugin_info(path.clone(), print_full_path, describe).map(|info| (path, info))
        })
        .collect::<Result<Vec<(_, _)>, _>>()?
        .into_iter()
        .fold(HashMap::new(), |mut acc, (path, content)| {
            let bin_name = get_file_name(&path);
            acc.entry(bin_name.clone())
                .or_insert_with(|| (bin_name, vec![], content.clone()))
                .1
                .push(path);
            acc
        })
        .into_values()
        .map(|(bin_name, mut paths, content)| {
            paths.sort();
            paths.dedup();
            (bin_name, paths, content)
        })
        .collect::<Vec<_>>();
    plugins.sort_by(|a, b| a.0.cmp(&b.0));

    info!("Installed Plugins:");
    for plugin in plugins {
        info!("{}", plugin.2);
        if plugin.1.len() > 1 {
            println_warning(&format!("Multiple paths found for {}", plugin.0));
            for path in plugin.1 {
                println_warning(&format!("   {}", path.display()));
            }
        }
    }
    Ok(())
}

/// Find a plugin's description
///
/// Given a canonical plugin path, returns the description included in the `-h` opt.
/// Returns a generic description if a description cannot be found
fn parse_description_for_plugin(plugin: &Path) -> String {
    use std::process::Command;
    let default_description = "No description found for this plugin.";
    let proc = Command::new(plugin)
        .arg("-h")
        .output()
        .expect("Could not get plugin description.");

    let stdout = String::from_utf8_lossy(&proc.stdout);

    // If the plugin doesn't support a -h flag
    match stdout.split('\n').nth(1) {
        Some(x) => {
            if x.is_empty() {
                default_description.to_owned()
            } else {
                x.to_owned()
            }
        }
        None => default_description.to_owned(),
    }
}

/// # Panics
///
/// Format a given plugin's line to stdout
///
/// Formatting is based on a combination of `print_full_path` and `describe`. Will
/// panic if there is a problem retrieving a plugin's name or path
fn format_print_description(
    path: PathBuf,
    print_full_path: bool,
    describe: bool,
) -> ForcResult<String> {
    let display = if print_full_path {
        path.display().to_string()
    } else {
        get_file_name(&path)
    };

    let description = parse_description_for_plugin(&path);

    if describe {
        Ok(format!("  {display} \t\t{description}"))
    } else {
        Ok(display)
    }
}

/// # Panics
///
/// This function assumes that file names will never be empty since it is only used with
/// paths yielded from plugin::find_all(), as well as that the file names are in valid
/// unicode format since file names should be prefixed with `forc-`. Should one of these 2
/// assumptions fail, this function panics.
fn get_plugin_info(path: PathBuf, print_full_path: bool, describe: bool) -> ForcResult<String> {
    format_print_description(path, print_full_path, describe)
        .map_err(|e| anyhow!("Could not get plugin info: {}", e.as_ref()).into())
}