use clap::Parser;
use clap_complete::Shell;
use clap_verbosity_flag::Verbosity;
use gflow::utils::STYLES;
use std::num::NonZeroUsize;
#[derive(Debug, Parser)]
#[command(
name = "gjob",
author,
version=gflow::build_info::version(),
about = "Controls and manages jobs in the gflow scheduler.",
)]
#[command(styles=STYLES)]
pub struct GJob {
#[command(flatten)]
pub verbosity: Verbosity,
#[arg(long, global = true, help = "Path to the config file", hide = true)]
pub config: Option<std::path::PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Parser)]
pub enum Commands {
#[command(visible_alias = "a")]
Attach {
#[arg(help = "Job ID to attach to (supports @ for most recent job)", value_hint = clap::ValueHint::Other)]
job: String,
},
#[command(visible_alias = "l")]
Log {
#[arg(help = "Job ID to view the log for (supports @ for most recent job)", value_hint = clap::ValueHint::Other)]
job: String,
#[arg(
short = 'f',
long = "first",
help = "Print only the first N lines of the job log",
value_name = "LINES",
conflicts_with = "last"
)]
first: Option<NonZeroUsize>,
#[arg(
short = 'l',
long = "last",
help = "Print only the last N lines of the job log",
value_name = "LINES",
conflicts_with = "first"
)]
last: Option<NonZeroUsize>,
},
#[command(visible_alias = "h")]
Hold {
#[arg(
help = "Job ID(s) to hold. Supports ranges like \"1-3\" or individual IDs like \"1,2,3\"",
value_hint = clap::ValueHint::Other
)]
job: String,
},
#[command(visible_alias = "r")]
Release {
#[arg(
help = "Job ID(s) to release. Supports ranges like \"1-3\" or individual IDs like \"1,2,3\"",
value_hint = clap::ValueHint::Other
)]
job: String,
},
#[command(visible_alias = "u")]
Update {
#[arg(
help = "Job ID(s) to update. Supports ranges like \"1-3\" or individual IDs like \"1,2,3\"",
value_hint = clap::ValueHint::Other
)]
job: String,
#[arg(short = 'c', long, help = "Update command", value_hint = clap::ValueHint::Other)]
command: Option<String>,
#[arg(short = 's', long, help = "Update script path", value_hint = clap::ValueHint::FilePath)]
script: Option<std::path::PathBuf>,
#[arg(short = 'g', long, help = "Update number of GPUs")]
gpus: Option<u32>,
#[arg(short = 'e', long, help = "Update conda environment", value_hint = clap::ValueHint::Other)]
conda_env: Option<String>,
#[arg(long, help = "Clear conda environment")]
clear_conda_env: bool,
#[arg(short = 'p', long, help = "Update priority (0-255)")]
priority: Option<u8>,
#[arg(short = 't', long, help = "Update time limit (formats: HH:MM:SS, MM:SS, or MM)", value_hint = clap::ValueHint::Other)]
time_limit: Option<String>,
#[arg(long, help = "Clear time limit")]
clear_time_limit: bool,
#[arg(short = 'm', long, help = "Update memory limit (formats: 100G, 1024M, or 512 for MB)", value_hint = clap::ValueHint::Other)]
memory_limit: Option<String>,
#[arg(long, help = "Clear memory limit")]
clear_memory_limit: bool,
#[arg(
long = "gpu-memory",
visible_alias = "max-gpu-mem",
help = "Update per-GPU memory limit (formats: 24G, 16384M, or 8192 for MB)",
value_hint = clap::ValueHint::Other
)]
gpu_memory_limit: Option<String>,
#[arg(long, help = "Clear per-GPU memory limit")]
clear_gpu_memory_limit: bool,
#[arg(
short = 'd',
long,
help = "Update dependencies (comma-separated job IDs)",
value_delimiter = ','
)]
depends_on: Option<Vec<u32>>,
#[arg(
long,
help = "Update dependencies with AND logic (all must finish)",
value_delimiter = ','
)]
depends_on_all: Option<Vec<u32>>,
#[arg(
long,
help = "Update dependencies with OR logic (any one must finish)",
value_delimiter = ','
)]
depends_on_any: Option<Vec<u32>>,
#[arg(long, help = "Enable auto-cancel when dependency fails")]
auto_cancel_on_dep_failure: bool,
#[arg(long, help = "Disable auto-cancel when dependency fails")]
no_auto_cancel_on_dep_failure: bool,
#[arg(long, help = "Update max concurrent jobs in group")]
max_concurrent: Option<usize>,
#[arg(long, help = "Clear max concurrent limit")]
clear_max_concurrent: bool,
#[arg(long, help = "Update automatic retry limit")]
max_retries: Option<u32>,
#[arg(long, help = "Clear automatic retry limit")]
clear_max_retries: bool,
#[arg(long = "param", help = "Update parameter (KEY=VALUE, can be repeated)", value_hint = clap::ValueHint::Other)]
params: Vec<String>,
},
#[command(visible_alias = "s")]
Show {
#[arg(
help = "Job ID(s) to show details for. Supports ranges like \"1-3\" or individual IDs like \"1,2,3\"",
value_hint = clap::ValueHint::Other
)]
job: String,
},
Redo {
#[arg(help = "Job ID to resubmit (supports @ for most recent job)", value_hint = clap::ValueHint::Other)]
job: String,
#[arg(short, long, help = "Override number of GPUs")]
gpus: Option<u32>,
#[arg(short, long, help = "Override priority")]
priority: Option<u8>,
#[arg(short = 'd', long, help = "Override or set dependency (job ID or @)", value_hint = clap::ValueHint::Other)]
depends_on: Option<String>,
#[arg(
short,
long,
help = "Override time limit (formats: HH:MM:SS, MM:SS, or MM)",
value_hint = clap::ValueHint::Other
)]
time: Option<String>,
#[arg(
short = 'm',
long,
help = "Override memory limit (formats: 100G, 1024M, or 512 for MB)",
value_hint = clap::ValueHint::Other
)]
memory: Option<String>,
#[arg(
long = "gpu-memory",
visible_alias = "max-gpu-mem",
help = "Override per-GPU memory limit (formats: 24G, 16384M, or 8192 for MB)",
value_hint = clap::ValueHint::Other
)]
gpu_memory: Option<String>,
#[arg(short = 'e', long, help = "Override conda environment", value_hint = clap::ValueHint::Other)]
conda_env: Option<String>,
#[arg(long, help = "Clear dependency from original job")]
clear_deps: bool,
#[arg(
long,
help = "Also redo dependent jobs that were cancelled due to this job's failure"
)]
cascade: bool,
},
#[command(visible_alias = "close")]
CloseSessions {
#[arg(
short = 'j',
long,
help = "Job ID(s) to close sessions for. Supports ranges like \"1-3\" or individual IDs like \"1,2,3\"",
value_hint = clap::ValueHint::Other
)]
jobs: Option<String>,
#[arg(
short = 's',
long,
help = "Close sessions for jobs in specific state(s). Accepts: queued, hold, running, finished, failed, cancelled, timeout",
value_delimiter = ',',
value_hint = clap::ValueHint::Other
)]
state: Option<Vec<gflow::core::job::JobState>>,
#[arg(
short = 'p',
long,
help = "Close sessions matching this pattern (substring match on session name)",
value_hint = clap::ValueHint::Other
)]
pattern: Option<String>,
#[arg(
short = 'a',
long,
help = "Close sessions for all completed jobs (finished, failed, cancelled, timeout)"
)]
all: bool,
},
Completion {
#[arg(value_enum)]
shell: Shell,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_log_first_option() {
let args =
GJob::try_parse_from(["gjob", "log", "@", "--first", "25"]).expect("should parse");
match args.command {
Commands::Log { job, first, last } => {
assert_eq!(job, "@");
assert_eq!(first.map(NonZeroUsize::get), Some(25));
assert_eq!(last, None);
}
other => panic!("unexpected command: {other:?}"),
}
}
#[test]
fn rejects_conflicting_log_slice_options() {
let err = GJob::try_parse_from(["gjob", "log", "42", "--first", "10", "--last", "10"])
.expect_err("should reject conflicting options");
let message = err.to_string();
assert!(message.contains("--first"));
assert!(message.contains("--last"));
}
}