From 7eda8bb721bab2ec4c00517a2eadd07f0a5ddac8 Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 15:44:19 -0700 Subject: [PATCH 01/13] fop(1): working prototype --- GNUmakefile | 3 +++ src/fop.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/fop.rs diff --git a/GNUmakefile b/GNUmakefile index ca38f5c..35da1d9 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -60,6 +60,9 @@ test: build false: src/false.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs +fop: src/fop.rs build_dir + $(RUSTC) $(RUSTCFLAGS) -o build/bin/fop src/fop.rs + intcmp: src/intcmp.c build_dir $(CC) $(CFLAGS) -o build/bin/intcmp src/intcmp.c diff --git a/src/fop.rs b/src/fop.rs new file mode 100644 index 0000000..9c25463 --- /dev/null +++ b/src/fop.rs @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + */ + +use std::{ + env::args, + io::{ Read, stdin, Write }, + process::{ Command, exit, Stdio }, +}; + +fn main() { + let argv = args().collect::>(); + + let index = match argv.get(1) { + Some(i) => { + i.parse::().unwrap_or_else(|_| { + eprintln!("{}: {}: Not an integer.", argv[0], i); + exit(1); + }) + }, + None => { + eprintln!("Usage: {} index command args...", argv[0]); + exit(1); + }, + }; + + let mut buf = String::new(); + stdin().read_to_string(&mut buf).unwrap(); + let mut fields = buf.split('␞').collect::>(); + + argv.get(2).unwrap_or_else(|| { + eprintln!("Usage: {} index command args...", argv[0]); + exit(1); + }); + + let opts = argv.iter().clone().skip(3).collect::>(); + + let mut spawned = Command::new(argv.get(2).unwrap()) + .args(opts) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let field = fields.get(index).unwrap_or_else(|| { + eprintln!("{}: {}: No such index in input.", argv[0], index.to_string()); + exit(1); + }); + + if let Some(mut child_stdin) = spawned.stdin.take() { + child_stdin.write_all(field.as_bytes()).unwrap(); + drop(child_stdin); + } + + let output = spawned.wait_with_output().unwrap(); + + let new_field = String::from_utf8(output.stdout).unwrap(); + + fields[index] = &new_field; + + print!("{}", fields.join("␞")); +} From 5d9f6f3245847f9f3fda34e807366e9f1dfe39f1 Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 16:55:50 -0700 Subject: [PATCH 02/13] GNUmakefile: fixed fop(1) segfault --- GNUmakefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 35da1d9..64b433f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -20,9 +20,8 @@ CC=cc CFLAGS=-O3 -Lbuild/lib -idirafter include RUSTC=rustc +nightly -RUSTCFLAGS=-Zlocation-detail=none -Copt-level=z -Ccodegen-units=1 \ - -Cpanic=abort -Clto=y -Cstrip=symbols -Ctarget-cpu=native \ - -Clink-args=-Wl,-n,-N,--no-dynamic-linker,--no-pie,--build-id=none +RUSTCFLAGS=-Zlocation-detail=none -Copt-level=z -Ccodegen-units=1 -Cpanic=abort + -Clto=y -Cstrip=symbols -Ctarget-cpu=native ifeq ($(CC), gcc) CFLAGS=-O3 -s -Wl,-z,noseparate-code,-z,nosectionheader -flto -Lbuild/lib \ From 3355cb3dc3705b9fa31841cde1b6f405746f9098 Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 22:42:50 -0700 Subject: [PATCH 03/13] GNUmakefile, fop(1): added sysexits support --- GNUmakefile | 16 +++++++++++----- src/fop.rs | 23 ++++++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 64b433f..b357cd4 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -20,8 +20,9 @@ CC=cc CFLAGS=-O3 -Lbuild/lib -idirafter include RUSTC=rustc +nightly -RUSTCFLAGS=-Zlocation-detail=none -Copt-level=z -Ccodegen-units=1 -Cpanic=abort - -Clto=y -Cstrip=symbols -Ctarget-cpu=native +RUSTCFLAGS=-Zlocation-detail=none -Copt-level=z -Ccodegen-units=1 \ + -Cpanic=abort -Clto=y -Cstrip=symbols -Ctarget-cpu=native \ + --extern sysexits=build/o/libsysexits.rlib ifeq ($(CC), gcc) CFLAGS=-O3 -s -Wl,-z,noseparate-code,-z,nosectionheader -flto -Lbuild/lib \ @@ -39,7 +40,7 @@ endif build: build_dir false intcmp scrut str strcmp true build_dir: - mkdir -p build/o build/lib build/bin + mkdir -p build/bin build/lib build/o clean: rm -rf build/ @@ -56,10 +57,16 @@ test: build tests/cc-compat.sh tests/posix-compat.sh +sysexits: build_dir + bindgen --default-macro-constant-type signed \ + "$$(printf '#include \n' | cpp -M -idirafter include - \ + | sed 's/ /\n/g' | grep sysexits.h)" \ + | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - + false: src/false.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs -fop: src/fop.rs build_dir +fop: src/fop.rs build_dir sysexits $(RUSTC) $(RUSTCFLAGS) -o build/bin/fop src/fop.rs intcmp: src/intcmp.c build_dir @@ -76,4 +83,3 @@ strcmp: src/strcmp.c build_dir true: src/true.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/true src/true.rs - diff --git a/src/fop.rs b/src/fop.rs index 9c25463..287660c 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -18,13 +18,18 @@ use std::{ env::args, - io::{ Read, stdin, Write }, + io::{ stdin, Write }, process::{ Command, exit, Stdio }, }; +extern crate sysexits; +use sysexits::EX_USAGE; + fn main() { let argv = args().collect::>(); + let usage = format!("Usage: {} index command [args...]", argv[0]); + let index = match argv.get(1) { Some(i) => { i.parse::().unwrap_or_else(|_| { @@ -33,18 +38,18 @@ fn main() { }) }, None => { - eprintln!("Usage: {} index command args...", argv[0]); - exit(1); + eprintln!("{}", usage); + exit(EX_USAGE); }, }; let mut buf = String::new(); - stdin().read_to_string(&mut buf).unwrap(); + stdin().read_line(&mut buf).unwrap(); let mut fields = buf.split('␞').collect::>(); argv.get(2).unwrap_or_else(|| { - eprintln!("Usage: {} index command args...", argv[0]); - exit(1); + eprintln!("{}", usage); + exit(EX_USAGE); }); let opts = argv.iter().clone().skip(3).collect::>(); @@ -57,7 +62,11 @@ fn main() { .unwrap(); let field = fields.get(index).unwrap_or_else(|| { - eprintln!("{}: {}: No such index in input.", argv[0], index.to_string()); + eprintln!( + "{}: {}: No such index in input.", + argv[0], + index.to_string() + ); exit(1); }); From b74bbd6c921ca745d244d8b491e985ee842b1e9e Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 22:47:06 -0700 Subject: [PATCH 04/13] fop(1): finished implementing sysexits --- src/fop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fop.rs b/src/fop.rs index 287660c..68cc06b 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -23,7 +23,7 @@ use std::{ }; extern crate sysexits; -use sysexits::EX_USAGE; +use sysexits::{ EX_DATAERR, EX_USAGE }; fn main() { let argv = args().collect::>(); @@ -67,7 +67,7 @@ fn main() { argv[0], index.to_string() ); - exit(1); + exit(EX_DATAERR); }); if let Some(mut child_stdin) = spawned.stdin.take() { From 9ed70e96488b3902a282e8a356e68e4a74610792 Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 22:48:36 -0700 Subject: [PATCH 05/13] GNUmakefile: removed stdlib dep --- GNUmakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GNUmakefile b/GNUmakefile index b357cd4..e53ec6f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -58,7 +58,7 @@ test: build tests/posix-compat.sh sysexits: build_dir - bindgen --default-macro-constant-type signed \ + bindgen --default-macro-constant-type signed --use-core \ "$$(printf '#include \n' | cpp -M -idirafter include - \ | sed 's/ /\n/g' | grep sysexits.h)" \ | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - From 5df9c33d704997db64224b043547ee68c7c1ae6e Mon Sep 17 00:00:00 2001 From: emma Date: Wed, 27 Dec 2023 23:20:31 -0700 Subject: [PATCH 06/13] GNUmakefile: bandaid solution to fallback sysexits.h not working with Rust --- GNUmakefile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index e53ec6f..da8a673 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,7 +40,9 @@ endif build: build_dir false intcmp scrut str strcmp true build_dir: - mkdir -p build/bin build/lib build/o + # keep build/include until bindgen(1) has stdin support + # https://github.com/rust-lang/rust-bindgen/issues/2703 + mkdir -p build/bin build/include build/lib build/o clean: rm -rf build/ @@ -58,8 +60,12 @@ test: build tests/posix-compat.sh sysexits: build_dir + # bandage solution until bindgen(1) gets stdin support + printf '#define EXIT_FAILURE 1\n' | cat - include/sysexits.h \ + > build/include/sysexits.h bindgen --default-macro-constant-type signed --use-core \ - "$$(printf '#include \n' | cpp -M -idirafter include - \ + "$$(printf '#include \n' \ + | cpp -M -idirafter "$$PWD/build/include" - \ | sed 's/ /\n/g' | grep sysexits.h)" \ | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - From cb12a5b8fc37032adcf752c9056361fbeebd4b53 Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 28 Dec 2023 22:33:04 -0700 Subject: [PATCH 07/13] GNUmakefile: relative path for include, formatting --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index da8a673..7722d7f 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -65,8 +65,8 @@ sysexits: build_dir > build/include/sysexits.h bindgen --default-macro-constant-type signed --use-core \ "$$(printf '#include \n' \ - | cpp -M -idirafter "$$PWD/build/include" - \ - | sed 's/ /\n/g' | grep sysexits.h)" \ + | cpp -M -idirafter "build/include" - \ + | sed 's/ /\n/g' | grep sysexits.h)" \ | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - false: src/false.rs build_dir From f89a686d3c8b0bcad120e22f1534083ce43ecc0f Mon Sep 17 00:00:00 2001 From: emma Date: Thu, 28 Dec 2023 22:42:05 -0700 Subject: [PATCH 08/13] fop(1): made checking argv better --- src/fop.rs | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/fop.rs b/src/fop.rs index 68cc06b..1ed31fd 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -28,30 +28,20 @@ use sysexits::{ EX_DATAERR, EX_USAGE }; fn main() { let argv = args().collect::>(); - let usage = format!("Usage: {} index command [args...]", argv[0]); + argv.get(2).unwrap_or_else(|| { + eprintln!("Usage: {} index command [args...]", argv[0]); + exit(EX_USAGE); + }); - let index = match argv.get(1) { - Some(i) => { - i.parse::().unwrap_or_else(|_| { - eprintln!("{}: {}: Not an integer.", argv[0], i); - exit(1); - }) - }, - None => { - eprintln!("{}", usage); - exit(EX_USAGE); - }, - }; + let index = argv[1].parse::().unwrap_or_else(|_| { + eprintln!("{}: {}: Not an integer.", argv[0], argv[1]); + exit(EX_DATAERR); + }); let mut buf = String::new(); stdin().read_line(&mut buf).unwrap(); let mut fields = buf.split('␞').collect::>(); - argv.get(2).unwrap_or_else(|| { - eprintln!("{}", usage); - exit(EX_USAGE); - }); - let opts = argv.iter().clone().skip(3).collect::>(); let mut spawned = Command::new(argv.get(2).unwrap()) From 0c2223d4fb1a11315caf367b67dc3525f5d4a1c1 Mon Sep 17 00:00:00 2001 From: emma Date: Fri, 29 Dec 2023 15:17:39 -0700 Subject: [PATCH 09/13] getopt-rs(3): added getopt library for Rust --- GNUmakefile | 8 +- src/getopt-rs/error.rs | 95 +++++++++ src/getopt-rs/errorkind.rs | 61 ++++++ src/getopt-rs/lib.rs | 72 +++++++ src/getopt-rs/opt.rs | 89 +++++++++ src/getopt-rs/parser.rs | 382 +++++++++++++++++++++++++++++++++++++ src/getopt-rs/result.rs | 59 ++++++ src/getopt-rs/tests.rs | 228 ++++++++++++++++++++++ 8 files changed, 992 insertions(+), 2 deletions(-) create mode 100644 src/getopt-rs/error.rs create mode 100644 src/getopt-rs/errorkind.rs create mode 100644 src/getopt-rs/lib.rs create mode 100644 src/getopt-rs/opt.rs create mode 100644 src/getopt-rs/parser.rs create mode 100644 src/getopt-rs/result.rs create mode 100644 src/getopt-rs/tests.rs diff --git a/GNUmakefile b/GNUmakefile index 7c03eea..ee77926 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,7 +40,7 @@ endif build: build_dir false intcmp scrut str strcmp true build_dir: - mkdir -p build/o build/lib build/bin + mkdir -p build/o build/lib build/bin build/test clean: rm -rf build/ @@ -53,9 +53,10 @@ install: build cp -f docs/*.1 $(PREFIX)/share/man/man1/ # cp -f docs/*.3 $(PREFIX)/share/man/man3/ -test: build +test: build_dir tests/cc-compat.sh tests/posix-compat.sh src/getopt-rs/tests.rs tests/cc-compat.sh tests/posix-compat.sh + $(RUSTC) --test src/getopt-rs/lib.rs -o build/test/getopt false: src/false.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs @@ -75,3 +76,6 @@ strcmp: src/strcmp.c build_dir true: src/true.rs build_dir $(RUSTC) $(RUSTCFLAGS) -o build/bin/true src/true.rs +libgetopt: src/getopt-rs/lib.rs + $(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \ + -o build/o/libgetopt.rlib src/lib.rs diff --git a/src/getopt-rs/error.rs b/src/getopt-rs/error.rs new file mode 100644 index 0000000..322af02 --- /dev/null +++ b/src/getopt-rs/error.rs @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::{ error, fmt }; + +use crate::ErrorKind::{ self, * }; + +/// A basic error type for [`Parser`](struct.Parser.html) +#[derive(Debug, Eq, PartialEq)] +pub struct Error { + culprit: char, + kind: ErrorKind, +} + +impl Error { + /// Creates a new error using a known kind and the character that caused the + /// issue. + pub fn new(kind: ErrorKind, culprit: char) -> Self { + Self { culprit, kind } + } + + /// Returns the [`ErrorKind`](enum.ErrorKind.html) for this error. + pub fn kind(self) -> ErrorKind { + self.kind + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.kind { + MissingArgument => write!( + f, + "option requires an argument -- {:?}", + self.culprit, + ), + UnknownOption => write!(f, "unknown option -- {:?}", self.culprit), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} diff --git a/src/getopt-rs/errorkind.rs b/src/getopt-rs/errorkind.rs new file mode 100644 index 0000000..5475d8e --- /dev/null +++ b/src/getopt-rs/errorkind.rs @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/// What kinds of errors [`Parser`](struct.Parser.html) can return. +#[derive(Debug, Eq, PartialEq)] +pub enum ErrorKind { + /// An argument was not found for an option that was expecting one. + MissingArgument, + /// An unknown option character was encountered. + UnknownOption, +} diff --git a/src/getopt-rs/lib.rs b/src/getopt-rs/lib.rs new file mode 100644 index 0000000..62f0e0d --- /dev/null +++ b/src/getopt-rs/lib.rs @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +//! # getopt +//! +//! `getopt` provides a minimal, (essentially) POSIX-compliant option parser. + +pub use crate::{ + error::Error, + errorkind::ErrorKind, + opt::Opt, + parser::Parser, + result::Result +}; + +mod error; +mod errorkind; +mod opt; +mod parser; +mod result; +#[cfg(test)] +mod tests; diff --git a/src/getopt-rs/opt.rs b/src/getopt-rs/opt.rs new file mode 100644 index 0000000..05b51e6 --- /dev/null +++ b/src/getopt-rs/opt.rs @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::fmt; + +/// A single option. +/// +/// For `Opt(x, y)`: +/// - `x` is the character representing the option. +/// - `y` is `Some` string, or `None` if no argument was expected. +/// +/// # Example +/// +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "foo"]; +/// # let args: Vec = vec!["program", "-abc", "foo"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let optstring = "ab:c"; +/// let mut opts = getopt::Parser::new(&args, optstring); +/// +/// assert_eq!(Opt('a', None), opts.next().transpose()?.unwrap()); +/// assert_eq!(Opt('b', Some("c".to_string())), opts.next().transpose()?.unwrap()); +/// assert_eq!(None, opts.next().transpose()?); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Opt(pub char, pub Option); + +impl fmt::Display for Opt { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Opt({:?}, {:?})", self.0, self.1) + } +} diff --git a/src/getopt-rs/parser.rs b/src/getopt-rs/parser.rs new file mode 100644 index 0000000..6f06cc3 --- /dev/null +++ b/src/getopt-rs/parser.rs @@ -0,0 +1,382 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::collections::HashMap; + +use crate::{ error::Error, errorkind::ErrorKind, opt::Opt, result::Result }; + +/// The core of the `getopt` crate. +/// +/// `Parser` is implemented as an iterator over the options present in the given +/// argument vector. +/// +/// The method [`next`](#method.next) does the heavy lifting. +/// +/// # Examples +/// +/// ## Simplified usage: +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "foo"]; +/// # let args: Vec = vec!["program", "-abc", "foo"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let mut opts = getopt::Parser::new(&args, "ab:c"); +/// +/// assert_eq!(Some(Opt('a', None)), opts.next().transpose()?); +/// assert_eq!(1, opts.index()); +/// assert_eq!(Some(Opt('b', Some("c".to_string()))), opts.next().transpose()?); +/// assert_eq!(2, opts.index()); +/// assert_eq!(None, opts.next()); +/// assert_eq!(2, opts.index()); +/// assert_eq!("foo", args[opts.index()]); +/// # Ok(()) +/// # } +/// ``` +/// +/// ## A more idiomatic example: +/// ``` +/// # fn main() -> Result<(), Box> { +/// use getopt::Opt; +/// +/// // args = ["program", "-abc", "-d", "foo", "-e", "bar"]; +/// # let mut args: Vec = vec!["program", "-abc", "-d", "foo", "-e", "bar"] +/// # .into_iter() +/// # .map(String::from) +/// # .collect(); +/// let mut opts = getopt::Parser::new(&args, "ab:cd:e"); +/// +/// let mut a_flag = false; +/// let mut b_flag = String::new(); +/// let mut c_flag = false; +/// let mut d_flag = String::new(); +/// let mut e_flag = false; +/// +/// loop { +/// match opts.next().transpose()? { +/// None => break, +/// Some(opt) => match opt { +/// Opt('a', None) => a_flag = true, +/// Opt('b', Some(arg)) => b_flag = arg.clone(), +/// Opt('c', None) => c_flag = true, +/// Opt('d', Some(arg)) => d_flag = arg.clone(), +/// Opt('e', None) => e_flag = true, +/// _ => unreachable!(), +/// }, +/// } +/// } +/// +/// let new_args = args.split_off(opts.index()); +/// +/// assert_eq!(true, a_flag); +/// assert_eq!("c", b_flag); +/// assert_eq!(false, c_flag); +/// assert_eq!("foo", d_flag); +/// assert_eq!(true, e_flag); +/// +/// assert_eq!(1, new_args.len()); +/// assert_eq!("bar", new_args.first().unwrap()); +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Eq, PartialEq)] +pub struct Parser { + opts: HashMap, + args: Vec>, + index: usize, + point: usize, +} + +impl Parser { + /// Create a new `Parser`, which will process the arguments in `args` + /// according to the options specified in `optstring`. + /// + /// For compatibility with + /// [`std::env::args`](https://doc.rust-lang.org/std/env/fn.args.html), + /// valid options are expected to begin at the second element of `args`, and + /// `index` is + /// initialised to `1`. + /// If `args` is structured differently, be sure to call + /// [`set_index`](#method.set_index) before the first invocation of + /// [`next`](#method.next). + /// + /// `optstring` is a string of recognised option characters; if a character + /// is followed by a colon (`:`), that option takes an argument. + /// + /// # Note: + /// Transforming the OS-specific argument strings into a vector of `String`s + /// is the sole responsibility of the calling program, as it involves some + /// level of potential information loss (which this crate does not presume + /// to handle unilaterally) and error handling (which would complicate the + /// interface). + pub fn new(args: &[String], optstring: &str) -> Self { + let optstring: Vec = optstring.chars().collect(); + let mut opts = HashMap::new(); + let mut i = 0; + let len = optstring.len(); + + while i < len { + let j = i + 1; + + if j < len && optstring[j] == ':' { + opts.insert(optstring[i], true); + i += 1; + } else { + opts.insert(optstring[i], false); + } + i += 1; + } + + Self { + opts, + // "explode" the args into a vector of character vectors, to allow + // indexing + args: args.iter().map(|e| e.chars().collect()).collect(), + index: 1, + point: 0, + } + } + + /// Return the current `index` of the parser. + /// + /// `args[index]` will always point to the the next element of `args`; when + /// the parser is + /// finished with an element, it will increment `index`. + /// + /// After the last option has been parsed (and [`next`](#method.next) is + /// returning `None`), + /// `index` will point to the first non-option argument. + pub fn index(&self) -> usize { + self.index + } + + // `point` must be reset to 0 whenever `index` is changed + + /// Modify the current `index` of the parser. + pub fn set_index(&mut self, value: usize) { + self.index = value; + self.point = 0; + } + + /// Increment the current `index` of the parser. + /// + /// This use case is common enough to warrant its own optimised method. + pub fn incr_index(&mut self) { + self.index += 1; + self.point = 0; + } +} + +impl Iterator for Parser { + type Item = Result; + + /// Returns the next option, if any. + /// + /// Returns an [`Error`](struct.Error.html) if an unexpected option is + /// encountered or if an + /// expected argument is not found. + /// + /// Parsing stops at the first non-hyphenated argument; or at the first + /// argument matching "-"; + /// or after the first argument matching "--". + /// + /// When no more options are available, `next` returns `None`. + /// + /// # Examples + /// + /// ## "-" + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-", "-a"]; + /// # let args: Vec = vec!["program", "-", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!(None, opts.next()); + /// assert_eq!("-", args[opts.index()]); + /// ``` + /// + /// ## "--" + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "--", "-a"]; + /// # let args: Vec = vec!["program", "--", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!(None, opts.next()); + /// assert_eq!("-a", args[opts.index()]); + /// ``` + /// + /// ## Unexpected option: + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-b"]; + /// # let args: Vec = vec!["program", "-b"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a"); + /// + /// assert_eq!( + /// "unknown option -- 'b'".to_string(), + /// opts.next().unwrap().unwrap_err().to_string() + /// ); + /// ``` + /// + /// ## Missing argument: + /// ``` + /// use getopt::Parser; + /// + /// // args = ["program", "-a"]; + /// # let args: Vec = vec!["program", "-a"] + /// # .into_iter() + /// # .map(String::from) + /// # .collect(); + /// let mut opts = Parser::new(&args, "a:"); + /// + /// assert_eq!( + /// "option requires an argument -- 'a'".to_string(), + /// opts.next().unwrap().unwrap_err().to_string() + /// ); + /// ``` + fn next(&mut self) -> Option> { + if self.point == 0 { + /* + * Rationale excerpts below taken verbatim from "The Open Group Base + * Specifications Issue 7, 2018 edition", IEEE Std 1003.1-2017 + * (Revision of IEEE Std 1003.1-2008). + * Copyright © 2001-2018 IEEE and The Open Group. + */ + + /* + * If, when getopt() is called: + * argv[optind] is a null pointer + * *argv[optind] is not the character '-' + * argv[optind] points to the string "-" + * getopt() shall return -1 without changing optind. + */ + if self.index >= self.args.len() + || self.args[self.index].is_empty() + || self.args[self.index][0] != '-' + || self.args[self.index].len() == 1 + { + return None; + } + + /* + * If: + * argv[optind] points to the string "--" + * getopt() shall return -1 after incrementing index. + */ + if self.args[self.index][1] == '-' && self.args[self.index].len() == 2 { + self.incr_index(); + return None; + } + + // move past the starting '-' + self.point += 1; + } + + let opt = self.args[self.index][self.point]; + self.point += 1; + + match self.opts.get(&opt) { + None => { + if self.point >= self.args[self.index].len() { + self.incr_index(); + } + Some(Err(Error::new(ErrorKind::UnknownOption, opt))) + } + Some(false) => { + if self.point >= self.args[self.index].len() { + self.incr_index(); + } + + Some(Ok(Opt(opt, None))) + } + Some(true) => { + let arg: String = if self.point >= self.args[self.index].len() { + self.incr_index(); + if self.index >= self.args.len() { + return Some(Err(Error::new( + ErrorKind::MissingArgument, + opt, + ))); + } + self.args[self.index].iter().collect() + } else { + self.args[self.index] + .clone() + .split_off(self.point) + .iter() + .collect() + }; + + self.incr_index(); + + Some(Ok(Opt(opt, Some(arg)))) + } + } + } +} diff --git a/src/getopt-rs/result.rs b/src/getopt-rs/result.rs new file mode 100644 index 0000000..015a402 --- /dev/null +++ b/src/getopt-rs/result.rs @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use std::result; + +use crate::error::Error; + +/// A specialized `Result` type for use with [`Parser`](struct.Parser.html) +pub type Result = result::Result; diff --git a/src/getopt-rs/tests.rs b/src/getopt-rs/tests.rs new file mode 100644 index 0000000..c53d517 --- /dev/null +++ b/src/getopt-rs/tests.rs @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Emma Tebibyte + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * 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/. + * + * This file incorporates work covered by the following copyright and permission + * notice: + * The Clear BSD License + * + * Copyright © 2017-2023 David Wildasin + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted (subject to the limitations in the disclaimer + * below) provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions, and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions, and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED + * BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +use crate::{Opt, Parser}; + +macro_rules! basic_test { + ($name:ident, $expect:expr, $next:expr, [$($arg:expr),+], $optstr:expr) => ( + #[test] + fn $name() -> Result<(), String> { + let expect: Option = $expect; + let args: Vec = vec![$($arg),+] + .into_iter() + .map(String::from) + .collect(); + let next: Option = $next; + let mut opts = Parser::new(&args, $optstr); + + match opts.next().transpose() { + Err(error) => { + return Err(format!("next() returned {:?}", error)) + }, + Ok(actual) => if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ) + }, + }; + + match next { + None => if opts.index() < args.len() { + return Err(format!( + "expected end of args; got {:?}", args[opts.index()] + )) + }, + Some(n) => if args[opts.index()] != n { + return Err(format!( + "next arg: expected {:?}; got {:?}", + n, + args[opts.index()] + )) + }, + }; + + Ok(()) + } + ) +} + +#[rustfmt::skip] basic_test!( + blank_arg, None, Some(String::new()), ["x", ""], "a" +); +#[rustfmt::skip] basic_test!( + double_dash, None, Some("-a".to_string()), ["x", "--", "-a", "foo"], "a" +); +#[rustfmt::skip] basic_test!(no_opts_1, None, None, ["x"], "a"); +#[rustfmt::skip] basic_test!( + no_opts_2, None, Some("foo".to_string()), ["x", "foo"], "a" +); +#[rustfmt::skip] basic_test!( + no_opts_3, None, Some("foo".to_string()), ["x", "foo", "-a"], "a" +); +#[rustfmt::skip] basic_test!( + single_dash, None, Some("-".to_string()), ["x", "-", "-a", "foo"], "a" +); +#[rustfmt::skip] basic_test!( + single_opt, + Some(Opt('a', None)), + Some("foo".to_string()), + ["x", "-a", "foo"], + "a" +); +#[rustfmt::skip] basic_test!( + single_optarg, + Some(Opt('a', Some("foo".to_string()))), + None, + ["x", "-a", "foo"], + "a:" +); + +macro_rules! error_test { + ($name:ident, $expect:expr, [$($arg:expr),+], $optstr:expr) => ( + #[test] + fn $name() -> Result<(), String> { + let expect: String = $expect.to_string(); + let args: Vec = vec![$($arg),+] + .into_iter() + .map(String::from) + .collect(); + let mut opts = Parser::new(&args, $optstr); + + match opts.next() { + None => { + return Err(format!( + "unexpected successful response: end of options" + )) + }, + Some(Err(actual)) => { + let actual = actual.to_string(); + + if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ); + } + }, + Some(Ok(opt)) => { + return Err( + format!("unexpected successful response: {:?}", opt) + ) + }, + }; + + Ok(()) + } + ) +} + +#[rustfmt::skip] error_test!( + bad_opt, + "unknown option -- 'b'", + ["x", "-b"], + "a" +); + +#[rustfmt::skip] error_test!( + missing_optarg, + "option requires an argument -- 'a'", + ["x", "-a"], + "a:" +); + +#[test] +fn multiple() -> Result<(), String> { + let args: Vec = vec!["x", "-abc", "-d", "foo", "-e", "bar"] + .into_iter() + .map(String::from) + .collect(); + let optstring = "ab:d:e".to_string(); + let mut opts = Parser::new(&args, &optstring); + + macro_rules! check_result { + ($expect:expr) => { + let expect: Option = $expect; + match opts.next().transpose() { + Err(error) => { + return Err(format!("next() returned {:?}", error)); + }, + Ok(actual) => { + if actual != expect { + return Err( + format!("expected {:?}; got {:?}", expect, actual) + ); + } + } + }; + }; + } + + check_result!(Some(Opt('a', None))); + check_result!(Some(Opt('b', Some("c".to_string())))); + check_result!(Some(Opt('d', Some("foo".to_string())))); + check_result!(Some(Opt('e', None))); + check_result!(None); + + Ok(()) +} + +#[test] +fn continue_after_error() { + let args: Vec = vec!["x", "-z", "-abc"] + .into_iter() + .map(String::from) + .collect(); + let optstring = "ab:d:e".to_string(); + for _opt in Parser::new(&args, &optstring) { + // do nothing, should not panic + } +} From 622c13021d23d616a03b71cec0b5ca67926514be Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 15 Jan 2024 14:34:59 -0700 Subject: [PATCH 10/13] GNUmakefile: replaced with POSIX Makefile --- .gitignore | 1 + GNUmakefile => Makefile | 73 +++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 39 deletions(-) rename GNUmakefile => Makefile (55%) diff --git a/.gitignore b/.gitignore index 567609b..edd9d60 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build/ +dist/ diff --git a/GNUmakefile b/Makefile similarity index 55% rename from GNUmakefile rename to Makefile index 7722d7f..871b5ba 100644 --- a/GNUmakefile +++ b/Makefile @@ -7,9 +7,10 @@ # permitted in any medium without royalty provided the copyright notice and this # notice are preserved. This file is offered as-is, without any warranty. -# If we want to use POSIX make we can’t use ifeq -# .POSIX: -# .PRAGMA: posix_202x +.POSIX: +.PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1) + +.PHONY: all .PHONY: clean .PHONY: install .PHONY: test @@ -19,73 +20,67 @@ PREFIX=/usr/local CC=cc CFLAGS=-O3 -Lbuild/lib -idirafter include -RUSTC=rustc +nightly -RUSTCFLAGS=-Zlocation-detail=none -Copt-level=z -Ccodegen-units=1 \ - -Cpanic=abort -Clto=y -Cstrip=symbols -Ctarget-cpu=native \ +RUSTC=rustc +RUSTCFLAGS=-Copt-level=z -Ccodegen-units=1 -Cpanic=abort -Clto=y \ + -Cstrip=symbols -Ctarget-cpu=native \ --extern sysexits=build/o/libsysexits.rlib -ifeq ($(CC), gcc) - CFLAGS=-O3 -s -Wl,-z,noseparate-code,-z,nosectionheader -flto -Lbuild/lib \ - -idirafter include -endif +all: false fop intcmp scrut str strcmp true test -ifeq ($(CC), clang) - CFLAGS=-O3 -Wall -Lbuild/lib -idirafter include -endif - -ifeq ($(CC), tcc) - CFLAGS=-O3 -s -Wl -flto -Lbuild/lib -idirafter include -endif - -build: build_dir false intcmp scrut str strcmp true - -build_dir: +build: # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 - mkdir -p build/bin build/include build/lib build/o + mkdir -p build/bin build/include build/o # build/lib clean: - rm -rf build/ + rm -rf build/ dist/ -install: build - mkdir -p $(PREFIX)/bin $(PREFIX)/lib - mkdir -p $(PREFIX)/man/man1 $(PREFIX)/man/man3 - cp -f build/lib/*.so $(PREFIX)/lib/ - cp -f build/bin/* $(PREFIX)/bin/ - cp -f docs/*.1 $(PREFIX)/man/man1/ - cp -f docs/*.3 $(PREFIX)/man/man3/ +dist: all + mkdir -p \ + dist/bin \ + dist/man/man1 \ + # dist/$(PREFIX)/lib \ + # dist/$(PREFIX)/man/man3 + cp build/bin/* dist/bin/ + # cp build/lib/* dist/$(PREFIX)/lib/ + cp docs/*.1 dist/man/man1/ + # cp docs/*.3 dist/$(PREFIX)/man/man3/ + +install: dist + mkdir -p $(PREFIX) + cp -r dist/* $(PREFIX)/ test: build tests/cc-compat.sh tests/posix-compat.sh -sysexits: build_dir +sysexits: build # bandage solution until bindgen(1) gets stdin support printf '#define EXIT_FAILURE 1\n' | cat - include/sysexits.h \ > build/include/sysexits.h - bindgen --default-macro-constant-type signed --use-core \ + bindgen --default-macro-constant-type signed --use-core --formatter=none \ "$$(printf '#include \n' \ | cpp -M -idirafter "build/include" - \ | sed 's/ /\n/g' | grep sysexits.h)" \ | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - -false: src/false.rs build_dir +false: src/false.rs build $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs -fop: src/fop.rs build_dir sysexits +fop: src/fop.rs build sysexits $(RUSTC) $(RUSTCFLAGS) -o build/bin/fop src/fop.rs -intcmp: src/intcmp.c build_dir +intcmp: src/intcmp.c build $(CC) $(CFLAGS) -o build/bin/intcmp src/intcmp.c -scrut: src/scrut.c build_dir +scrut: src/scrut.c build $(CC) $(CFLAGS) -o build/bin/scrut src/scrut.c -str: src/str.c build_dir +str: src/str.c build $(CC) $(CFLAGS) -o build/bin/str src/str.c -strcmp: src/strcmp.c build_dir +strcmp: src/strcmp.c build $(CC) $(CFLAGS) -o build/bin/strcmp src/strcmp.c -true: src/true.rs build_dir +true: src/true.rs build $(RUSTC) $(RUSTCFLAGS) -o build/bin/true src/true.rs From 49a3bb9f30a2d492b20420a504fe4e331eb9f0d0 Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 15 Jan 2024 15:30:21 -0700 Subject: [PATCH 11/13] Makefile, .gitignore, .editorconfig, configure, tests/cc-compat.sh: added configure script for compiler optimizations --- .editorconfig | 3 +++ .gitignore | 1 + Makefile | 20 +++++++++----------- configure | 39 +++++++++++++++++++++++++++++++++++++++ tests/cc-compat.sh | 5 +++-- 5 files changed, 55 insertions(+), 13 deletions(-) create mode 100755 configure diff --git a/.editorconfig b/.editorconfig index 416f893..30fb7fc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,6 @@ end_of_line = lf indent_style = tab indent_size = 4 insert_final_newline = true + +[configure] +indent_size = 2 diff --git a/.gitignore b/.gitignore index edd9d60..3882502 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ dist/ +*.mk diff --git a/Makefile b/Makefile index 871b5ba..951044b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# Copyright (c) 2023 Emma Tebibyte +# Copyright (c) 2023–2024 Emma Tebibyte # Copyright (c) 2023 DTB # Copyright (c) 2023 Sasha Koshka # SPDX-License-Identifier: FSFAP @@ -18,19 +18,17 @@ PREFIX=/usr/local CC=cc -CFLAGS=-O3 -Lbuild/lib -idirafter include - RUSTC=rustc -RUSTCFLAGS=-Copt-level=z -Ccodegen-units=1 -Cpanic=abort -Clto=y \ - -Cstrip=symbols -Ctarget-cpu=native \ - --extern sysexits=build/o/libsysexits.rlib -all: false fop intcmp scrut str strcmp true test +# to build, first run ./configure +include *.mk + +all: false fop intcmp scrut str strcmp true build: # keep build/include until bindgen(1) has stdin support # https://github.com/rust-lang/rust-bindgen/issues/2703 - mkdir -p build/bin build/include build/o # build/lib + mkdir -p build/bin build/include build/o build/lib clean: rm -rf build/ dist/ @@ -65,10 +63,10 @@ sysexits: build | $(RUSTC) $(RUSTCFLAGS) --crate-type lib -o build/o/libsysexits.rlib - false: src/false.rs build - $(RUSTC) $(RUSTCFLAGS) -o build/bin/false src/false.rs + $(RUSTC) $(RUSTFLAGS) -o build/bin/false src/false.rs fop: src/fop.rs build sysexits - $(RUSTC) $(RUSTCFLAGS) -o build/bin/fop src/fop.rs + $(RUSTC) $(RUSTFLAGS) -o build/bin/fop src/fop.rs intcmp: src/intcmp.c build $(CC) $(CFLAGS) -o build/bin/intcmp src/intcmp.c @@ -83,4 +81,4 @@ strcmp: src/strcmp.c build $(CC) $(CFLAGS) -o build/bin/strcmp src/strcmp.c true: src/true.rs build - $(RUSTC) $(RUSTCFLAGS) -o build/bin/true src/true.rs + $(RUSTC) $(RUSTFLAGS) -o build/bin/true src/true.rs diff --git a/configure b/configure new file mode 100755 index 0000000..eb1c8ae --- /dev/null +++ b/configure @@ -0,0 +1,39 @@ +#!/bin/sh + +# Copyright (c) 2023–2024 Emma Tebibyte +# SPDX-License-Identifier: FSFAP +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice and this +# notice are preserved. This file is offered as-is, without any warranty. + +set -e + +CFLAGS='-Lbuild/lib -idirafter include -O3' +RUSTFLAGS='-Copt-level=z -Ccodegen-units=1 -Cpanic=abort -Clto=y \ + -Cstrip=symbols -Ctarget-cpu=native \ + --extern sysexits=build/o/libsysexits.rlib' + +case "$@" in + clang) + CFLAGS="$CFLAGS -Wall" + ;; + clean) + rm *.mk || true + exit 0 + ;; + gcc) + CFLAGS="$CFLAGS -s -Wl,-z,noseparate-code,-z,nosectionheader -flto" + ;; + 'rustc +nightly') + RUSTFLAGS="+nightly -Zlocation-detail=none $RUSTFLAGS" + ;; + '') ;; + *) + printf 'Usage: %s [compiler]\n' "$0" + exit 64 # sysexits.h(3) EX_USAGE + ;; +esac + +printf 'CFLAGS=%s\n' "$CFLAGS" >cc.mk +printf 'RUSTFLAGS=%s\n' "$RUSTFLAGS" >rustc.mk diff --git a/tests/cc-compat.sh b/tests/cc-compat.sh index f6f66ea..b5762c8 100755 --- a/tests/cc-compat.sh +++ b/tests/cc-compat.sh @@ -9,18 +9,19 @@ set -e -if ! ls GNUmakefile >/dev/null 2>&1 +if ! ls Makefile >/dev/null 2>&1 then printf '%s: Run this script in the root of the project.\n' "$0" 1>&2 exit 64 # sysexits.h(3) EX_USAGE fi make clean +./configure clean +./configure for CC in cc \ clang \ gcc \ - tcc \ 'zig cc' do export CC From 52def0a32d4d1bf4b5a8be9c567cf4d180d70885 Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 15 Jan 2024 16:30:00 -0700 Subject: [PATCH 12/13] Makefile, fop(1): added argument parsing and -d option --- Makefile | 2 +- src/fop.rs | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index bee2f00..33cc695 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ sysexits: build libgetopt: src/getopt-rs/lib.rs $(RUSTC) $(RUSTCFLAGS) --crate-type=lib --crate-name=getopt \ - -o build/o/libgetopt.rlib src/lib.rs + -o build/o/libgetopt.rlib src/getopt-rs/lib.rs false: src/false.rs build $(RUSTC) $(RUSTFLAGS) -o build/bin/false src/false.rs diff --git a/src/fop.rs b/src/fop.rs index 1ed31fd..cd04f85 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -18,33 +18,54 @@ use std::{ env::args, - io::{ stdin, Write }, + io::{ Read, stdin, Write }, process::{ Command, exit, Stdio }, }; extern crate sysexits; +extern crate getopt; + +use getopt::{ Opt, Parser }; use sysexits::{ EX_DATAERR, EX_USAGE }; fn main() { let argv = args().collect::>(); + let mut d = '␞'; + let mut arg_parser = Parser::new(&argv, "d:"); - argv.get(2).unwrap_or_else(|| { + while let Some(opt) = arg_parser.next() { + match opt { + Ok(Opt('d', Some(arg))) => { + let arg_char = arg.chars().collect::>(); + if arg_char.len() > 1 { + eprintln!("{}: {}: Not a character.", argv[0], arg); + exit(EX_USAGE); + } else { d = arg_char[0]; } + }, + _ => {}, + }; + } + + let index_arg = arg_parser.index(); + let command_arg = arg_parser.index() + 1; + + argv.get(command_arg).unwrap_or_else(|| { eprintln!("Usage: {} index command [args...]", argv[0]); exit(EX_USAGE); }); - let index = argv[1].parse::().unwrap_or_else(|_| { + let index = argv[index_arg].parse::().unwrap_or_else(|_| { eprintln!("{}: {}: Not an integer.", argv[0], argv[1]); exit(EX_DATAERR); }); let mut buf = String::new(); - stdin().read_line(&mut buf).unwrap(); - let mut fields = buf.split('␞').collect::>(); + stdin().read_to_string(&mut buf).unwrap(); + let mut fields = buf.split(d).collect::>(); - let opts = argv.iter().clone().skip(3).collect::>(); + let opts = argv.iter().clone().skip(command_arg + 1).collect::>(); - let mut spawned = Command::new(argv.get(2).unwrap()) + let mut spawned = Command::new(argv.get(command_arg).unwrap()) .args(opts) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -71,5 +92,5 @@ fn main() { fields[index] = &new_field; - print!("{}", fields.join("␞")); + print!("{}", fields.join(&d.to_string())); } From 28becbafbcd866cab55df86563a88ba23873a484 Mon Sep 17 00:00:00 2001 From: emma Date: Mon, 15 Jan 2024 16:33:51 -0700 Subject: [PATCH 13/13] fop(1): updated copyright year and usage info --- src/fop.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fop.rs b/src/fop.rs index cd04f85..4f80bd6 100644 --- a/src/fop.rs +++ b/src/fop.rs @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Emma Tebibyte + * Copyright (c) 2023–2024 Emma Tebibyte * SPDX-License-Identifier: AGPL-3.0-or-later * * This program is free software: you can redistribute it and/or modify it under @@ -50,7 +50,7 @@ fn main() { let command_arg = arg_parser.index() + 1; argv.get(command_arg).unwrap_or_else(|| { - eprintln!("Usage: {} index command [args...]", argv[0]); + eprintln!("Usage: {} [-d delimiter] index command [args...]", argv[0]); exit(EX_USAGE); });