diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/LICENSE b/COPYING similarity index 100% rename from LICENSE rename to COPYING diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d57070b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,288 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "proc-macro2" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "serde" +version = "1.0.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tomcat" +version = "0.1.0" +dependencies = [ + "toml", + "yacexits", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yacexits" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe740dd05c1bbc919431e842e6c1bea30195e0518ae99cae35b7f0730ddc18" +dependencies = [ + "bindgen", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3684870 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "tomcat" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = [ "Emma Tebibyte " ] + +[dependencies] +toml = "0.5.9" +yacexits = "0.1.2" diff --git a/README.md b/README.md index 0e8f751..37199b7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,33 @@ -`tomcat` – a [TOML](https://toml.io) parser in POSIX shell +`tomcat` – a minimal [TOML](https://toml.io) parser for the command line. ## Installation ### From Source -Clone this repository and move the `tomcat` binary wherever your operating -system stores locally-installed binaries. This is usually `/usr/local/bin` or -`$HOME/.local/bin` for your user. Make sure the installation location is in your -`$PATH`. +1. Run `cargo --install --git https://git.tebibyte.media/emma/tomcat.git` +2. ??? +3. Profit + +Also make sure that your cargo `bin` directory (by default in +`"$CARGO_HOME"/bin`) is in your `$PATH`; I recommend symlinking the folder to +`"$HOME/.local/bin` and adding that to your user `$PATH`. + +## Usage + +Assuming the TOML file being parsed is `file.toml`: + +To get the value of top-level key `notable`: + +`tomcat notable file.toml` + +To get the value of `key` from the `main` table: + +`tomcat main.key file.toml` + +To get the value of `key` from the subtable `subtab` in table `main`: + +`tomcat main.subtab.key file.toml` + +To get the value of index `3` in array `arr` in table `main`: + +`tomcat main.arr[3] file.toml` diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a5cee39 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2022–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, + fs::File, + io::Read, + iter::Peekable, + os::fd::{ FromRawFd }, + path::Path, + str::FromStr, +}; + +use toml::Value; +use yacexits::*; + +fn parse_toml( + mut root: Value, + mut tabkey: Peekable>, + index: Option, +) -> Result { + let mut out = String::new(); + + while let Some(item) = tabkey.next() { + let mut value = match root.get(item) { + Some(val) => val, + None => { + return Err((format!("{}: No such table or key.", item), EX_DATAERR)); + }, + }; + + match value { + Value::Array(array) => { + let i = match index { + Some(i) => i, + None => { + for v in array.iter() { + out.push_str(&format!("{}\n", v.to_string())); + } + continue; + }, + }; + + match array.get(i) { + Some(val) => value = val, + None => { + return Err((format!("No value at index {}.", i), EX_DATAERR)); + }, + }; + }, + Value::Table(table) => { + if tabkey.peek().is_some() { + root = toml::Value::Table(table.to_owned()); + continue; + }; + }, + _ => { + if tabkey.peek().is_some() { + return Err((format!("{}: Not a table.", item), EX_DATAERR)); + } + }, + }; + + match value { + Value::Array(_) => {}, + Value::Boolean(boolean) => out.push_str(&format!("{:?}\n", boolean)), + Value::Datetime(datetime) => out.push_str(&format!("{:?}\n", datetime)), + Value::Float(float) => out.push_str(&format!("{:?}\n", float)), + Value::Integer(int) => out.push_str(&format!("{:?}\n", int)), + Value::String(string) => { + let contents = string.to_owned(); + let mut lines: Vec<&str> = contents.lines().collect(); + if lines.last().unwrap().is_empty() { _ = lines.pop(); } + + for line in lines.iter() { out.push_str(&format!("{}\n", line)); } + }, + _ => return Err((format!("{:?}: No such key.", item), EX_DATAERR)), + }; + } + Ok(out) +} + +fn main() { + let argv: Vec = args().collect(); + let usage_info = format!("Usage: {} [table.]key[[index]] [file...]", argv[0]); + + if argv.len() <= 1 { + eprintln!("{}", usage_info); + exit(EX_USAGE); + } + + let input = argv.get(2).unwrap_or(&"".to_owned()).to_owned(); + + let mut content = Vec::new(); + + match input.as_str() { + "-" | "" => unsafe { File::from_raw_fd(0) }, + _ => { + File::open(Path::new(&input)).unwrap_or_else(|_| { + eprintln!( + "{}: {}: No such file or directory.", + argv[0], + &input + ); + exit(EX_UNAVAILABLE); + }) + }, + }.read_to_end(&mut content).unwrap_or_else(|_| { + eprintln!("{}: Could not read input.", argv[0]); + exit(EX_OSERR); + }); + + + let mut tabkey: Vec<&str> = argv[1].split(".").collect(); + let mut indexvec = Vec::new(); + let mut index: Option = None; + + if tabkey.iter().skip(1).peekable().peek().is_some() { + indexvec = tabkey[1].split(&['[', ']'][..]).collect(); + tabkey[1] = indexvec.remove(0); + }; + + if ! indexvec.is_empty() { + let istr = indexvec.remove(0); + match usize::from_str(istr) { + Ok(i) => index = Some(i), + Err(_) => { + eprintln!("{}: {}: Cannot index by given value.", argv[0], istr); + exit(EX_USAGE); + }, + }; + } + + let root = String::from_utf8(content) + .unwrap_or_else(|_| { + eprintln!("{}: Input is not valid UTF-8.", argv[0]); + exit(EX_DATAERR); + }) + .parse::() + .unwrap_or_else(|_| { + eprintln!("{}: Unable to parse TOML.", argv[0]); + exit(EX_DATAERR); + }); + + let valiter = tabkey.iter().peekable(); + print!( + "{}", + match parse_toml(root, valiter, index) { + Ok(val) => val, + Err((err, code)) => { + eprintln!("{}: {}", argv[0], err); + exit(code); + }, + }, + ); +} diff --git a/tomcat b/tomcat deleted file mode 100755 index f59bac2..0000000 --- a/tomcat +++ /dev/null @@ -1,73 +0,0 @@ -#!/bin/sh - -set -e - -argv0="$0" -argv1="$1" -argv2="$2" - -# check usage -if ! test -n "$1"; then - printf "Usage: %s [OPTIONS] [TABLE.KEY[INDEX]] [FILE]\n" "$0" 1>&2 - exit 64 # sysexits(3) EX_USAGE -fi - -# test if input has a period -if test -n "$(printf "%s\n" "$argv1" | sed -n '/.*\..*/p')"; then - # cut out everything beyond the first period for TABLE - TABLE=$(printf "%s\n" "$argv1" | sed -n 's/\..*//p') - - # cut out everything before the first period for KEY - KEY=$(printf "%s\n" "$argv1" | sed -n 's/^[^.]*\.//p') - ! test -n "$KEY" &&\ - printf "%s: No key specified\n" "$argv0" 1>&2 &&\ - exit 65 # sysexits(3) EX_DATAERR -else - printf "%s: No key specified\n" "$argv0" 1>&2 - exit 65 # sysexits(3) EX_DATAERR -fi - -# remove array index from KEY -KEY=$(printf "%s\n" "$KEY" | sed 's/\[.*\]//g') -# set ARR to the array index -ARR=$(printf "%s\n" "$argv1" | sed -n 's/.\+\[//p' | tr -d ']') - -# test if argument 2 is a file or not -if test -e "$argv2"; then - # set TOML to the text from the file - TOML=$(sed 's/[^"#]#\+.*//g' <"$argv2" | sed 's/^#.*//g' ) -else - # set TOML to the text from stdin - TOML=$(printf "%s\n" "$argv2" | sed 's/[^"#]#\+.*//g') -fi - -if test -n "$TABLE"; then - KEYS=$(printf "%s\n" "$TOML" |\ - # output only lines between TABLE and the next table - awk "/^\[$TABLE\]/{flag=1; next} /^\[/{flag=0} flag" - ) -else - KEYS=$(printf "%s\n" "$TOML" |\ - # output only lines before the first table - awk '1;/^\[/{exit}' - | sed -n '/^[^[]/p') -fi - -# set VAL to the parsed KEYS list -VAL=$(printf "%s\n" "$KEYS" |\ - # remove the key from the string and delineate arrays, removing - # brackets, trailing commas, and leading spaces - sed -n "s/$KEY *= *//p" | sed 's/", "/ /g' | tr -d '[]"' |\ - sed 's/, *$//g' | sed 's/^ \+//g' | sed 's/ \+$//g') - -# test if ARR is set; if it is, then we have an array index to grab -if test -n "$ARR"; then - # change the line delineator to newlines for parsing and output the result - printf "%s\n" "$VAL" | sed 's/ /\n/g' | head -n "$ARR" |\ - tail -n 1 -elif ! test -n "$VAL"; then - printf "%s: %s: No such key or table\n" "$argv0" "$argv1" 1>&2 - exit 65 # sysexits(3) EX_DATAERR -else - printf "%s\n" "$VAL" -fi - -exit 0