Quote arguments when executing in a shell (#118)

* Quote arguments when executing in a shell

Fixes #107

* Parse quotes in `tmux_arguments`

This makes it possible to encode spaces in arguments. Maybe the config
value should be an array instead?

* Print error causes

Co-authored-by: Thomas Schönauer <37108907+DottoDev@users.noreply.github.com>
This commit is contained in:
Rebecca Turner
2022-11-03 12:46:43 -04:00
committed by GitHub
parent ff66611ec0
commit 55ba2d30c1
8 changed files with 37 additions and 19 deletions

7
Cargo.lock generated
View File

@@ -1606,6 +1606,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "2.1.2" version = "2.1.2"
@@ -1943,6 +1949,7 @@ dependencies = [
"self_update", "self_update",
"semver", "semver",
"serde", "serde",
"shell-words",
"shellexpand", "shellexpand",
"strum 0.24.1", "strum 0.24.1",
"sys-info", "sys-info",

View File

@@ -44,6 +44,7 @@ futures = "0.3"
regex = "1.5" regex = "1.5"
sys-info = "0.9" sys-info = "0.9"
semver = "1.0" semver = "1.0"
shell-words = "1.1.0"
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
notify-rust = "4.5" notify-rust = "4.5"

View File

@@ -4,7 +4,6 @@ use std::fs::write;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::{env, fs}; use std::{env, fs};
use anyhow::Result; use anyhow::Result;
use clap::{ArgEnum, Parser}; use clap::{ArgEnum, Parser};
use directories::BaseDirs; use directories::BaseDirs;
@@ -626,8 +625,16 @@ impl Config {
} }
/// Extra Tmux arguments /// Extra Tmux arguments
pub fn tmux_arguments(&self) -> &Option<String> { pub fn tmux_arguments(&self) -> anyhow::Result<Vec<String>> {
&self.config_file.tmux_arguments let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default();
shell_words::split(args)
// The only time the parse failed is in case of a missing close quote.
// The error message looks like this:
// Error: Failed to parse `tmux_arguments`: `'foo`
//
// Caused by:
// missing closing quote
.with_context(|| format!("Failed to parse `tmux_arguments`: `{args}`"))
} }
/// Prompt for a key before exiting /// Prompt for a key before exiting

View File

@@ -194,11 +194,12 @@ impl DryCommand {
print!( print!(
"Dry running: {} {}", "Dry running: {} {}",
self.program.to_string_lossy(), self.program.to_string_lossy(),
shell_words::join(
self.args self.args
.iter() .iter()
.map(|a| String::from(a.to_string_lossy())) .map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(" ") )
); );
match &self.directory { match &self.directory {
Some(dir) => println!(" in {}", dir.to_string_lossy()), Some(dir) => println!(" in {}", dir.to_string_lossy()),

View File

@@ -79,7 +79,7 @@ fn run() -> Result<()> {
if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() { if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() {
#[cfg(unix)] #[cfg(unix)]
{ {
tmux::run_in_tmux(config.tmux_arguments()); tmux::run_in_tmux(config.tmux_arguments()?);
} }
} }
@@ -524,7 +524,10 @@ fn main() {
.is_some()); .is_some());
if !skip_print { if !skip_print {
println!("Error: {}", error); // The `Debug` implementation of `anyhow::Result` prints a multi-line
// error message that includes all the 'causes' added with
// `.with_context(...)` calls.
println!("Error: {:?}", error);
} }
exit(1); exit(1);
} }

View File

@@ -79,6 +79,7 @@ impl Powershell {
println!("Updating modules..."); println!("Updating modules...");
ctx.run_type() ctx.run_type()
.execute(powershell) .execute(powershell)
// This probably doesn't need `shell_words::join`.
.args(["-NoProfile", "-Command", &cmd.join(" ")]) .args(["-NoProfile", "-Command", &cmd.join(" ")])
.check_run() .check_run()
} }

View File

@@ -24,7 +24,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
#[cfg(unix)] #[cfg(unix)]
{ {
prepare_async_ssh_command(&mut args); prepare_async_ssh_command(&mut args);
crate::tmux::run_command(ctx, &args.join(" "))?; crate::tmux::run_command(ctx, &shell_words::join(args))?;
Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into()) Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into())
} }

View File

@@ -29,12 +29,10 @@ struct Tmux {
} }
impl Tmux { impl Tmux {
fn new(args: &Option<String>) -> Self { fn new(args: Vec<String>) -> Self {
Self { Self {
tmux: which("tmux").expect("Could not find tmux"), tmux: which("tmux").expect("Could not find tmux"),
args: args args: if args.is_empty() { None } else { Some(args) },
.as_ref()
.map(|args| args.split_whitespace().map(String::from).collect()),
} }
} }
@@ -75,7 +73,7 @@ impl Tmux {
} }
} }
pub fn run_in_tmux(args: &Option<String>) -> ! { pub fn run_in_tmux(args: Vec<String>) -> ! {
let command = { let command = {
let mut command = vec![ let mut command = vec![
String::from("env"), String::from("env"),
@@ -83,10 +81,10 @@ pub fn run_in_tmux(args: &Option<String>) -> ! {
String::from("TOPGRADE_INSIDE_TMUX=1"), String::from("TOPGRADE_INSIDE_TMUX=1"),
]; ];
command.extend(env::args()); command.extend(env::args());
command.join(" ") shell_words::join(command)
}; };
let tmux = Tmux::new(args); let tmux = Tmux::new(args.clone());
if !tmux.has_session("topgrade").expect("Error detecting a tmux session") { if !tmux.has_session("topgrade").expect("Error detecting a tmux session") {
tmux.new_session("topgrade").expect("Error creating a tmux session"); tmux.new_session("topgrade").expect("Error creating a tmux session");
@@ -108,7 +106,7 @@ pub fn run_in_tmux(args: &Option<String>) -> ! {
} }
pub fn run_command(ctx: &ExecutionContext, command: &str) -> Result<()> { pub fn run_command(ctx: &ExecutionContext, command: &str) -> Result<()> {
Tmux::new(ctx.config().tmux_arguments()) Tmux::new(ctx.config().tmux_arguments()?)
.build() .build()
.args(["new-window", "-a", "-t", "topgrade:1", command]) .args(["new-window", "-a", "-t", "topgrade:1", command])
.env_remove("TMUX") .env_remove("TMUX")