/src/main.rs

https://github.com/Manishearth/oreutils · Rust · 187 lines · 171 code · 16 blank · 0 comment · 18 complexity · 808b2007e8f47855459e71e6bf48f101 MD5 · raw file

  1. use regex::Regex;
  2. use semver::Version;
  3. use std::process::Command;
  4. use structopt::StructOpt;
  5. mod fetch;
  6. #[derive(Debug, StructOpt)]
  7. #[structopt(
  8. name = "oreutils",
  9. about = "Installation manager for various CLI utilities reimagined in Rust",
  10. rename_all = "kebab-case"
  11. )]
  12. enum Opt {
  13. #[structopt(about = "Install the basic utilities: ripgrep, exa, bat, fd")]
  14. Install {
  15. #[structopt(help = "Specific tool to install. Omit to install all.")]
  16. tool: Option<String>,
  17. },
  18. #[structopt(
  19. about = "Upgrade any installed tools. Use `oreutils install` to install missing ones."
  20. )]
  21. Upgrade {
  22. #[structopt(help = "Specific tool to upgrade. Omit to upgrade all.")]
  23. tool: Option<String>,
  24. },
  25. #[structopt(about = "Uninstall all oreutils tools")]
  26. Uninstall,
  27. }
  28. #[derive(Clone, Copy)]
  29. struct Tool {
  30. name: &'static str,
  31. package: &'static str,
  32. cli: &'static str,
  33. }
  34. impl Tool {
  35. fn equals(&self, other: &str) -> bool {
  36. self.name == other || self.package == other || self.cli == other
  37. }
  38. }
  39. const TOOLS: &[Tool] = &[
  40. Tool {
  41. name: "ripgrep",
  42. package: "ripgrep",
  43. cli: "rg",
  44. },
  45. Tool {
  46. name: "exa",
  47. package: "exa",
  48. cli: "exa",
  49. },
  50. Tool {
  51. name: "bat",
  52. package: "bat",
  53. cli: "bat",
  54. },
  55. Tool {
  56. name: "fd",
  57. package: "fd-find",
  58. cli: "fd",
  59. },
  60. Tool {
  61. name: "sd",
  62. package: "sd",
  63. cli: "sd",
  64. }
  65. ];
  66. fn main() {
  67. let opt = Opt::from_args();
  68. match opt {
  69. Opt::Install {tool} => install(tool),
  70. Opt::Upgrade {tool} => upgrade(tool),
  71. Opt::Uninstall => uninstall(),
  72. }
  73. }
  74. fn install(tool: Option<String>) {
  75. for_each_tool(tool, |tool| {
  76. let exists = which::which(tool.cli);
  77. if exists.is_ok() {
  78. println!(
  79. "Tool {:?} already installed, use `oreutils upgrade` to upgrade",
  80. tool.name
  81. );
  82. return;
  83. }
  84. cargo_install(tool.package, false);
  85. });
  86. }
  87. fn for_each_tool<F: Fn(&Tool)>(tool: Option<String>, f: F) {
  88. if let Some(tool) = tool {
  89. for tool in TOOLS.iter().filter(|x| x.equals(&tool)) {
  90. f(tool)
  91. }
  92. } else {
  93. for tool in TOOLS.iter() {
  94. f(tool)
  95. }
  96. };
  97. }
  98. fn upgrade(tool: Option<String>) {
  99. for_each_tool(tool, |tool| {
  100. let res = upgrade_tool(tool);
  101. match res {
  102. Ok(vers) => println!("Tool {} updated to version {}", tool.name, vers),
  103. Err(Error::NotFound) => println!(
  104. "Tool {} not installed, use `oreutils install` to install",
  105. tool.name
  106. ),
  107. Err(Error::VersionBroken(None)) => {
  108. println!("`{} --version` didn't produce expected output", tool.cli)
  109. }
  110. Err(Error::VersionBroken(Some(v))) => println!(
  111. "`{} --version` didn't produce expected output: could not parse {}",
  112. tool.cli, v
  113. ),
  114. Err(Error::AlreadyUpdated(v)) => println!("Tool {} is already up to date at version {}", tool.name, v),
  115. Err(Error::CratesFetchError(e)) => println!(
  116. "Failed to fetch information for crate {} from crates.io: {}",
  117. tool.name, e
  118. ),
  119. }
  120. });
  121. }
  122. enum Error {
  123. NotFound,
  124. VersionBroken(Option<String>),
  125. CratesFetchError(fetch::FetchError),
  126. AlreadyUpdated(Version)
  127. }
  128. fn upgrade_tool(tool: &Tool) -> Result<Version, Error> {
  129. let output = Command::new(tool.cli)
  130. .args(&["--version"])
  131. .output()
  132. .map_err(|_| Error::NotFound)?;
  133. let output = String::from_utf8(output.stdout).map_err(|_| Error::VersionBroken(None))?;
  134. let output = output.lines().next().ok_or(Error::VersionBroken(None))?;
  135. let re = Regex::new(r"\d+\.\d+\.\d+").unwrap();
  136. let vers = re
  137. .find(output)
  138. .ok_or(Error::VersionBroken(Some(output.into())))?;
  139. let vers = vers.as_str();
  140. let vers = Version::parse(vers).map_err(|_| Error::VersionBroken(Some(vers.into())))?;
  141. let latest_vers =
  142. fetch::get_latest_version(tool.package).map_err(|e| Error::CratesFetchError(e))?;
  143. if vers < latest_vers {
  144. cargo_install(tool.package, true);
  145. Ok(latest_vers)
  146. } else {
  147. Err(Error::AlreadyUpdated(vers))
  148. }
  149. }
  150. fn uninstall() {
  151. unimplemented!()
  152. }
  153. fn cargo_install(pkg: &str, force: bool) {
  154. let mut cmd = Command::new("cargo");
  155. if force {
  156. cmd.args(&["install", "-f", pkg]);
  157. } else {
  158. cmd.args(&["install", pkg]);
  159. }
  160. cmd.env("RUSTFLAGS", "-Ctarget-cpu=native");
  161. let res = cmd.spawn();
  162. match res {
  163. Ok(mut child) => {
  164. let status = child.wait().expect("Command wasn't running");
  165. if !status.success() {
  166. eprintln!("Installing {:?} failed", pkg);
  167. }
  168. }
  169. Err(_) => eprintln!("Cargo didn't start"),
  170. }
  171. }