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

View File

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

View File

@@ -4,7 +4,6 @@ use std::fs::write;
use std::path::PathBuf;
use std::process::Command;
use std::{env, fs};
use anyhow::Result;
use clap::{ArgEnum, Parser};
use directories::BaseDirs;
@@ -626,8 +625,16 @@ impl Config {
}
/// Extra Tmux arguments
pub fn tmux_arguments(&self) -> &Option<String> {
&self.config_file.tmux_arguments
pub fn tmux_arguments(&self) -> anyhow::Result<Vec<String>> {
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

View File

@@ -194,11 +194,12 @@ impl DryCommand {
print!(
"Dry running: {} {}",
self.program.to_string_lossy(),
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
.join(" ")
shell_words::join(
self.args
.iter()
.map(|a| String::from(a.to_string_lossy()))
.collect::<Vec<String>>()
)
);
match &self.directory {
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() {
#[cfg(unix)]
{
tmux::run_in_tmux(config.tmux_arguments());
tmux::run_in_tmux(config.tmux_arguments()?);
}
}
@@ -524,7 +524,10 @@ fn main() {
.is_some());
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);
}

View File

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

View File

@@ -24,7 +24,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> {
#[cfg(unix)]
{
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())
}

View File

@@ -29,12 +29,10 @@ struct Tmux {
}
impl Tmux {
fn new(args: &Option<String>) -> Self {
fn new(args: Vec<String>) -> Self {
Self {
tmux: which("tmux").expect("Could not find tmux"),
args: args
.as_ref()
.map(|args| args.split_whitespace().map(String::from).collect()),
args: if args.is_empty() { None } else { Some(args) },
}
}
@@ -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 mut command = vec![
String::from("env"),
@@ -83,10 +81,10 @@ pub fn run_in_tmux(args: &Option<String>) -> ! {
String::from("TOPGRADE_INSIDE_TMUX=1"),
];
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") {
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<()> {
Tmux::new(ctx.config().tmux_arguments())
Tmux::new(ctx.config().tmux_arguments()?)
.build()
.args(["new-window", "-a", "-t", "topgrade:1", command])
.env_remove("TMUX")