Skip to content
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
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ readme = "README.md"
[workspace.dependencies]
anyhow = "1.0.95"
clap = "4.5.26"
clap_lex = "1.1.0"
config = { version = "0.15.11", features = ["toml"] }
normpath = "1.3"
numeric-sort = "0.1.5"
Expand Down
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ A helpful command-line interface for AUCPL problem setters.
- Generate input test cases from generator scripts
- Compare the outputs of two or more solutions
- Fuzz solutions to see if there are any bugs or unhandled edge cases
- Shell completions

Planned:

- Automatic formatting of problems and solution files
- Uploading problems and test cases to an online judge
- Testing code within judge environments
- Improve checking/validation of problems, covering more criteria
- Shell auto completions

## Install

Expand Down Expand Up @@ -104,15 +104,25 @@ Other
- `aucpl help`: Show help
- `aucpl sync`: Generate or update the problem mappings file

To make `aucpl cd` change your current shell directory, install the shell hook once per shell session:
To make `aucpl cd` change your current shell directory and to also enable dynamic problem/competition completions, install the shell hook once per shell session.

For bash/zsh:

```sh
eval $(aucpl shellinit)
eval "$(aucpl shellinit)"
```

For fish:

```fish
aucpl shellinit | source
```

Then you can run:

```sh
aucpl cd
aucpl cd <problem-name>
aucpl problem test -p <TAB>
aucpl comp finish <TAB>
```
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ path = "src/main.rs"
[dependencies]
anyhow.workspace = true
clap.workspace = true
clap_lex.workspace = true
config.workspace = true
normpath.workspace = true
numeric-sort.workspace = true
Expand Down
88 changes: 88 additions & 0 deletions crates/cli/src/cli/arg_builders.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
//! Shared clap argument builders for problem and competition args.

use clap::{Arg, ArgAction};

pub(crate) const PROBLEM_VALUE_NAME: &str = "PROBLEM";
pub(crate) const COMPETITION_VALUE_NAME: &str = "COMP";

const PROBLEM_HELP: &str = "Problem name (this is not the problem title)";
const COMPETITION_HELP: &str = "Competition name";

fn set_arg_metadata(arg: Arg, help: &'static str, value_name: &'static str) -> Arg {
arg.help(help).value_name(value_name).action(ArgAction::Set)
}

pub(crate) fn configure_problem_arg(arg: Arg) -> Arg {
set_arg_metadata(arg, PROBLEM_HELP, PROBLEM_VALUE_NAME)
}

pub(crate) fn configure_competition_arg(arg: Arg) -> Arg {
set_arg_metadata(arg, COMPETITION_HELP, COMPETITION_VALUE_NAME)
}

pub(crate) fn problem_arg_optional() -> Arg {
configure_problem_arg(Arg::new("problem"))
}

pub(crate) fn problem_option_arg_optional() -> Arg {
problem_arg_optional().short('p').long("problem")
}

pub(crate) fn problem_option_arg_required() -> Arg {
problem_option_arg_optional().required(true)
}

pub(crate) fn competition_arg_optional() -> Arg {
configure_competition_arg(Arg::new("comp"))
}

pub(crate) fn competition_arg_required() -> Arg {
competition_arg_optional().required(true)
}

pub(crate) fn competition_option_arg_optional() -> Arg {
competition_arg_optional().short('c').long("comp")
}

pub(crate) fn competition_option_arg_required() -> Arg {
competition_option_arg_optional().required(true)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn problem_option_arg_helper_sets_expected_metadata() {
let arg = problem_option_arg_required();

assert_eq!(arg.get_id().as_str(), "problem");
assert_eq!(arg.get_short(), Some('p'));
assert_eq!(arg.get_long(), Some("problem"));
assert_eq!(
arg.get_value_names()
.and_then(|names| names.first())
.map(|name| name.as_str()),
Some(PROBLEM_VALUE_NAME)
);
assert!(matches!(arg.get_action(), ArgAction::Set));
assert!(arg.is_required_set());
}

#[test]
fn competition_option_arg_helper_sets_expected_metadata() {
let arg = competition_option_arg_optional();

assert_eq!(arg.get_id().as_str(), "comp");
assert_eq!(arg.get_short(), Some('c'));
assert_eq!(arg.get_long(), Some("comp"));
assert_eq!(
arg.get_value_names()
.and_then(|names| names.first())
.map(|name| name.as_str()),
Some(COMPETITION_VALUE_NAME)
);
assert!(matches!(arg.get_action(), ArgAction::Set));
assert!(!arg.is_required_set());
}
}
9 changes: 3 additions & 6 deletions crates/cli/src/cli/cd.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use std::fs;

use anyhow::{Context, Result};
use clap::{Arg, ArgAction, ArgMatches, Command};
use clap::{ArgMatches, Command};
use normpath::PathExt;

use crate::cli::arg_builders::problem_arg_optional;
use crate::config::get_settings;
use crate::paths::resolve_stored_path;
use crate::problem::sync_mappings::get_problem;
Expand All @@ -15,11 +16,7 @@ pub fn cli() -> Command {
"Print the target directory for a problem or the workspace root.
Evaluate `aucpl shellinit` to instead cd to the directory.",
)
.arg(
Arg::new("problem")
.help("Problem name")
.action(ArgAction::Set),
)
.arg(problem_arg_optional())
}

pub fn exec(args: &ArgMatches) -> Result<()> {
Expand Down
56 changes: 13 additions & 43 deletions crates/cli/src/cli/comp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use std::fs;
use anyhow::{Context, Result};
use clap::{Arg, ArgAction, ArgMatches, Command};

use crate::cli::arg_builders::{
competition_arg_required, competition_option_arg_optional, competition_option_arg_required,
configure_competition_arg, problem_option_arg_required,
};
use crate::comp::{add, create, finish, list, remove, rename, solve, test};
use crate::config::get_settings;
use crate::problem::run::{RunnableCategory, RunnableFile};
Expand All @@ -16,18 +20,8 @@ pub fn cli() -> Command {
.about("Add a problem to a competition")
.arg_required_else_help(true)
.args([
Arg::new("comp")
.short('c')
.long("comp")
.help("Competition name")
.action(ArgAction::Set)
.required(true),
Arg::new("problem")
.short('p')
.long("problem")
.help("Problem name")
.action(ArgAction::Set)
.required(true),
competition_option_arg_required(),
problem_option_arg_required(),
]),
)
.subcommand(
Expand All @@ -44,48 +38,30 @@ pub fn cli() -> Command {
.subcommand(
Command::new("finish")
.about("Finish a competition and archive problems from the competition")
.args([Arg::new("comp")
.help("Competition name")
.action(ArgAction::Set)
.required(true)]),
.args([competition_arg_required()]),
)
.subcommand(
Command::new("list")
.about("List all competitions or list problems in a competition")
.args([Arg::new("comp")
.short('c')
.long("comp")
.help("Competition name")
.action(ArgAction::Set)]),
.args([competition_option_arg_optional()]),
)
.subcommand(
Command::new("remove")
.about("Remove a problem from a competition")
.arg_required_else_help(true)
.args([
Arg::new("comp")
.short('c')
.long("comp")
.help("Competition name")
.action(ArgAction::Set)
.required(true),
Arg::new("problem")
.short('p')
.long("problem")
.help("Problem name")
.action(ArgAction::Set)
.required(true),
competition_option_arg_required(),
problem_option_arg_required(),
]),
)
.subcommand(
Command::new("rename")
.about("Rename a competition")
.arg_required_else_help(true)
.args([
Arg::new("old_name")
configure_competition_arg(Arg::new("old_name"))
.long("old-name")
.help("Old competition name")
.action(ArgAction::Set)
.required(true),
Arg::new("new_name")
.long("new-name")
Expand All @@ -99,10 +75,7 @@ pub fn cli() -> Command {
.about("Generate output test cases for all problems in a competition")
.arg_required_else_help(true)
.args([
Arg::new("comp")
.help("Competition name")
.action(ArgAction::Set)
.required(true),
competition_arg_required(),
Arg::new("lang")
.long("lang")
.help("Language of the solution file (e.g. cpp, py)")
Expand All @@ -114,10 +87,7 @@ pub fn cli() -> Command {
.about("Run tests on all problems in a competition")
.arg_required_else_help(true)
.args([
Arg::new("comp")
.help("Competition name")
.action(ArgAction::Set)
.required(true),
competition_arg_required(),
Arg::new("lang")
.long("lang")
.help("Language of the solution file (e.g. cpp, py)")
Expand Down
Loading