1 unstable release
0.1.0 | Sep 12, 2024 |
---|
#989 in Command-line interface
36KB
676 lines
=============== getoptions-long
An argument parser library inspired by perl's Getopt::Long API.
COPYING
See COPYING.txt and license headers on all source code files.
BUGS
It's going to take more than a synapse mechanic to remove these bugs.
SEE ALSO
Getopt::Long: https://perldoc.perl.org/Getopt::Long
lib.rs
:
getoptions-long - library to parse command line inspired by perl's Getopt::Long Copyright (C) 2024 Ira Peach
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.
=============== getoptions-long
Argument parser inspired by Perl's Getopt::Long.
Provides a likely familiar option parser for those who may be familiar with Getopt::Long from
Perl. The specific options for Getopt::Long that were targeted are qw(:config gnu_getopt no_ignore_case no_auto_abbrev)
.
========== Quickstart
use std::cell::RefCell;
use getoptions_long::*;
let args: Vec<String> = std::env::args().collect();
// example invocation:
//
// backup-profile --list -n --backup-dir=/mnt/usr1/backups --backup-dir \
// "/mnt/usr1/backups test -v -- -f.txt
let prog = args[0].clone();
let args: Vec<String> = args.into_iter().skip(1).collect();
let usage = || {
let prog = std::path::Path::new(&prog).file_name().unwrap().to_string_lossy();
println!("usage: {prog} [-hlcpnv] [-b BACKUP] [--] [TAR_ARGUMENTS]");
println!(" backup firefox profiles on win32 from cygwin");
println!(" -h,--help display this usage");
println!(" -b,--backup-dir BACKUP output backup files to BACKUP (default '.')");
println!(" -l,--list list profiles and their paths");
println!(" -n,--dry-run perform dry run with commands printed");
println!(" -p,--print print Firefox configuration root");
println!(" -v,--verbose be noisier");
};
let mut backup_dir = String::new();
let mut dry_run = false;
let free_args = RefCell::new(vec![]);
let mut list = false;
let mut print = false;
let mut verbose = false;
let result = get_options(&mut [
Opt::SubSwitch("help|h", &|_| { usage(); std::process::exit(0); }),
Opt::Switch ("list|l", &mut list),
Opt::Switch ("p|print", &mut print),
Opt::Switch ("dry-run|n", &mut dry_run),
Opt::Arg ("backup-dir|b", &mut backup_dir),
Opt::Switch ("verbose|v", &mut verbose),
Opt::Free (&|arg| free_args.borrow_mut().push(arg.to_string())),
], &args);
if let Err(ref err) = result {
eprintln!("ERROR: parsing command line arguments: {err}");
//std::process::exit(1); // don't do this in a test
}
// ...
==== Why?
Why another command line parser? Well, I missed the options Getopt::Long can do.
In comparison to existing crates, such as clap and gumdrop, this parser is active in your code
only at the function invocation site (usually get_options
if you have a fully realized list
of strings, or get_options_env
for quick and dirty). It also uses a //lot// of mutability,
which Rust exposes in excruciating detail. You do not have to define your own type, use derive
macros, and hope that it does what you want (clap), or drop to a frustrating builder invocation
chain to enable last-argument precedence (clap), or end up in a frustrating API for using
callbacks (clap, gumdrop). (I'm actually not sure it's possible to use callbacks with clap &
grumdrop, because my brain might just not understand the documentation) (it is, but you have to
use the Arg API in clap)
Callbacks allow a lot of things. The main thing I use them for is mutually exclusive command line arguments with last precedence, such as:
use getoptions_long::*;
let args = ["--foo", "--baz", "--bar", "--baz"];
let command = std::cell::RefCell::new(String::new());
let sub: &dyn for<'a> Fn(&'a str) = &|arg| {
let mut command = command.borrow_mut();
*command = format!("command-{}", arg);
};
let result = get_options_str(&mut [
Opt::SubSwitch("foo", sub),
Opt::SubSwitch("bar", sub),
Opt::SubSwitch("baz", sub),
], &args);
assert_eq!(result, Ok(()));
assert_eq!(command.into_inner(), "command-baz");
They can be trivially extended to adding repeat arguments, as well.
If you aren't familiar with the Rustonomicon (I definitely am not), you might be wondering why it is quite unergonomic to need to use RefCell. Encapsulating a value that is later borrowed from a RefCell is one of the scarce few ways to pass the same closure to multiple switches (because while we're not violating the multiple mutable borrows rule, we do promise Rust that we're being careful).
==== Q&A:
- Is it fast? Seems fast enough for my needs.
- How much does it allocate? No idea. Definitely allocates.
- no_std? No STDs, but I don't think compiling with no_std is in scope.
- Other Getopt::Long options? Maybe. I mostly went with what I go with and seems fairly consistent with POSIX getopt with GNU extensions (I'm actually interested in testing exactly how compliant we are).
- Any benchmarks? Nope.
- Dependencies? std. That's it.
- How long did it take you? Not long enough that I actually released it; less than 20 hours of full headed stubborness.
- Rewrite Perl in Rust? Have you looked at the source code? My eyes go spirally when I see way too many macros and ifdefs. I ain't doing that. (unless...?)
- Why not clap or gumdrop? See above.
- Is this based off of Getopt::Long? Nope. No source code was referenced; if it were I would keep the same licensing terms (and I'm probably open to relicensing given enough value for labor).