A simple shell wrapper that handles argument parsing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

Fly 460c0ab2

+247
+1
.gitignore
··· 1 + /target
+16
Cargo.lock
··· 1 + # This file is automatically @generated by Cargo. 2 + # It is not intended for manual editing. 3 + version = 4 4 + 5 + [[package]] 6 + name = "argsh" 7 + version = "0.1.0" 8 + dependencies = [ 9 + "lexopt", 10 + ] 11 + 12 + [[package]] 13 + name = "lexopt" 14 + version = "0.3.1" 15 + source = "registry+https://github.com/rust-lang/crates.io-index" 16 + checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7"
+8
Cargo.toml
··· 1 + [package] 2 + name = "argsh" 3 + version = "0.1.0" 4 + edition = "2024" 5 + publish = false 6 + 7 + [dependencies] 8 + lexopt = "0.3.1"
+39
README.md
··· 1 + # argsh 2 + 3 + This is a simple shell wrapper for scripts that handles argument parsing, because argument parsing in bash is a pain in the ass. 4 + 5 + ## Usage 6 + 7 + Put argsh in the shebang, instead of your shell as such: 8 + 9 + ```bash 10 + #!/bin/env argsh <arguments> 11 + ``` 12 + 13 + And you define the arguments as a semicolon separated list of arguments. 14 + 15 + The only valid characters for an argument are `a-z` and `-`. 16 + 17 + A plain argument such as `arg` is a required argument, which will be passed to the script as a positional argument. 18 + 19 + An argument starting with `$` such as `$arg` is an optional argument, which will be passed to the script as an environment variable like `$ARGSH_ARG`, where all `-` get converted to `_`. 20 + 21 + Any additional positional arguments passed to the script get appended after the required arguments. 22 + 23 + Optionally, you can set the shell to use as such `bash|name;count`, defaults to `sh` if ommited. 24 + 25 + ## Limitations 26 + 27 + For the short version of arguments to work properly, all arguments must start with different characters. 28 + 29 + All arguments must have a matching value, as such, flags aren't possible using argsh. 30 + 31 + Limit of 256 arguments (if you have more than that please use an actual programming language). 32 + 33 + ## Testing 34 + 35 + Run the test script as such and mess around with the arguments 36 + 37 + ``` 38 + ./test --required indeed --also-required nice --optional woah --second-optional crazy am additional 39 + ```
+1
rustfmt.toml
··· 1 + hard_tabs = true
+164
src/main.rs
··· 1 + mod args { 2 + #[derive(Debug)] 3 + pub struct Args { 4 + pub format: String, 5 + pub script: String, 6 + pub inner_parser: lexopt::Parser, 7 + } 8 + impl Args { 9 + pub fn parse() -> Result<Self, lexopt::Error> { 10 + use lexopt::prelude::*; 11 + 12 + let mut parser = lexopt::Parser::from_env(); 13 + 14 + let format = parser.value()?.parse()?; 15 + let script = parser.value()?.parse()?; 16 + 17 + Ok(Self { 18 + format, 19 + script, 20 + inner_parser: parser, 21 + }) 22 + } 23 + } 24 + } 25 + 26 + mod config { 27 + #[derive(Debug, Eq, PartialEq, std::hash::Hash)] 28 + pub enum Arg { 29 + Required(String, u8), 30 + Optional(String), 31 + } 32 + 33 + fn is_valid_name(s: &str) -> bool { 34 + !s.is_empty() && s.chars().all(|c| matches!(c, 'a'..='z' | '-')) 35 + } 36 + 37 + pub struct Config { 38 + pub shell: Option<String>, 39 + pub args: Vec<Arg>, 40 + } 41 + impl Config { 42 + pub fn parse(format: &str) -> Result<Self, lexopt::Error> { 43 + let (shell, args_raw) = { 44 + let mut split: Vec<&str> = format.splitn(2, '|').collect(); 45 + 46 + let shell = match split.len() { 47 + 1 => None, 48 + 2 => Some(split.remove(0)), 49 + _ => panic!("what the fuck"), 50 + }; 51 + 52 + (shell, split.pop().unwrap_or_default()) 53 + }; 54 + 55 + let mut args: Vec<Arg> = Vec::new(); 56 + let mut pos: u8 = 0; 57 + 58 + for arg in args_raw.split(';') { 59 + if let Some(arg) = arg.strip_prefix('$') { 60 + if is_valid_name(arg) { 61 + args.push(Arg::Optional(arg.to_owned())); 62 + } else { 63 + return Err("Invalid argument name".into()); 64 + } 65 + } else { 66 + if is_valid_name(arg) { 67 + pos += 1; 68 + args.push(Arg::Required(arg.to_owned(), pos)); 69 + } else { 70 + return Err("Invalid argument name".into()); 71 + } 72 + } 73 + } 74 + 75 + Ok(Self { 76 + shell: shell.map(str::to_owned), 77 + args, 78 + }) 79 + } 80 + 81 + pub fn match_arg(&self, arg_to_match: &str) -> Option<&Arg> { 82 + self.args.iter().find(|arg| match arg { 83 + Arg::Required(arg, _) => arg == arg_to_match, 84 + Arg::Optional(arg) => arg == arg_to_match, 85 + }) 86 + } 87 + 88 + pub fn match_arg_short(&self, arg_to_match: char) -> Option<&Arg> { 89 + self.args.iter().find(|arg| match arg { 90 + Arg::Required(arg, _) => { 91 + arg.chars().nth(0).map_or(false, |arg| arg == arg_to_match) 92 + } 93 + Arg::Optional(arg) => arg.chars().nth(0).map_or(false, |arg| arg == arg_to_match), 94 + }) 95 + } 96 + } 97 + } 98 + 99 + fn execute( 100 + parser: &mut lexopt::Parser, 101 + config: config::Config, 102 + script: &str, 103 + ) -> Result<(), lexopt::Error> { 104 + use lexopt::prelude::*; 105 + use std::collections::{BTreeMap, HashMap}; 106 + use std::os::unix::process::CommandExt; 107 + 108 + let mut pos_args: Vec<String> = Vec::new(); 109 + let mut args: HashMap<&config::Arg, String> = HashMap::new(); 110 + 111 + while let Some(arg) = parser.next()? { 112 + match arg { 113 + Short(short) => { 114 + if let Some(arg) = config.match_arg_short(short) { 115 + args.insert(arg, parser.value()?.parse()?); 116 + } else { 117 + return Err(arg.unexpected()); 118 + } 119 + } 120 + Long(long) => { 121 + if let Some(arg) = config.match_arg(long) { 122 + args.insert(arg, parser.value()?.parse()?); 123 + } else { 124 + return Err(arg.unexpected()); 125 + } 126 + } 127 + Value(val) => pos_args.push(val.parse()?), 128 + } 129 + } 130 + 131 + let mut required_args: BTreeMap<u8, String> = BTreeMap::new(); 132 + let mut optional_args: Vec<(String, String)> = Vec::new(); 133 + 134 + for (arg, value) in args { 135 + match arg { 136 + config::Arg::Required(_, pos) => { 137 + required_args.insert(pos - 1, value); 138 + } 139 + config::Arg::Optional(arg) => { 140 + optional_args.push(( 141 + format!("ARGSH_{}", arg.replace('-', "_").to_uppercase()), 142 + value, 143 + )); 144 + } 145 + } 146 + } 147 + 148 + let error = std::process::Command::new(config.shell.unwrap_or("sh".to_owned())) 149 + .envs(optional_args) 150 + .arg(script) 151 + .args(required_args.into_values()) 152 + .args(pos_args) 153 + .exec(); 154 + 155 + return Err(lexopt::Error::Custom(Box::new(error))); 156 + } 157 + 158 + fn main() -> Result<(), lexopt::Error> { 159 + let mut args = args::Args::parse()?; 160 + 161 + let config = config::Config::parse(&args.format)?; 162 + 163 + execute(&mut args.inner_parser, config, &args.script) 164 + }
+18
test
··· 1 + #!./target/debug/argsh bash|required;also-required;$optional;$second-optional 2 + 3 + echo "--required on pos 1: $1" 4 + echo "--also-required on pos 2: $2" 5 + 6 + if [[ -n "$ARGSH_OPTIONAL" ]]; then 7 + echo "--optional: $ARGSH_OPTIONAL" 8 + else 9 + echo "--optional not set" 10 + fi 11 + 12 + if [[ -n "$ARGSH_SECOND_OPTIONAL" ]]; then 13 + echo "--second-optional: $ARGSH_SECOND_OPTIONAL" 14 + else 15 + echo "--second-optional not set" 16 + fi 17 + 18 + echo "full args: $@"