Skip to content

Commit

Permalink
util: add exec command for arbitrary aliases
Browse files Browse the repository at this point in the history
  • Loading branch information
senekor committed Nov 8, 2024
1 parent 2aadae1 commit 08daf32
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions cli/src/commands/util/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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!
///
/// This feature may be removed entirely in the future. It may be replaced by
/// an embedded scripting language with limited capabilities for safety.
///
/// 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(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(())
}
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
55 changes: 55 additions & 0 deletions cli/tests/[email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2161,6 +2163,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 entirely in the future. It may be replaced by
an embedded scripting language with limited capabilities for safety.
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
30 changes: 30 additions & 0 deletions cli/tests/test_util_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
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 entirely in the future. It may be replaced by
an embedded scripting language with limited capabilities for safety.

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

0 comments on commit 08daf32

Please sign in to comment.