1 unstable release

0.1.0 Sep 12, 2024

#989 in Command-line interface

AGPL-3.0-or-later

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:

  1. Is it fast? Seems fast enough for my needs.
  2. How much does it allocate? No idea. Definitely allocates.
  3. no_std? No STDs, but I don't think compiling with no_std is in scope.
  4. 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).
  5. Any benchmarks? Nope.
  6. Dependencies? std. That's it.
  7. How long did it take you? Not long enough that I actually released it; less than 20 hours of full headed stubborness.
  8. 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...?)
  9. Why not clap or gumdrop? See above.
  10. 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).

No runtime deps