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 3, 2024
1 parent 15da697 commit 0145ba3
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 59 additions & 0 deletions cli/src/commands/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String>,
}

/// Available shell completions
#[derive(clap::ValueEnum, Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum ShellCompletion {
Expand All @@ -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),
}
}

Expand Down Expand Up @@ -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<u8> {
use clap_complete::generate;
Expand Down
44 changes: 44 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 @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 <COMMAND> [ARGS]...`
###### **Arguments:**
* `<COMMAND>` — External command to execute
* `<ARGS>` — Arguments to pass to the external command
## `jj undo`
Undo an operation (shortcut for `jj op undo`)
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());
}
22 changes: 22 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0145ba3

Please sign in to comment.