├── .gitattributes ├── .gitignore ├── tools ├── test ├── lib.bash ├── release └── build-release ├── Cargo.toml ├── LICENSE ├── CHANGELOG.md ├── src ├── query_parser.rs └── main.rs ├── README.md ├── test └── test.rs └── Cargo.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | Cargo.lock -diff 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /tools/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | shopt -s globstar 4 | 5 | show_failure() { 6 | echo 7 | echo "FAILED" 8 | } 9 | trap '{ set +x; } 2>&- ; show_failure' EXIT 10 | 11 | # shellcheck disable=SC2046,SC2207 12 | shell_scripts=( $(git ls-files tools/) ) 13 | 14 | set -x 15 | cargo test -q 16 | cargo check -q 17 | cargo clippy -q 18 | shellcheck -P SCRIPTDIR -- "${shell_scripts[@]}" 19 | cargo fmt --check 20 | { set +x; } 2>&- 21 | 22 | trap - EXIT 23 | echo "Success!" 24 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "toml-cli" 3 | version = "0.2.3" 4 | description = "A simple CLI for editing and querying TOML files." 5 | authors = ["Greg Price "] 6 | repository = "https://github.com/gnprice/toml-cli" 7 | readme = "README.md" 8 | license = "MIT" 9 | 10 | edition = "2021" 11 | 12 | [[bin]] 13 | name = "toml" 14 | path = "src/main.rs" 15 | 16 | [[test]] 17 | name = "integration" 18 | path = "test/test.rs" 19 | 20 | [dependencies] 21 | anyhow = "1.0.66" 22 | nom = "7.1.1" 23 | serde = "1.0" 24 | serde_json = "1.0" 25 | structopt = "0.3" 26 | thiserror = "1.0.37" 27 | toml_edit = "0.15" 28 | 29 | [dev-dependencies] 30 | tempfile = "3.3.0" 31 | -------------------------------------------------------------------------------- /tools/lib.bash: -------------------------------------------------------------------------------- 1 | die() { 2 | echo "$1" >&2 3 | exit 1 4 | } 5 | 6 | get_version() { 7 | cargo run -q -- get Cargo.toml package.version --raw 8 | } 9 | 10 | # Set variables for color codes if color appropriate, or empty if not. 11 | # 12 | # Color is deemed appropriate just if stderr is a terminal. 13 | # 14 | # Variables set: reset, bold 15 | prepare_colors() { 16 | local should_color= 17 | if [ -t 2 ]; then 18 | should_color=yes 19 | fi 20 | 21 | reset= 22 | bold= 23 | if [ -n "${should_color}" ]; then 24 | reset=$'\033'[0m 25 | bold=$'\033'[1m 26 | fi 27 | : "${reset}" "${bold}" # dummy use, to reassure shellcheck 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright 2019 Greg Price 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for toml-cli 2 | 3 | ## Unreleased 4 | 5 | * Started publishing release binaries for Linux. These have also been 6 | backfilled for past releases, back to v0.2.1. (#3) 7 | * Switched from `failure` as a dependency to `anyhow` and `thiserror`, 8 | its recommended successors. 9 | 10 | 11 | ## 0.2.3 12 | 13 | * `toml get` on a missing key no longer panics. This gives it the same 14 | behavior as `git config`: print nothing, and exit with failure. (#14) 15 | * Fix query parse error on empty quoted key `""`, 16 | as in `toml get data.toml 'foo."".bar'`. (#20) 17 | 18 | 19 | ## 0.2.2 20 | 21 | * New option `toml get -r` / `--raw`. (#19) 22 | 23 | 24 | ## 0.2.1 25 | 26 | * **Breaking**: Previously `toml get` on a missing key would print "null" 27 | and exit with success. Now it panics. (The panic was filed as #14 and 28 | fixed in v0.2.3. Since v0.2.3 there are also tests that would catch this 29 | sort of unplanned behavior change.) 30 | 31 | * Update `lexical-core` dependency, fixing build on recent Rust toolchains. (#12) 32 | * Update `toml_edit` dependency, fixing parse error on dotted keys. (#2) 33 | * Update dependencies generally. 34 | * Adjust so `cargo fmt` and `cargo clippy` are clean. 35 | 36 | 37 | ## 0.2.0 38 | 39 | * **Breaking**: Change query format from `.foo.bar` to `foo.bar`, 40 | like TOML itself. 41 | 42 | 43 | ## 0.1.0 44 | 45 | Initial release. 46 | 47 | * `toml get`. 48 | * `toml set`, just printing the modified version. 49 | -------------------------------------------------------------------------------- /tools/release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | this_file=$(readlink -f "${BASH_SOURCE[0]}") 5 | this_dir=${this_file%/*} 6 | . "${this_dir}"/lib.bash 7 | 8 | increment_version() { 9 | local bump_type="$1" old_version="$2" 10 | case "${bump_type}" in 11 | patch) echo "${old_version}" \ 12 | | perl -F'\.' -le 'print(join ".", $F[0], $F[1], $F[2]+1)' ;; 13 | minor) echo "${old_version}" \ 14 | | perl -F'\.' -le 'print(join ".", $F[0], $F[1]+1, 0)' ;; 15 | major) echo "${old_version}" \ 16 | | perl -F'\.' -le 'print(join ".", $F[0]+1, 0, 0)' ;; 17 | esac 18 | } 19 | 20 | update_version() { 21 | local old_version="$1" new_version="$2" 22 | cargo run -q -- set Cargo.toml package.version "${new_version}" \ 23 | | sponge Cargo.toml 24 | new_version=$new_version \ 25 | perl -i -0pe 's/^name = "toml-cli"\nversion = \K".*?"/"${ENV{new_version}}"/m' \ 26 | Cargo.lock 27 | old_version=$old_version new_version=$new_version \ 28 | perl -i -0pe 's/^toml-\S+ \K${ENV{old_version}}/${ENV{new_version}}/gm' \ 29 | README.md 30 | new_version=$new_version \ 31 | perl -i -0pe 's/^## Unreleased\n\K/\n\n## ${ENV{new_version}}\n/m' \ 32 | CHANGELOG.md 33 | } 34 | 35 | start_release() { 36 | (( $# == 1 )) || die "usage: tools/release start {patch|minor|major}" 37 | local bump_type="$1" 38 | case "${bump_type}" in 39 | patch|minor|major) ;; 40 | *) die "usage: tools/release start {patch|minor|major}" 41 | esac 42 | 43 | local old_version new_version tag_name 44 | old_version=$(get_version) 45 | new_version=$(increment_version "${bump_type}" "${old_version}") 46 | tag_name=v${new_version} 47 | 48 | update_version "${old_version}" "${new_version}" 49 | 50 | git commit -am "Release version ${new_version}." 51 | git tag "${tag_name}" 52 | 53 | prepare_colors 54 | cat <&2 55 | 56 | Version updated: ${bold}${new_version}${reset} 57 | 58 | Next steps: 59 | 60 | \$ ${bold}git log --stat -p upstream..${reset} # check your work 61 | 62 | \$ ${bold}tools/build-release linux-x86${reset} 63 | 64 | \$ ${bold}git push --atomic upstream main ${tag_name}${reset} 65 | 66 | \$ ${bold}cargo publish${reset} 67 | 68 | * visit ${bold}https://github.com/gnprice/toml-cli/releases${reset} and: 69 | * create release from tag 70 | * add changelog 71 | * upload artifacts ${bold}target/archive/toml-${new_version}-*${reset} 72 | EOF 73 | } 74 | 75 | case "${1-}" in 76 | start) shift && start_release "$@" ;; 77 | *) die "usage: tools/release start ...ARGS" ;; 78 | esac 79 | -------------------------------------------------------------------------------- /tools/build-release: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | this_file=$(readlink -f "${BASH_SOURCE[0]}") 5 | this_dir=${this_file%/*} 6 | . "${this_dir}"/lib.bash 7 | 8 | # Export SOURCE_DATE_EPOCH, computing from Git, if not already present. 9 | # 10 | # This means that if some higher-level build script wants to set this 11 | # variable, we'll follow its choice; otherwise, we use the commit date 12 | # of the current Git commit. 13 | # 14 | # See: https://reproducible-builds.org/docs/source-date-epoch/ 15 | export_source_date_epoch() { 16 | : "${SOURCE_DATE_EPOCH:=$(git log -1 --format=%ct)}" 17 | export SOURCE_DATE_EPOCH 18 | } 19 | 20 | # Like `tar -czf`, but with more-reproducible output. 21 | tar_czf_reproducibly() { 22 | local outfile="$1" 23 | shift 24 | 25 | export_source_date_epoch 26 | 27 | # For this formidable set of `tar` options, see: 28 | # https://reproducible-builds.org/docs/archives/ 29 | tar --sort=name --mtime="@${SOURCE_DATE_EPOCH}" \ 30 | --owner=0 --group=0 --numeric-owner \ 31 | --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime \ 32 | -c "$@" \ 33 | | gzip -n >"${outfile}" 34 | } 35 | 36 | archive() { 37 | local slug_target="$1" target_dir="$2" 38 | local version slug tmpdir staging outdir artifact 39 | 40 | version=$(get_version) 41 | slug="toml-${version}-${slug_target}" 42 | 43 | tmpdir=$(mktemp -d) 44 | staging="${tmpdir}/${slug}" 45 | mkdir -p "${staging}" 46 | cp {README.md,LICENSE,CHANGELOG.md} "${staging}"/ 47 | cp "${target_dir}"/release/toml "${staging}"/ 48 | 49 | outdir=target/archive 50 | artifact="${outdir}"/"${slug}".tar.gz 51 | mkdir -p "${outdir}" 52 | tar_czf_reproducibly "${artifact}" -C "${tmpdir}" "${slug}" 53 | echo "${artifact}" 54 | } 55 | 56 | # Build (a tarball containing) a statically-linked binary for Linux. 57 | build_linux_x86() { 58 | local rust_target=x86_64-unknown-linux-musl 59 | local target_dir=target/"${rust_target}" 60 | 61 | export_source_date_epoch 62 | 63 | cross build --verbose --release --target "${rust_target}" 64 | strip "${target_dir}"/release/toml 65 | 66 | # Call the artifact "toml-0.M.N-x86_64-linux.tar.gz" rather than 67 | # a more puzzling-looking name with "unknown" and "musl". 68 | archive x86_64-linux "${target_dir}" 69 | } 70 | 71 | (( $# == 1 )) || die "usage: tools/build-release TARGET" 72 | opt_target="$1" 73 | 74 | case "${opt_target}" in 75 | linux-x86) build_linux_x86;; 76 | # linux-arm) # TODO 77 | # macos) # TODO 78 | # archive) archive "$@";; # perhaps add for use developing this script 79 | *) die "unknown target: ${opt_target}";; 80 | esac 81 | -------------------------------------------------------------------------------- /src/query_parser.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | /// Query language is simple: a query is a "TOML path", or tpath. 4 | pub struct Query(pub Vec); 5 | 6 | #[derive(Debug, PartialEq, Eq)] 7 | pub enum TpathSegment { 8 | Name(String), 9 | Num(usize), 10 | } 11 | 12 | use nom::{ 13 | branch::alt, 14 | bytes::complete::{escaped_transform, tag, take_while1, take_while_m_n}, 15 | character::complete::{char, digit1, none_of, one_of}, 16 | combinator::{all_consuming, map, map_res}, 17 | error::Error, 18 | multi::many0, 19 | sequence::{delimited, preceded, tuple}, 20 | Err, IResult, 21 | }; 22 | 23 | fn hex_unicode_scalar(len: usize, s: &str) -> IResult<&str, char> { 24 | map_res( 25 | take_while_m_n(len, len, |c: char| c.is_ascii_hexdigit()), 26 | |s: &str| char::try_from(u32::from_str_radix(s, 16).unwrap()), 27 | )(s) 28 | } 29 | 30 | fn basic_string_escape(s: &str) -> IResult<&str, char> { 31 | alt(( 32 | one_of("\\\""), 33 | map(char('b'), |_| '\x08'), 34 | map(char('t'), |_| '\t'), 35 | map(char('n'), |_| '\n'), 36 | map(char('f'), |_| '\x0c'), 37 | map(char('r'), |_| '\r'), 38 | preceded(char('u'), |s| hex_unicode_scalar(4, s)), 39 | preceded(char('U'), |s| hex_unicode_scalar(8, s)), 40 | ))(s) 41 | } 42 | 43 | fn basic_string(s: &str) -> IResult<&str, String> { 44 | let string_body = alt(( 45 | escaped_transform(none_of("\\\""), '\\', basic_string_escape), 46 | // TODO report a nom bug in escaped_transform: it rejects empty sequence. 47 | // https://github.com/Geal/nom/issues/953#issuecomment-525557597 48 | // https://docs.rs/nom/7.1.1/src/nom/bytes/complete.rs.html#570-577 49 | map(tag(""), String::from), 50 | )); 51 | delimited(char('"'), string_body, char('"'))(s) 52 | } 53 | 54 | fn bare_string(s: &str) -> IResult<&str, &str> { 55 | take_while1(|c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_')(s) 56 | } 57 | 58 | fn key_string(s: &str) -> IResult<&str, String> { 59 | alt((basic_string, map(bare_string, String::from)))(s) 60 | } 61 | 62 | fn array_index(s: &str) -> IResult<&str, usize> { 63 | map_res(digit1, |i: &str| i.parse())(s) 64 | } 65 | 66 | fn tpath_segment_name(s: &str) -> IResult<&str, TpathSegment> { 67 | map(key_string, TpathSegment::Name)(s) 68 | } 69 | 70 | #[rustfmt::skip] 71 | fn tpath_segment_num(s: &str) -> IResult<&str, TpathSegment> { 72 | map(delimited(char('['), array_index, char(']')), TpathSegment::Num)(s) 73 | } 74 | 75 | #[rustfmt::skip] 76 | fn tpath_segment_rest(s: &str) -> IResult<&str, TpathSegment> { 77 | alt(( 78 | preceded(char('.'), tpath_segment_name), 79 | tpath_segment_num, 80 | ))(s) 81 | } 82 | 83 | #[rustfmt::skip] 84 | fn tpath(s: &str) -> IResult<&str, Vec> { 85 | alt(( 86 | map(all_consuming(char('.')), |_| vec![]), 87 | // Must start with a name, because TOML root is always a table. 88 | map(tuple((tpath_segment_name, many0(tpath_segment_rest))), 89 | |(hd, mut tl)| { tl.insert(0, hd); tl }), 90 | ))(s) 91 | } 92 | 93 | pub fn parse_query(s: &str) -> Result>> { 94 | all_consuming(tpath)(s).map(|(trailing, res)| { 95 | assert!(trailing.is_empty()); 96 | Query(res) 97 | }) 98 | } 99 | 100 | #[test] 101 | fn test_parse_query() { 102 | use TpathSegment::{Name, Num}; 103 | let name = |n: &str| Name(n.to_string()); 104 | for (s, expected) in vec![ 105 | (".", Ok(vec![])), 106 | ("a", Ok(vec![name("a")])), 107 | ("a.b", Ok(vec![name("a"), name("b")])), 108 | ("\"a.b\"", Ok(vec![name("a.b")])), 109 | ("\"\"", Ok(vec![name("")])), 110 | ("a.\"\".b", Ok(vec![name("a"), name(""), name("b")])), 111 | ("..", Err(())), 112 | ("a[1]", Ok(vec![name("a"), Num(1)])), 113 | ("a[b]", Err(())), 114 | ("a[1].b", Ok(vec![name("a"), Num(1), name("b")])), 115 | ("a.b[1]", Ok(vec![name("a"), name("b"), Num(1)])), 116 | ] { 117 | let actual = parse_query(s); 118 | // This could use some slicker check that prints the actual on failure. 119 | // Also nice would be to proceed to try the other test cases. 120 | match expected { 121 | Ok(q) => assert!(q == actual.unwrap().0), 122 | Err(_) => assert!(actual.is_err()), 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # toml-cli 2 | 3 | This is the home of the `toml` command, a simple CLI for editing 4 | and querying TOML files. 5 | 6 | The intent of the `toml` command is to be useful 7 | * in shell scripts, for consulting or editing a config file; 8 | * and in instructions a human can follow for editing a config file, 9 | as a command to copy-paste and run. 10 | 11 | A source of inspiration for the interface is the `git config` command, 12 | which serves both of these purposes very well without knowing anything 13 | about the semantics of Git config files -- only their general 14 | structure. 15 | 16 | A key property is that when editing, we seek to *preserve formatting 17 | and comments* -- the only change to the file should be the one the 18 | user specifically asked for. To do this we rely on the `toml_edit` 19 | crate, which also underlies `cargo-edit`. There are a few edge cases 20 | where `toml_edit` can rearrange an oddly-formatted file (described in 21 | the `toml_edit` documentation); but for typical TOML files, we 22 | maintain this property with perfect fidelity. 23 | 24 | The command's status is **experimental**. The current interface does 25 | not yet serve its purposes as well as it could, and **incompatible 26 | changes** are anticipated. 27 | 28 | 29 | ## Installation 30 | 31 | ### Linux download 32 | 33 | [Precompiled binaries are published for Linux.][releases] 34 | The binaries are static executables, and work on any Linux 35 | distribution. 36 | 37 | Currently no binaries are published for other platforms. 38 | Doing so for more platforms is a desired future step 39 | ([#22], [#21], [#5]). In the meantime, see Cargo instructions below. 40 | 41 | [releases]: https://github.com/gnprice/toml-cli/releases 42 | 43 | 44 | ### Using Cargo 45 | 46 | If you have Cargo (the Rust build tool) installed, you can install the 47 | `toml` CLI by running: 48 | ``` 49 | $ cargo install toml-cli 50 | ``` 51 | 52 | To install Cargo, follow the instructions [on rust-lang.org][install-rust]. 53 | 54 | [install-rust]: https://www.rust-lang.org/learn/get-started 55 | 56 | [#5]: https://github.com/gnprice/toml-cli/issues/5 57 | [#21]: https://github.com/gnprice/toml-cli/issues/21 58 | [#22]: https://github.com/gnprice/toml-cli/issues/22 59 | 60 | 61 | ## Usage 62 | 63 | ### Reading: `toml get` 64 | 65 | To read specific data, pass a *TOML path*: a sequence of *path 66 | segments*, each of which is either: 67 | * `.KEY`, to index into a table or inline-table, or 68 | * `[INDEX]`, to index into an array-of-tables or array. 69 | 70 | Data is emitted by default as JSON: 71 | 72 | ``` 73 | $ toml get Cargo.toml bin[0] 74 | {"name":"toml","path":"src/main.rs"} 75 | ``` 76 | 77 | When the data is a string, the `--raw`/`-r` option prints it directly, 78 | for convenience in contexts like a shell script: 79 | 80 | ``` 81 | $ toml get Cargo.toml dependencies.serde --raw 82 | 1.0 83 | ``` 84 | 85 | If you need a more complex query, consider a tool like `jq`, with 86 | `toml` simply transforming the file to JSON: 87 | 88 | ``` 89 | $ toml get pyoxidizer.toml . | jq ' 90 | .embedded_python_config[] | select(.build_target | not) | .raw_allocator 91 | ' -r 92 | jemalloc 93 | ``` 94 | 95 | (The TOML path `.` is an alias for the empty path, describing the 96 | whole file.) 97 | 98 | ### Writing (ish): `toml set` 99 | 100 | To edit the data, pass a TOML path specifying where in the parse tree 101 | to put it, and then the data value to place there: 102 | 103 | ``` 104 | $ cat >foo.toml < 133 | 134 | FLAGS: 135 | -h, --help Prints help information 136 | -V, --version Prints version information 137 | 138 | SUBCOMMANDS: 139 | get Print some data from the file 140 | help Prints this message or the help of the given subcommand(s) 141 | set Edit the file to set some data (currently, just print modified version) 142 | ``` 143 | 144 | ### `toml get` 145 | 146 | ``` 147 | $ toml get --help 148 | toml-get 0.2.3 149 | Print some data from the file 150 | 151 | Read the given TOML file, find the data within it at the given query, 152 | and print. 153 | 154 | If the TOML document does not have the given key, exit with a 155 | failure status. 156 | 157 | Output is JSON by default. With `--raw`/`-r`, if the data is a 158 | string, print it directly. With `--output-toml`, print the data 159 | as a fragment of TOML. 160 | 161 | USAGE: 162 | toml get [FLAGS] 163 | 164 | FLAGS: 165 | -h, --help Prints help information 166 | --output-toml Print as a TOML fragment (default: print as JSON) 167 | -r, --raw Print strings raw, not as JSON 168 | -V, --version Prints version information 169 | 170 | ARGS: 171 | Path to the TOML file to read 172 | Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) 173 | ``` 174 | 175 | ### `toml set` 176 | 177 | ``` 178 | $ toml set --help 179 | toml-set 0.2.3 180 | Edit the file to set some data (currently, just print modified version) 181 | 182 | USAGE: 183 | toml set 184 | 185 | FLAGS: 186 | -h, --help Prints help information 187 | -V, --version Prints version information 188 | 189 | ARGS: 190 | Path to the TOML file to read 191 | Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) 192 | String value to place at the given spot (bool, array, etc. are TODO) 193 | ``` 194 | -------------------------------------------------------------------------------- /test/test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | use std::process; 5 | use std::process::Output; 6 | use std::str; 7 | 8 | use tempfile::TempDir; 9 | 10 | macro_rules! tomltest { 11 | ($name:ident, $fun:expr) => { 12 | #[test] 13 | fn $name() { 14 | $fun(TestCaseState::new()); 15 | } 16 | }; 17 | } 18 | 19 | macro_rules! tomltest_get_err { 20 | ($name:ident, $args:expr, $pattern:expr) => { 21 | tomltest!($name, |mut t: TestCaseState| { 22 | t.write_file(INPUT); 23 | t.cmd.args(["get", &t.filename()]).args($args); 24 | check_contains($pattern, &t.expect_error()); 25 | }); 26 | }; 27 | } 28 | 29 | macro_rules! tomltest_get_err_empty { 30 | ($name:ident, $args:expr) => { 31 | tomltest!($name, |mut t: TestCaseState| { 32 | t.write_file(INPUT); 33 | t.cmd.args(["get", &t.filename()]).args($args); 34 | check_eq("", &t.expect_error()); 35 | }); 36 | }; 37 | } 38 | 39 | macro_rules! tomltest_get { 40 | ($name:ident, $args:expr, $expected:expr) => { 41 | tomltest!($name, |mut t: TestCaseState| { 42 | t.write_file(INPUT); 43 | t.cmd.args(["get", &t.filename()]).args($args); 44 | check_eq($expected, &t.expect_success()); 45 | }); 46 | }; 47 | } 48 | 49 | macro_rules! tomltest_get1 { 50 | ($name:ident, $key:expr, $expected:expr) => { 51 | tomltest!($name, |mut t: TestCaseState| { 52 | t.write_file(INPUT); 53 | t.cmd.args(["get", &t.filename(), $key]); 54 | let expected = format!("{}\n", serde_json::to_string(&$expected).unwrap()); 55 | check_eq(&expected, &t.expect_success()); 56 | }); 57 | }; 58 | } 59 | 60 | tomltest!(help_if_no_args, |mut t: TestCaseState| { 61 | check_contains("-h, --help", &t.expect_error()); 62 | }); 63 | 64 | const INPUT: &str = r#" 65 | key = "value" 66 | int = 17 67 | bool = true 68 | 69 | # this is a TOML comment 70 | bare-Key_1 = "bare" # another TOML comment 71 | "quoted key‽" = "quoted" 72 | "" = "empty" 73 | dotted.a = "dotted-a" 74 | dotted . b = "dotted-b" 75 | 76 | [foo] 77 | x = "foo-x" 78 | y.yy = "foo-yy" 79 | "#; 80 | 81 | tomltest_get1!(get_string, "key", "value"); 82 | tomltest_get1!(get_int, "int", 17); 83 | tomltest_get1!(get_bool, "bool", true); 84 | // TODO test remaining TOML value types: float, datetime, and aggregates: 85 | // array, table, inline table, array of tables. 86 | 87 | // Test the various TOML key syntax: https://toml.io/en/v1.0.0#keys 88 | tomltest_get1!(get_bare_key, "bare-Key_1", "bare"); 89 | tomltest_get1!(get_quoted_key, "\"quoted key‽\"", "quoted"); 90 | tomltest_get1!(get_empty_key, "\"\"", "empty"); 91 | tomltest_get1!(get_dotted_key, "dotted.a", "dotted-a"); 92 | tomltest_get1!(get_dotted_spaced_key, "dotted.b", "dotted-b"); 93 | tomltest_get1!(get_nested, "foo.x", "foo-x"); 94 | tomltest_get1!(get_nested_dotted, "foo.y.yy", "foo-yy"); 95 | // TODO test `get` inside arrays and arrays of tables 96 | 97 | tomltest_get!(get_string_raw, ["--raw", "key"], "value\n"); 98 | // TODO test `get --raw` on non-strings 99 | 100 | // TODO test `get --output-toml` 101 | 102 | tomltest_get_err!(get_invalid_query, [".bad"], "syntax error in query: .bad"); 103 | tomltest_get_err_empty!(get_missing, ["nosuchkey"]); 104 | tomltest_get_err_empty!(get_missing_num, ["key[1]"]); 105 | 106 | macro_rules! tomltest_set { 107 | ($name:ident, $args:expr, $expected:expr) => { 108 | tomltest!($name, |mut t: TestCaseState| { 109 | t.write_file(INITIAL); 110 | t.cmd.args(["set", &t.filename()]).args($args); 111 | check_eq(&$expected, &t.expect_success()); 112 | }); 113 | }; 114 | } 115 | 116 | const INITIAL: &str = r#" 117 | [x] 118 | y = 1 119 | "#; 120 | 121 | #[rustfmt::skip] 122 | tomltest_set!(set_string_existing, ["x.y", "new"], r#" 123 | [x] 124 | y = "new" 125 | "#); 126 | 127 | #[rustfmt::skip] 128 | tomltest_set!(set_string_existing_table, ["x.z", "123"], format!( 129 | r#"{INITIAL}z = "123" 130 | "#)); 131 | 132 | #[rustfmt::skip] 133 | tomltest_set!(set_string_new_table, ["foo.bar", "baz"], format!( 134 | r#"{INITIAL} 135 | [foo] 136 | bar = "baz" 137 | "#)); 138 | 139 | #[rustfmt::skip] 140 | tomltest_set!(set_string_toplevel, ["foo", "bar"], format!( 141 | r#"foo = "bar" 142 | {INITIAL}"#)); 143 | 144 | // TODO test `set` on string with newlines and other fun characters 145 | // TODO test `set` when existing value is an array, table, or array of tables 146 | // TODO test `set` inside existing array or inline table 147 | // TODO test `set` inside existing array of tables 148 | 149 | struct TestCaseState { 150 | cmd: process::Command, 151 | #[allow(dead_code)] // We keep the TempDir around to prolong its lifetime 152 | dir: TempDir, 153 | filename: PathBuf, 154 | } 155 | 156 | impl TestCaseState { 157 | pub fn new() -> Self { 158 | let cmd = process::Command::new(env!("CARGO_BIN_EXE_toml")); 159 | let dir = tempfile::tempdir().expect("failed to create tempdir"); 160 | let filename = dir.path().join("test.toml"); 161 | TestCaseState { cmd, dir, filename } 162 | } 163 | 164 | pub fn expect_success(&mut self) -> String { 165 | let out = self.cmd.output().unwrap(); 166 | if !out.status.success() { 167 | self.fail(&out, "Command failed!"); 168 | } else if !out.stderr.is_empty() { 169 | self.fail(&out, "Command printed to stderr despite success"); 170 | } 171 | String::from_utf8(out.stdout).unwrap() 172 | } 173 | 174 | pub fn expect_error(&mut self) -> String { 175 | let out = self.cmd.output().unwrap(); 176 | if out.status.success() { 177 | self.fail(&out, "Command succeeded; expected failure"); 178 | } else if !out.stdout.is_empty() { 179 | self.fail(&out, "Command printed to stdout despite failure"); 180 | } 181 | String::from_utf8(out.stderr).unwrap() 182 | } 183 | 184 | fn fail(&self, out: &Output, summary: &str) { 185 | panic!( 186 | "\n============\ 187 | \n{}\ 188 | \ncmdline: {:?}\ 189 | \nstatus: {}\ 190 | \nstderr: {}\ 191 | \nstdout: {}\ 192 | \n============\n", 193 | summary, 194 | self.cmd, 195 | out.status, 196 | String::from_utf8_lossy(&out.stderr), 197 | String::from_utf8_lossy(&out.stdout), 198 | ) 199 | } 200 | 201 | pub fn write_file(&self, contents: &str) { 202 | fs::write(&self.filename, contents).expect("failed to write test fixture"); 203 | } 204 | 205 | pub fn filename(&self) -> String { 206 | // TODO we don't really need a String here, do we? 207 | String::from(self.filename.as_os_str().to_str().unwrap()) 208 | } 209 | } 210 | 211 | /// Like `assert!(actual.contains(pattern))`, but with more informative output. 212 | #[rustfmt::skip] 213 | fn check_contains(pattern: &str, actual: &str) { 214 | if actual.contains(pattern) { 215 | return; 216 | } 217 | panic!(" 218 | /~~ expected pattern: 219 | {} 220 | /~~ got: 221 | {}/~~ 222 | ", pattern, actual); 223 | } 224 | 225 | /// Like `assert_eq!`, but with more-readable output for debugging failed tests. 226 | /// 227 | /// In particular, print the strings directly rather than with `{:?}`. 228 | #[rustfmt::skip] 229 | fn check_eq(expected: &str, actual: &str) { 230 | if expected != actual { 231 | panic!(" 232 | ~~~ expected: 233 | {}~~~ got: 234 | {}~~~ 235 | ", expected, actual); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod query_parser; 2 | 3 | use std::path::PathBuf; 4 | use std::str; 5 | use std::{fs, process::exit}; 6 | 7 | use anyhow::Error; 8 | use serde::ser::{Serialize, SerializeMap, SerializeSeq, Serializer}; 9 | use structopt::StructOpt; 10 | use thiserror::Error; 11 | use toml_edit::{value, Document, Item, Table, Value}; 12 | 13 | use query_parser::{parse_query, Query, TpathSegment}; 14 | 15 | // TODO: Get more of the description in the README into the CLI help. 16 | #[derive(StructOpt)] 17 | #[structopt(about)] 18 | enum Args { 19 | /// Print some data from the file 20 | /// 21 | /// Read the given TOML file, find the data within it at the given query, 22 | /// and print. 23 | /// 24 | /// If the TOML document does not have the given key, exit with a 25 | /// failure status. 26 | /// 27 | /// Output is JSON by default. With `--raw`/`-r`, if the data is a 28 | /// string, print it directly. With `--output-toml`, print the data 29 | /// as a fragment of TOML. 30 | // Without verbatim_doc_comment, the paragraphs get rewrapped to like 31 | // 120 columns wide. 32 | #[structopt(verbatim_doc_comment)] 33 | Get { 34 | /// Path to the TOML file to read 35 | #[structopt(parse(from_os_str))] 36 | path: PathBuf, 37 | 38 | /// Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) 39 | query: String, 40 | 41 | #[structopt(flatten)] 42 | opts: GetOpts, 43 | }, 44 | 45 | /// Edit the file to set some data (currently, just print modified version) 46 | Set { 47 | /// Path to the TOML file to read 48 | #[structopt(parse(from_os_str))] 49 | path: PathBuf, 50 | 51 | /// Query within the TOML data (e.g. `dependencies.serde`, `foo[0].bar`) 52 | query: String, 53 | 54 | /// String value to place at the given spot (bool, array, etc. are TODO) 55 | value_str: String, // TODO more forms 56 | }, 57 | // 58 | // TODO: append/add (name TBD) 59 | } 60 | 61 | #[derive(StructOpt)] 62 | struct GetOpts { 63 | /// Print as a TOML fragment (default: print as JSON) 64 | #[structopt(long)] 65 | output_toml: bool, 66 | 67 | /// Print strings raw, not as JSON 68 | // (No effect when the item isn't a string, just like `jq -r`.) 69 | #[structopt(long, short)] 70 | raw: bool, 71 | } 72 | 73 | #[derive(Debug, Error)] 74 | enum CliError { 75 | #[error("syntax error in query: {0}")] 76 | QuerySyntaxError(String), 77 | #[error("numeric index into non-array")] 78 | NotArray(), 79 | #[error("array index out of bounds")] 80 | ArrayIndexOob(), 81 | } 82 | 83 | /// An error that should cause a failure exit, but no message on stderr. 84 | #[derive(Debug, Error)] 85 | enum SilentError { 86 | #[error("key not found: {key}")] 87 | KeyNotFound { key: String }, 88 | } 89 | 90 | fn main() { 91 | let args = Args::from_args(); 92 | let result = match args { 93 | Args::Get { path, query, opts } => get(&path, &query, &opts), 94 | Args::Set { 95 | path, 96 | query, 97 | value_str, 98 | } => set(&path, &query, &value_str), 99 | }; 100 | result.unwrap_or_else(|err| { 101 | match err.downcast::() { 102 | Ok(_) => {} 103 | Err(err) => { 104 | eprintln!("toml: {}", err); 105 | } 106 | } 107 | exit(1); 108 | }) 109 | } 110 | 111 | fn read_parse(path: &PathBuf) -> Result { 112 | // TODO: better report errors like ENOENT 113 | let data = fs::read(path)?; 114 | let data = str::from_utf8(&data)?; 115 | Ok(data.parse::()?) 116 | } 117 | 118 | fn get(path: &PathBuf, query: &str, opts: &GetOpts) -> Result<(), Error> { 119 | let tpath = parse_query_cli(query)?.0; 120 | let doc = read_parse(path)?; 121 | 122 | if opts.output_toml { 123 | print_toml_fragment(&doc, &tpath); 124 | return Ok(()); 125 | } 126 | 127 | let item = walk_tpath(doc.as_item(), &tpath); 128 | let item = item.ok_or(SilentError::KeyNotFound { key: query.into() })?; 129 | 130 | if opts.raw { 131 | if let Item::Value(Value::String(s)) = item { 132 | println!("{}", s.value()); 133 | return Ok(()); 134 | } 135 | } 136 | 137 | println!("{}", serde_json::to_string(&JsonItem(item))?); 138 | Ok(()) 139 | } 140 | 141 | fn print_toml_fragment(doc: &Document, tpath: &[TpathSegment]) { 142 | use TpathSegment::{Name, Num}; 143 | 144 | let mut item = doc.as_item(); 145 | let mut breadcrumbs = vec![]; 146 | for seg in tpath { 147 | breadcrumbs.push((item, seg)); 148 | match seg { 149 | Name(n) => item = &item[n], 150 | Num(n) => item = &item[n], 151 | } 152 | } 153 | 154 | let mut item = item.clone(); 155 | while let Some((parent, seg)) = breadcrumbs.pop() { 156 | match (seg, parent) { 157 | (Name(n), Item::Table(t)) => { 158 | // TODO clean up all this copying; may need more from toml_edit API 159 | let mut next = t.clone(); 160 | while !next.is_empty() { 161 | let (k, _) = next.iter().next().unwrap(); 162 | let k = String::from(k); 163 | next.remove(&k); 164 | } 165 | next[n] = item; 166 | item = Item::Table(next); 167 | } 168 | (Num(_), Item::ArrayOfTables(a)) => { 169 | // TODO clean up this copying too 170 | let mut next = a.clone(); 171 | next.clear(); 172 | match item { 173 | #[rustfmt::skip] 174 | Item::Table(t) => { next.push(t); } 175 | _ => panic!("malformed TOML parse-tree"), 176 | } 177 | item = Item::ArrayOfTables(next); 178 | } 179 | _ => panic!("UNIMPLEMENTED: --output-toml inside inline data"), // TODO 180 | } 181 | } 182 | let doc = Document::from(item.into_table().unwrap()); 183 | print!("{}", doc); 184 | } 185 | 186 | fn set(path: &PathBuf, query: &str, value_str: &str) -> Result<(), Error> { 187 | let tpath = parse_query_cli(query)?.0; 188 | let mut doc = read_parse(path)?; 189 | 190 | let mut item = doc.as_item_mut(); 191 | let mut already_inline = false; 192 | let mut tpath = &tpath[..]; 193 | use TpathSegment::{Name, Num}; 194 | while let Some(seg) = tpath.first() { 195 | tpath = &tpath[1..]; // TODO simplify to `for`, unless end up needing a tail 196 | match seg { 197 | Num(n) => { 198 | let len = match &item { 199 | Item::ArrayOfTables(a) => a.len(), 200 | Item::Value(Value::Array(a)) => a.len(), 201 | _ => Err(CliError::NotArray())?, 202 | }; 203 | if n >= &len { 204 | Err(CliError::ArrayIndexOob())?; 205 | } 206 | #[allow(clippy::single_match)] 207 | match &item { 208 | Item::Value(_) => already_inline = true, 209 | _ => (), 210 | }; 211 | item = &mut item[n]; 212 | } 213 | Name(n) => { 214 | match &item { 215 | Item::Table(_) => (), 216 | Item::Value(Value::InlineTable(_)) => already_inline = true, 217 | // TODO make this more directly construct the new, inner part? 218 | _ => { 219 | *item = if already_inline { 220 | Item::Value(Value::InlineTable(Default::default())) 221 | } else { 222 | Item::Table(Table::new()) 223 | } 224 | } 225 | }; 226 | item = &mut item[n]; 227 | } 228 | } 229 | } 230 | *item = value(value_str); 231 | 232 | // TODO actually write back 233 | print!("{}", doc); 234 | Ok(()) 235 | } 236 | 237 | fn parse_query_cli(query: &str) -> Result { 238 | parse_query(query).map_err(|_err| { 239 | CliError::QuerySyntaxError(query.into()) // TODO: perhaps use parse-error details? 240 | }) 241 | } 242 | 243 | fn walk_tpath<'a>( 244 | mut item: &'a toml_edit::Item, 245 | tpath: &[TpathSegment], 246 | ) -> Option<&'a toml_edit::Item> { 247 | use TpathSegment::{Name, Num}; 248 | for seg in tpath { 249 | match seg { 250 | Name(n) => item = item.get(n)?, 251 | Num(n) => item = item.get(n)?, 252 | } 253 | } 254 | Some(item) 255 | } 256 | 257 | // TODO Can we do newtypes more cleanly than this? 258 | struct JsonItem<'a>(&'a toml_edit::Item); 259 | 260 | impl Serialize for JsonItem<'_> { 261 | fn serialize(&self, serializer: S) -> Result 262 | where 263 | S: Serializer, 264 | { 265 | match self.0 { 266 | Item::Value(v) => JsonValue(v).serialize(serializer), 267 | Item::Table(t) => JsonTable(t).serialize(serializer), 268 | Item::ArrayOfTables(a) => { 269 | let mut seq = serializer.serialize_seq(Some(a.len()))?; 270 | for t in a.iter() { 271 | seq.serialize_element(&JsonTable(t))?; 272 | } 273 | seq.end() 274 | } 275 | Item::None => serializer.serialize_none(), 276 | } 277 | } 278 | } 279 | 280 | struct JsonTable<'a>(&'a toml_edit::Table); 281 | 282 | impl Serialize for JsonTable<'_> { 283 | fn serialize(&self, serializer: S) -> Result 284 | where 285 | S: Serializer, 286 | { 287 | let mut map = serializer.serialize_map(Some(self.0.len()))?; 288 | for (k, v) in self.0.iter() { 289 | map.serialize_entry(k, &JsonItem(v))?; 290 | } 291 | map.end() 292 | } 293 | } 294 | 295 | struct JsonValue<'a>(&'a toml_edit::Value); 296 | 297 | impl Serialize for JsonValue<'_> { 298 | fn serialize(&self, serializer: S) -> Result 299 | where 300 | S: Serializer, 301 | { 302 | #[allow(clippy::redundant_pattern_matching)] 303 | if let Some(v) = self.0.as_integer() { 304 | v.serialize(serializer) 305 | } else if let Some(v) = self.0.as_float() { 306 | v.serialize(serializer) 307 | } else if let Some(v) = self.0.as_bool() { 308 | v.serialize(serializer) 309 | } else if let Some(v) = self.0.as_str() { 310 | v.serialize(serializer) 311 | } else if let Some(_) = self.0.as_datetime() { 312 | "UNIMPLEMENTED: DateTime".serialize(serializer) // TODO 313 | } else if let Some(arr) = self.0.as_array() { 314 | let mut seq = serializer.serialize_seq(Some(arr.len()))?; 315 | for e in arr.iter() { 316 | seq.serialize_element(&JsonValue(e))?; 317 | } 318 | seq.end() 319 | } else if let Some(t) = self.0.as_inline_table() { 320 | let mut map = serializer.serialize_map(Some(t.len()))?; 321 | for (k, v) in t.iter() { 322 | map.serialize_entry(k, &JsonValue(v))?; 323 | } 324 | map.end() 325 | } else { 326 | panic!("unknown variant of toml_edit::Value"); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ansi_term" 7 | version = "0.12.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 10 | dependencies = [ 11 | "winapi", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.66" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.14" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 25 | dependencies = [ 26 | "hermit-abi", 27 | "libc", 28 | "winapi", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "bitflags" 39 | version = "1.3.2" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 42 | 43 | [[package]] 44 | name = "bytes" 45 | version = "1.3.0" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" 48 | 49 | [[package]] 50 | name = "cfg-if" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 54 | 55 | [[package]] 56 | name = "clap" 57 | version = "2.34.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 60 | dependencies = [ 61 | "ansi_term", 62 | "atty", 63 | "bitflags", 64 | "strsim", 65 | "textwrap", 66 | "unicode-width", 67 | "vec_map", 68 | ] 69 | 70 | [[package]] 71 | name = "combine" 72 | version = "4.6.6" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 75 | dependencies = [ 76 | "bytes", 77 | "memchr", 78 | ] 79 | 80 | [[package]] 81 | name = "either" 82 | version = "1.8.0" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" 85 | 86 | [[package]] 87 | name = "fastrand" 88 | version = "1.8.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 91 | dependencies = [ 92 | "instant", 93 | ] 94 | 95 | [[package]] 96 | name = "hashbrown" 97 | version = "0.12.3" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 100 | 101 | [[package]] 102 | name = "heck" 103 | version = "0.3.3" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 106 | dependencies = [ 107 | "unicode-segmentation", 108 | ] 109 | 110 | [[package]] 111 | name = "hermit-abi" 112 | version = "0.1.19" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 115 | dependencies = [ 116 | "libc", 117 | ] 118 | 119 | [[package]] 120 | name = "indexmap" 121 | version = "1.9.2" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" 124 | dependencies = [ 125 | "autocfg", 126 | "hashbrown", 127 | ] 128 | 129 | [[package]] 130 | name = "instant" 131 | version = "0.1.12" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 134 | dependencies = [ 135 | "cfg-if", 136 | ] 137 | 138 | [[package]] 139 | name = "itertools" 140 | version = "0.10.5" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 143 | dependencies = [ 144 | "either", 145 | ] 146 | 147 | [[package]] 148 | name = "itoa" 149 | version = "1.0.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" 152 | 153 | [[package]] 154 | name = "lazy_static" 155 | version = "1.4.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 158 | 159 | [[package]] 160 | name = "libc" 161 | version = "0.2.137" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" 164 | 165 | [[package]] 166 | name = "memchr" 167 | version = "2.5.0" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 170 | 171 | [[package]] 172 | name = "minimal-lexical" 173 | version = "0.2.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 176 | 177 | [[package]] 178 | name = "nom" 179 | version = "7.1.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 182 | dependencies = [ 183 | "memchr", 184 | "minimal-lexical", 185 | ] 186 | 187 | [[package]] 188 | name = "proc-macro-error" 189 | version = "1.0.4" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 192 | dependencies = [ 193 | "proc-macro-error-attr", 194 | "proc-macro2", 195 | "quote", 196 | "syn", 197 | "version_check", 198 | ] 199 | 200 | [[package]] 201 | name = "proc-macro-error-attr" 202 | version = "1.0.4" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 205 | dependencies = [ 206 | "proc-macro2", 207 | "quote", 208 | "version_check", 209 | ] 210 | 211 | [[package]] 212 | name = "proc-macro2" 213 | version = "1.0.47" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" 216 | dependencies = [ 217 | "unicode-ident", 218 | ] 219 | 220 | [[package]] 221 | name = "quote" 222 | version = "1.0.21" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 225 | dependencies = [ 226 | "proc-macro2", 227 | ] 228 | 229 | [[package]] 230 | name = "redox_syscall" 231 | version = "0.2.16" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 234 | dependencies = [ 235 | "bitflags", 236 | ] 237 | 238 | [[package]] 239 | name = "remove_dir_all" 240 | version = "0.5.3" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 243 | dependencies = [ 244 | "winapi", 245 | ] 246 | 247 | [[package]] 248 | name = "ryu" 249 | version = "1.0.11" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" 252 | 253 | [[package]] 254 | name = "serde" 255 | version = "1.0.148" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" 258 | 259 | [[package]] 260 | name = "serde_json" 261 | version = "1.0.89" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" 264 | dependencies = [ 265 | "itoa", 266 | "ryu", 267 | "serde", 268 | ] 269 | 270 | [[package]] 271 | name = "strsim" 272 | version = "0.8.0" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 275 | 276 | [[package]] 277 | name = "structopt" 278 | version = "0.3.26" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 281 | dependencies = [ 282 | "clap", 283 | "lazy_static", 284 | "structopt-derive", 285 | ] 286 | 287 | [[package]] 288 | name = "structopt-derive" 289 | version = "0.4.18" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 292 | dependencies = [ 293 | "heck", 294 | "proc-macro-error", 295 | "proc-macro2", 296 | "quote", 297 | "syn", 298 | ] 299 | 300 | [[package]] 301 | name = "syn" 302 | version = "1.0.105" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" 305 | dependencies = [ 306 | "proc-macro2", 307 | "quote", 308 | "unicode-ident", 309 | ] 310 | 311 | [[package]] 312 | name = "tempfile" 313 | version = "3.3.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 316 | dependencies = [ 317 | "cfg-if", 318 | "fastrand", 319 | "libc", 320 | "redox_syscall", 321 | "remove_dir_all", 322 | "winapi", 323 | ] 324 | 325 | [[package]] 326 | name = "textwrap" 327 | version = "0.11.0" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 330 | dependencies = [ 331 | "unicode-width", 332 | ] 333 | 334 | [[package]] 335 | name = "thiserror" 336 | version = "1.0.37" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" 339 | dependencies = [ 340 | "thiserror-impl", 341 | ] 342 | 343 | [[package]] 344 | name = "thiserror-impl" 345 | version = "1.0.37" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" 348 | dependencies = [ 349 | "proc-macro2", 350 | "quote", 351 | "syn", 352 | ] 353 | 354 | [[package]] 355 | name = "toml-cli" 356 | version = "0.2.3" 357 | dependencies = [ 358 | "anyhow", 359 | "nom", 360 | "serde", 361 | "serde_json", 362 | "structopt", 363 | "tempfile", 364 | "thiserror", 365 | "toml_edit", 366 | ] 367 | 368 | [[package]] 369 | name = "toml_datetime" 370 | version = "0.5.0" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "808b51e57d0ef8f71115d8f3a01e7d3750d01c79cac4b3eda910f4389fdf92fd" 373 | 374 | [[package]] 375 | name = "toml_edit" 376 | version = "0.15.0" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "b1541ba70885967e662f69d31ab3aeca7b1aaecfcd58679590b893e9239c3646" 379 | dependencies = [ 380 | "combine", 381 | "indexmap", 382 | "itertools", 383 | "toml_datetime", 384 | ] 385 | 386 | [[package]] 387 | name = "unicode-ident" 388 | version = "1.0.5" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" 391 | 392 | [[package]] 393 | name = "unicode-segmentation" 394 | version = "1.10.0" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" 397 | 398 | [[package]] 399 | name = "unicode-width" 400 | version = "0.1.10" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" 403 | 404 | [[package]] 405 | name = "vec_map" 406 | version = "0.8.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 409 | 410 | [[package]] 411 | name = "version_check" 412 | version = "0.9.4" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 415 | 416 | [[package]] 417 | name = "winapi" 418 | version = "0.3.9" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 421 | dependencies = [ 422 | "winapi-i686-pc-windows-gnu", 423 | "winapi-x86_64-pc-windows-gnu", 424 | ] 425 | 426 | [[package]] 427 | name = "winapi-i686-pc-windows-gnu" 428 | version = "0.4.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 431 | 432 | [[package]] 433 | name = "winapi-x86_64-pc-windows-gnu" 434 | version = "0.4.0" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 437 | --------------------------------------------------------------------------------