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

feat: add features for easily integrating argc as lib #323

Merged
merged 3 commits into from
Apr 27, 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
17 changes: 16 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,19 @@ jobs:
run: cargo clippy --all --all-targets

- name: Format
run: cargo fmt --all --check
run: cargo fmt --all --check

features:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install Rust Toolchain Components
uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2

- uses: taiki-e/install-action@cargo-hack

- run: cargo hack --no-dev-deps check --feature-powerset --depth 2 --lib
5 changes: 5 additions & 0 deletions Argcfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ test() {
cargo test "$@"
}

# @cmd Test features matrix
test-features() {
cargo hack --no-dev-deps check --feature-powerset --depth 2 --lib
}

# @cmd Check the project
# @alias c
check() {
Expand Down
49 changes: 39 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,52 @@ autotests = false
categories = ["command-line-utilities", "development-tools"]
keywords = ["cli", "bash", "runner", "arg", "parser"]

[features]
default = ["application"]
# Feature required for argc the application. Should be disabled when depending on
# argc as a library.
application = [
"native-runtime",
"eval-bash",
"build",
"mangen",
"completions",
"compgen",
"export",
"wrap-help",

# deps
"num_cpus",
"threadpool",
"base64",
]
native-runtime = ["which"]
eval = []
eval-bash = ["eval"]
build = []
mangen = ["roff"]
completions = []
compgen = ["dirs", "natord"]
export = ["serde_json", "indexmap/serde"]
wrap-help = ["textwrap"]

[dependencies]
anyhow = "1"
convert_case = "0.6"
indexmap = { version = "2.1", features = ["serde"] }
indexmap = { version = "2.1" }
nom = "7.1"
either = "1.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
which = "6.0"
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
which = { version = "6.0", optional = true }
shell-words = "1.1"
textwrap = "0.16"
dirs = "5.0"
num_cpus = "1.16"
threadpool = "1.8"
base64 = "0.22"
natord = "1.0"
roff = "0.2"
textwrap = { version = "0.16", optional = true }
dirs = { version = "5.0", optional = true }
num_cpus = { version = "1.16", optional = true }
threadpool = { version = "1.8", optional = true }
base64 = { version = "0.22", optional = true }
natord = { version = "1.0", optional = true }
roff = { version = "0.2", optional = true }

[dev-dependencies]
insta = "1.30"
Expand Down
2 changes: 1 addition & 1 deletion examples/multiline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
# are treated as the long description. A line which is not a comment ends the block.
cmd() { :; }

eval "$(TERM_WIDTH=`tput cols` argc --argc-eval "$0" "$@")"
eval "$(TERM_WIDTH=${TERM_WIDTH:-`tput cols`} argc --argc-eval "$0" "$@")"
4 changes: 3 additions & 1 deletion src/argc_value.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use indexmap::IndexMap;

#[cfg(feature = "eval-bash")]
use crate::utils::{
argc_var_name, escape_shell_words, expand_dotenv, AFTER_HOOK, ARGC_REQUIRE_TOOLS, BEFORE_HOOK,
VARIABLE_PREFIX,
Expand All @@ -25,8 +26,9 @@ pub enum ArgcValue {
Error((String, i32)),
}

#[cfg(feature = "eval-bash")]
impl ArgcValue {
pub fn to_shell(values: &[Self]) -> String {
pub fn to_bash(values: &[Self]) -> String {
let mut list = vec![];
let mut last = String::new();
let mut exit = false;
Expand Down
62 changes: 41 additions & 21 deletions src/bin/argc/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ mod parallel;

use anyhow::{anyhow, bail, Context, Result};
use argc::{
utils::{escape_shell_words, get_current_dir, get_shell_path, is_true_value, termwidth},
Shell,
utils::{escape_shell_words, is_true_value, ARGC_COMPLETION_SCRIPT_PATH},
NativeRuntime, Runtime, Shell,
};
use base64::{engine::general_purpose, Engine as _};
use std::{
Expand All @@ -12,7 +12,6 @@ use std::{
path::{Path, PathBuf},
process,
};
use which::which;

const ARGC_SCRIPT_NAMES: [&str; 6] = [
"Argcfile.sh",
Expand All @@ -38,6 +37,7 @@ fn main() {
}

fn run() -> Result<i32> {
let runtime = NativeRuntime;
let args: Vec<String> = std::env::args().collect();
let mut argc_cmd = None;
if let Some(arg) = args.get(1) {
Expand Down Expand Up @@ -65,9 +65,15 @@ fn run() -> Result<i32> {
code.push_str(&cmds.join(" "));
println!("{code}")
} else {
let values = argc::eval(&source, &cmd_args, Some(&args[2]), termwidth())?;
let values = argc::eval(
runtime,
&source,
&cmd_args,
Some(&args[2]),
get_term_width(),
)?;
let dir_vars = export_dir_vars(&args[2]);
let code = argc::ArgcValue::to_shell(&values);
let code = argc::ArgcValue::to_bash(&values);
let export_vars = export_argc_variables(&code);
println!("{dir_vars}{export_vars}{code}")
}
Expand All @@ -85,7 +91,7 @@ fn run() -> Result<i32> {
"--argc-build" => {
let script_path = &args[2];
let (source, args) = parse_script_args(&args[2..])?;
let script = argc::build(&source, &args[0])?;
let script = argc::build(&source, &args[0], get_term_width())?;
if let Some(outpath) = args.get(1) {
let script_name = get_script_name(script_path)?;
let (outpath, new) = ensure_outpath(outpath, script_name)
Expand Down Expand Up @@ -127,7 +133,7 @@ fn run() -> Result<i32> {
print!("{}", script);
}
"--argc-compgen" => {
run_compgen(args.to_vec());
run_compgen(runtime, args.to_vec());
}
"--argc-export" => {
let (source, args) = parse_script_args(&args[2..])?;
Expand All @@ -138,22 +144,22 @@ fn run() -> Result<i32> {
if args.len() <= 3 {
bail!("Usage: argc --argc-parallel <SCRIPT> <ARGS>...");
}
let shell = get_shell_path()?;
let shell = runtime.shell_path()?;
let (source, cmd_args) = parse_script_args(&args[2..])?;
if !source.contains("--argc-eval") {
bail!("Parallel only available for argc based scripts.")
}
let script_file = args[2].clone();
parallel::parallel(&shell, &script_file, &cmd_args[1..])?;
parallel::parallel(runtime, &shell, &script_file, &cmd_args[1..])?;
}
"--argc-script-path" => {
let (_, script_file) =
get_script_path(true).ok_or_else(|| anyhow!("Argcfile not found."))?;
println!("{}", script_file.display());
}
"--argc-shell-path" => {
let shell = get_shell_path()?;
println!("{}", shell.display());
let shell = runtime.shell_path()?;
println!("{}", shell);
}
"--argc-help" => {
println!("{}", get_argc_help())
Expand All @@ -167,11 +173,11 @@ fn run() -> Result<i32> {
}
Ok(0)
} else {
let shell = get_shell_path()?;
let shell = runtime.shell_path()?;
let (script_dir, script_file) = get_script_path(true)
.ok_or_else(|| anyhow!("Argcfile not found, try `argc --argc-help` for help."))?;
let mut envs = HashMap::new();
if let Some(cwd) = get_current_dir() {
if let Some(cwd) = runtime.current_dir() {
envs.insert("ARGC_PWD".to_string(), escape_shell_words(&cwd));
}
let mut command = process::Command::new(shell);
Expand All @@ -196,7 +202,7 @@ fn run() -> Result<i32> {
}
}

fn run_compgen(mut args: Vec<String>) -> Option<()> {
fn run_compgen(runtime: NativeRuntime, mut args: Vec<String>) -> Option<()> {
if args.len() < 6 {
return None;
};
Expand All @@ -208,8 +214,8 @@ fn run_compgen(mut args: Vec<String>) -> Option<()> {
}
} else if let Some(script_file) = search_completion_script(&mut args) {
args[3] = script_file.to_string_lossy().to_string();
} else if let Ok(script_file) = which(&args[4]) {
args[3] = script_file.to_string_lossy().to_string();
} else if let Some(script_file) = runtime.which(&args[4]) {
args[3] = script_file;
}
}
let no_color = std::env::var("NO_COLOR")
Expand All @@ -218,13 +224,21 @@ fn run_compgen(mut args: Vec<String>) -> Option<()> {
let output = if &args[4] == "argc" && (args[3].is_empty() || args[5].starts_with("--argc")) {
let cmd_args = &args[4..];
let script = get_argc_script_code();
argc::compgen(shell, "", &script, cmd_args, no_color).ok()?
argc::compgen(
runtime,
shell,
ARGC_COMPLETION_SCRIPT_PATH,
&script,
cmd_args,
no_color,
)
.ok()?
} else if args[3].is_empty() {
let cmd_args = &args[4..];
argc::compgen(shell, "", "# @arg path*", cmd_args, no_color).ok()?
argc::compgen(runtime, shell, "", "# @arg path*", cmd_args, no_color).ok()?
} else {
let (source, cmd_args) = parse_script_args(&args[3..]).ok()?;
argc::compgen(shell, &args[3], &source, &cmd_args[1..], no_color).ok()?
argc::compgen(runtime, shell, &args[3], &source, &cmd_args[1..], no_color).ok()?
};
if !output.is_empty() {
println!("{output}");
Expand Down Expand Up @@ -321,10 +335,16 @@ fn get_argc_version() -> String {
format!("{name} {version}")
}

fn get_term_width() -> Option<usize> {
env::var("TERM_WIDTH").ok().and_then(|v| v.parse().ok())
}

fn export_dir_vars(path: &str) -> String {
if let (Some(argc_script_dir), Some(cwd)) = (
get_argc_script_dir(path),
env::var("ARGC_PWD").ok().or_else(get_current_dir),
env::var("ARGC_PWD")
.ok()
.or_else(|| env::current_dir().ok().map(|v| v.to_string_lossy().into())),
) {
let cd = if argc_script_dir != cwd {
format!("cd {}\n", escape_shell_words(&argc_script_dir))
Expand Down Expand Up @@ -431,7 +451,7 @@ fn candidate_script_names() -> Vec<String> {
}

fn search_completion_script(args: &mut Vec<String>) -> Option<PathBuf> {
let search_paths = std::env::var("ARGC_COMPLETIONS_PATH").ok()?;
let search_paths = env::var("ARGC_COMPLETIONS_PATH").ok()?;
let cmd = Path::new(&args[4])
.file_stem()
.and_then(|v| v.to_str())?
Expand Down
16 changes: 10 additions & 6 deletions src/bin/argc/parallel.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
use anyhow::Result;
use argc::utils::{get_shell_args, path_env_with_exe};
use argc::{NativeRuntime, Runtime};
use std::collections::HashMap;
use std::path::Path;
use std::process::{self, Command};
use std::sync::mpsc::channel;
use threadpool::ThreadPool;

pub const PARALLEL_MODE: &str = "___parallel___";

pub fn parallel(shell: &Path, script_file: &str, args: &[String]) -> Result<()> {
pub fn parallel(
runtime: NativeRuntime,
shell: &str,
script_file: &str,
args: &[String],
) -> Result<()> {
let jobs = to_jobs(args);
let jobs_len = jobs.len();
let pool = ThreadPool::new(num_cpus::get());
let (tx, rx) = channel();
let path_env = path_env_with_exe();
let mut shell_extra_args = get_shell_args(shell);
let path_env = runtime.path_env_with_current_exe();
let mut shell_extra_args = runtime.shell_args(shell);
shell_extra_args.push(script_file.to_string());
shell_extra_args.push(PARALLEL_MODE.to_string());
for (i, job_args) in jobs.into_iter().enumerate() {
let tx = tx.clone();
let shell = shell.to_path_buf();
let shell = shell.to_string();
let path_env = path_env.clone();
let shell_extra_args = shell_extra_args.clone();
pool.execute(move || {
Expand Down
14 changes: 7 additions & 7 deletions src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ _argc_check_bool() {
),
];

pub fn build(source: &str, root_name: &str) -> Result<String> {
pub fn build(source: &str, root_name: &str, wrap_width: Option<usize>) -> Result<String> {
let cmd = Command::new(source, root_name)?;
let output = build_root(&cmd);
let output = build_root(&cmd, wrap_width);
let mut build_block = false;
let mut insert_at = None;
let mut newlines = vec![];
Expand Down Expand Up @@ -210,8 +210,8 @@ pub fn build(source: &str, root_name: &str) -> Result<String> {
Ok(newlines.join("\n"))
}

fn build_root(cmd: &Command) -> String {
let command = build_command(cmd);
fn build_root(cmd: &Command, wrap_width: Option<usize>) -> String {
let command = build_command(cmd, wrap_width);
let dotenv = if let Some(value) = cmd.dotenv() {
format!("\n {}", expand_dotenv(value))
} else {
Expand Down Expand Up @@ -273,15 +273,15 @@ _argc_run "$@"
)
}

fn build_command(cmd: &Command) -> String {
fn build_command(cmd: &Command, wrap_width: Option<usize>) -> String {
let suffix = if cmd.is_root() {
String::new()
} else {
format!("_{}", cmd.paths.join("_"))
};

let usage = {
let usage = cmd.render_help(None);
let usage = cmd.render_help(wrap_width);
let usage = usage.trim();
format!(
r#"
Expand Down Expand Up @@ -313,7 +313,7 @@ _argc_version{suffix}() {{
let subcmds = cmd
.subcommands
.iter()
.map(build_command)
.map(|v| build_command(v, wrap_width))
.collect::<Vec<String>>()
.join("");

Expand Down
Loading