Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: add exec command for arbitrary aliases #4759

Merged
merged 1 commit into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* The `jj desc` and `jj st` aliases are now hidden to not interfere with shell
completion. They remain available.

* New command `jj util exec` that can be used for arbitrary aliases.

### Fixed bugs

## [0.23.0] - 2024-11-06
Expand Down
95 changes: 95 additions & 0 deletions cli/src/commands/util/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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::user_error_with_message;
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!
///
/// This feature may be removed or replaced by an embedded scripting language in
/// the future.
///
/// 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<String>,
}

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(|err| {
user_error_with_message(
format!("Failed to execute external command '{}'", &args.command),
err,
)
})?;
if !status.success() {
let error_msg = if let Some(exit_code) = status.code() {
format!("External command exited with {exit_code}")
} else {
// signal
format!("External command was terminated by: {status}")
};
return Err(user_error(error_msg));
}
Ok(())
}
5 changes: 5 additions & 0 deletions cli/src/commands/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

mod completion;
mod config_schema;
mod exec;
mod gc;
mod mangen;
mod markdown_help;
Expand All @@ -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;
Expand All @@ -40,6 +43,7 @@ use crate::ui::Ui;
pub(crate) enum UtilCommand {
Completion(UtilCompletionArgs),
ConfigSchema(UtilConfigSchemaArgs),
Exec(UtilExecArgs),
Gc(UtilGcArgs),
Mangen(UtilMangenArgs),
MarkdownHelp(UtilMarkdownHelp),
Expand All @@ -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),
Expand Down
56 changes: 56 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
@@ -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
---
<!-- BEGIN MARKDOWN-->

Expand Down Expand Up @@ -92,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)
Expand Down Expand Up @@ -2117,6 +2119,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
Expand Down Expand Up @@ -2162,6 +2165,59 @@ 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!

This feature may be removed or replaced by an embedded scripting language in
the future.

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 <COMMAND> [ARGS]...`

###### **Arguments:**

* `<COMMAND>` — External command to execute
* `<ARGS>` — Arguments to pass to the external command



## `jj util gc`

Run backend-dependent garbage collection
Expand Down
31 changes: 31 additions & 0 deletions cli/tests/test_util_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use insta::assert_snapshot;

use crate::common::strip_last_line;
use crate::common::TestEnvironment;

#[test]
Expand Down Expand Up @@ -112,3 +113,33 @@ fn test_shell_completions() {
test("nushell");
test("zsh");
}

#[test]
senekor marked this conversation as resolved.
Show resolved Hide resolved
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"],
);
insta::assert_snapshot!(strip_last_line(&err), @"Error: Failed to execute external command 'missing-program'");
}
35 changes: 35 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,41 @@ 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!

This feature may be removed or replaced by an embedded scripting language in
the future.

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
Expand Down