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:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user