diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c47ccf157..5bf382fa19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). for some keywords (e.g. `jj help -k revsets`). To see a list of the available keywords you can do `jj help --help`. +* New command `jj util exec` that can be used for arbitrary aliases. + ### Fixed bugs * Error on `trunk()` revset resolution is now handled gracefully. diff --git a/cli/src/commands/util.rs b/cli/src/commands/util.rs index e60e1cd941..4b9fde1779 100644 --- a/cli/src/commands/util.rs +++ b/cli/src/commands/util.rs @@ -25,6 +25,7 @@ use tracing::instrument; use crate::cli_util::CommandHelper; use crate::command_error::user_error; use crate::command_error::CommandError; +use crate::command_error::CommandErrorKind; use crate::ui::Ui; /// Infrequently used commands such as for generating shell completions @@ -35,6 +36,7 @@ pub(crate) enum UtilCommand { Mangen(UtilMangenArgs), MarkdownHelp(UtilMarkdownHelp), ConfigSchema(UtilConfigSchemaArgs), + Exec(UtilExecArgs), } // Using an explicit `doc` attribute prevents rustfmt from mangling the list @@ -99,6 +101,44 @@ pub(crate) struct UtilMarkdownHelp {} #[derive(clap::Args, Clone, Debug)] pub(crate) struct UtilConfigSchemaArgs {} +/// Execute an external command via jj +/// +/// This is useful for arbitrary aliases. For example, 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 have to manage your script as a separate file, you can +/// even inline a script 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, +} + /// Available shell completions #[derive(clap::ValueEnum, Clone, Copy, Debug, Eq, Hash, PartialEq)] enum ShellCompletion { @@ -122,6 +162,7 @@ pub(crate) fn cmd_util( UtilCommand::Mangen(args) => cmd_util_mangen(ui, command, args), UtilCommand::MarkdownHelp(args) => cmd_util_markdownhelp(ui, command, args), UtilCommand::ConfigSchema(args) => cmd_util_config_schema(ui, command, args), + UtilCommand::Exec(args) => cmd_util_exec(args), } } @@ -230,6 +271,24 @@ fn cmd_util_config_schema( Ok(()) } +fn cmd_util_exec(args: &UtilExecArgs) -> Result<(), CommandError> { + let status = std::process::Command::new(&args.command) + .args(&args.args) + .spawn() + .map_err(|e| CommandError::new(CommandErrorKind::User, e))? + .wait() + .unwrap(); + if !status.success() { + let error_msg = if let Some(code) = status.code() { + format!("external command failed with exit code {code}") + } else { + "external command was terminated by signal".into() + }; + return Err(CommandError::new(CommandErrorKind::User, error_msg)); + } + Ok(()) +} + impl ShellCompletion { fn generate(&self, cmd: &mut Command) -> Vec { use clap_complete::generate; diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index b72045d60c..17ced69dd6 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -1,6 +1,7 @@ --- source: cli/tests/test_generate_md_cli_help.rs description: "AUTO-GENERATED FILE, DO NOT EDIT. This cli reference is generated by a test as an `insta` snapshot. MkDocs includes this snapshot from docs/cli-reference.md." +snapshot_kind: text --- @@ -95,6 +96,7 @@ This document contains the help content for the `jj` command-line program. * [`jj util mangen`↴](#jj-util-mangen) * [`jj util markdown-help`↴](#jj-util-markdown-help) * [`jj util config-schema`↴](#jj-util-config-schema) +* [`jj util exec`↴](#jj-util-exec) * [`jj undo`↴](#jj-undo) * [`jj version`↴](#jj-version) * [`jj workspace`↴](#jj-workspace) @@ -2118,6 +2120,7 @@ Infrequently used commands such as for generating shell completions * `mangen` — Print a ROFF (manpage) * `markdown-help` — Print the CLI help for all subcommands in Markdown * `config-schema` — Print the JSON schema for the jj TOML config format +* `exec` — Execute an external command via jj @@ -2192,6 +2195,47 @@ 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. For example, 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 have to manage your script as a separate file, you can +even inline a script 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 undo` Undo an operation (shortcut for `jj op undo`) diff --git a/cli/tests/test_util_command.rs b/cli/tests/test_util_command.rs index bd019b2f21..b19f06dadd 100644 --- a/cli/tests/test_util_command.rs +++ b/cli/tests/test_util_command.rs @@ -112,3 +112,34 @@ fn test_shell_completions() { test("nushell"); test("zsh"); } + +#[cfg(unix)] +#[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 1c8522f4d3..f3607571f2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -528,6 +528,28 @@ You can define aliases for commands, including their arguments. For example: aliases.l = ["log", "-r", "(main..@):: | (main..@)-"] ``` +If you would like to create an alias that's more complicated than a short form +of a single jj command, you can use `jj util exec` in your alias to execute +arbitrary external commands. This is analogous to `git`'s feature where +executables in you `$PATH` starting with `git-*` can be called implicitly with +`git *`. 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