diff --git a/CHANGELOG.md b/CHANGELOG.md index ca5a9e48f4..6f69f5d8a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). * Templates now support the `==` logical operator for `Boolean`, `Integer`, and `String` types. +* New command `jj util exec` that can be used for arbitrary aliases. + ### Fixed bugs ## [0.23.0] - 2024-11-06 diff --git a/cli/src/commands/util/exec.rs b/cli/src/commands/util/exec.rs new file mode 100644 index 0000000000..bc56cdef0c --- /dev/null +++ b/cli/src/commands/util/exec.rs @@ -0,0 +1,85 @@ +// Copyright 2024 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::cli_util::CommandHelper; +use crate::command_error::user_error; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Execute an external command via jj +/// +/// This is useful for arbitrary aliases. +/// +/// !! WARNING !! +/// +/// The following technique just provides a convenient syntax for running +/// arbitrary code on your system. Using it irresponsibly may cause damage +/// ranging from breaking the behavior of `jj undo` to wiping your file system. +/// Exercise the same amount of caution while writing these aliases as you would +/// when typing commands into the terminal! +/// +/// Let's assume you have a script called "my-jj-script" in you $PATH and you +/// would like to execute it as "jj my-script". You would add the following line +/// to your configuration file to achieve that: +/// +/// ```toml +/// [aliases] +/// my-script = ["util", "exec", "--", "my-jj-script"] +/// # ^^^^ +/// # This makes sure that flags are passed to your script instead of parsed by jj. +/// ``` +/// +/// If you don't want to manage your script as a separate file, you can even +/// inline it into your config file: +/// +/// ```toml +/// [aliases] +/// my-inline-script = ["util", "exec", "--", "bash", "-c", """ +/// #!/usr/bin/env bash +/// set -euo pipefail +/// echo "Look Ma, everything in one file!" +/// echo "args: $@" +/// """, ""] +/// # ^^ +/// # This last empty string will become "$0" in bash, so your actual arguments +/// # are all included in "$@" and start at "$1" as expected. +/// ``` +#[derive(clap::Args, Clone, Debug)] +#[command(verbatim_doc_comment)] +pub(crate) struct UtilExecArgs { + /// External command to execute + command: String, + /// Arguments to pass to the external command + args: Vec, +} + +pub fn cmd_util_exec( + _ui: &mut Ui, + _command: &CommandHelper, + args: &UtilExecArgs, +) -> Result<(), CommandError> { + let status = std::process::Command::new(&args.command) + .args(&args.args) + .status() + .map_err(user_error)?; + if !status.success() { + let error_msg = if let Some(exit_code) = status.code() { + format!("External command exited with {exit_code}") + } else { + "External command was terminated by signal".into() + }; + return Err(user_error(error_msg)); + } + Ok(()) +} diff --git a/cli/src/commands/util/mod.rs b/cli/src/commands/util/mod.rs index 2b8e63cd0e..2248994e1b 100644 --- a/cli/src/commands/util/mod.rs +++ b/cli/src/commands/util/mod.rs @@ -14,6 +14,7 @@ mod completion; mod config_schema; +mod exec; mod gc; mod mangen; mod markdown_help; @@ -25,6 +26,8 @@ use self::completion::cmd_util_completion; use self::completion::UtilCompletionArgs; use self::config_schema::cmd_util_config_schema; use self::config_schema::UtilConfigSchemaArgs; +use self::exec::cmd_util_exec; +use self::exec::UtilExecArgs; use self::gc::cmd_util_gc; use self::gc::UtilGcArgs; use self::mangen::cmd_util_mangen; @@ -40,6 +43,7 @@ use crate::ui::Ui; pub(crate) enum UtilCommand { Completion(UtilCompletionArgs), ConfigSchema(UtilConfigSchemaArgs), + Exec(UtilExecArgs), Gc(UtilGcArgs), Mangen(UtilMangenArgs), MarkdownHelp(UtilMarkdownHelp), @@ -54,6 +58,7 @@ pub(crate) fn cmd_util( match subcommand { UtilCommand::Completion(args) => cmd_util_completion(ui, command, args), UtilCommand::ConfigSchema(args) => cmd_util_config_schema(ui, command, args), + UtilCommand::Exec(args) => cmd_util_exec(ui, command, args), UtilCommand::Gc(args) => cmd_util_gc(ui, command, args), UtilCommand::Mangen(args) => cmd_util_mangen(ui, command, args), UtilCommand::MarkdownHelp(args) => cmd_util_markdown_help(ui, command, args), diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 0cd8d6e303..1bf068c28f 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -93,6 +93,7 @@ This document contains the help content for the `jj` command-line program. * [`jj util`↴](#jj-util) * [`jj util completion`↴](#jj-util-completion) * [`jj util config-schema`↴](#jj-util-config-schema) +* [`jj util exec`↴](#jj-util-exec) * [`jj util gc`↴](#jj-util-gc) * [`jj util mangen`↴](#jj-util-mangen) * [`jj util markdown-help`↴](#jj-util-markdown-help) @@ -2116,6 +2117,7 @@ Infrequently used commands such as for generating shell completions * `completion` — Print a command-line-completion script * `config-schema` — Print the JSON schema for the jj TOML config format +* `exec` — Execute an external command via jj * `gc` — Run backend-dependent garbage collection * `mangen` — Print a ROFF (manpage) * `markdown-help` — Print the CLI help for all subcommands in Markdown @@ -2161,6 +2163,56 @@ Print the JSON schema for the jj TOML config format +## `jj util exec` + +Execute an external command via jj + +This is useful for arbitrary aliases. + +!! WARNING !! + +The following technique just provides a convenient syntax for running +arbitrary code on your system. Using it irresponsibly may cause damage +ranging from breaking the behavior of `jj undo` to wiping your file system. +Exercise the same amount of caution while writing these aliases as you would +when typing commands into the terminal! + +Let's assume you have a script called "my-jj-script" in you $PATH and you +would like to execute it as "jj my-script". You would add the following line +to your configuration file to achieve that: + +```toml +[aliases] +my-script = ["util", "exec", "--", "my-jj-script"] +# ^^^^ +# This makes sure that flags are passed to your script instead of parsed by jj. +``` + +If you don't want to manage your script as a separate file, you can even +inline it into your config file: + +```toml +[aliases] +my-inline-script = ["util", "exec", "--", "bash", "-c", """ +#!/usr/bin/env bash +set -euo pipefail +echo "Look Ma, everything in one file!" +echo "args: $@" +""", ""] +# ^^ +# This last empty string will become "$0" in bash, so your actual arguments +# are all included in "$@" and start at "$1" as expected. +``` + +**Usage:** `jj util exec [ARGS]...` + +###### **Arguments:** + +* `` — External command to execute +* `` — Arguments to pass to the external command + + + ## `jj util gc` Run backend-dependent garbage collection diff --git a/cli/tests/test_util_command.rs b/cli/tests/test_util_command.rs index bd019b2f21..25277eedf4 100644 --- a/cli/tests/test_util_command.rs +++ b/cli/tests/test_util_command.rs @@ -112,3 +112,33 @@ fn test_shell_completions() { test("nushell"); test("zsh"); } + +#[test] +fn test_util_exec() { + let test_env = TestEnvironment::default(); + let formatter_path = assert_cmd::cargo::cargo_bin("fake-formatter"); + let (out, err) = test_env.jj_cmd_ok( + test_env.env_root(), + &[ + "util", + "exec", + "--", + formatter_path.to_str().unwrap(), + "--append", + "hello", + ], + ); + insta::assert_snapshot!(out, @"hello"); + // Ensures only stdout contains text + assert!(err.is_empty()); +} + +#[test] +fn test_util_exec_fail() { + let test_env = TestEnvironment::default(); + let err = test_env.jj_cmd_failure( + test_env.env_root(), + &["util", "exec", "--", "missing-program"], + ); + assert!(!err.is_empty()); +} diff --git a/docs/config.md b/docs/config.md index be1ca87eee..ca829f1e7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -528,6 +528,38 @@ You can define aliases for commands, including their arguments. For example: aliases.l = ["log", "-r", "(main..@):: | (main..@)-"] ``` +This alias syntax can only run a single jj command. However, you may want to +execute multiple jj commands with a single alias, or run arbitrary scripts that +complement your version control workflow. This can be done, but be aware of the +danger: + +!!! warning + + The following technique just provides a convenient syntax for running + arbitrary code on your system. Using it irresponsibly may cause damage + ranging from breaking the behavior of `jj undo` to wiping your file system. + Exercise the same amount of caution while writing these aliases as you would + when typing commands into the terminal! + +The command `jj util exec` will simply run any command you pass to it as an +argument. Additional arguments are passed through. Here are some examples: + +```toml +[aliases] +my-script = ["util", "exec", "--", "my-jj-script"] +# ^^^^ +# This makes sure that flags are passed to your script instead of parsed by jj. +my-inline-script = ["util", "exec", "--", "bash", "-c", """ +#!/usr/bin/env bash +set -euo pipefail +echo "Look Ma, everything in one file!" +echo "args: $@" +""", ""] +# ^^ +# This last empty string will become "$0" in bash, so your actual arguments +# are all included in "$@" and start at "$1" as expected. +``` + ## Editor The default editor is set via `ui.editor`, though there are several places to