VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)
VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)
VERSION=$(grep '^version' Cargo.toml | head -1 | cut -d'"' -f2)
# Get a nested value
VERSION=$(toml-query Cargo.toml dependencies.serde.version) # List keys in a table
for dep in $(toml-query Cargo.toml --keys dependencies); do echo "$dep"
done # Guard on a flag
if toml-query Cargo.toml --exists package.metadata.docs.rs; then cargo doc --no-deps
fi # Pipe into jq for transformations
toml-query Cargo.toml dependencies --format json | jq 'keys | length' # Bump a version in CI
toml-query Cargo.toml package.version --edit "$NEW_VERSION"
# Get a nested value
VERSION=$(toml-query Cargo.toml dependencies.serde.version) # List keys in a table
for dep in $(toml-query Cargo.toml --keys dependencies); do echo "$dep"
done # Guard on a flag
if toml-query Cargo.toml --exists package.metadata.docs.rs; then cargo doc --no-deps
fi # Pipe into jq for transformations
toml-query Cargo.toml dependencies --format json | jq 'keys | length' # Bump a version in CI
toml-query Cargo.toml package.version --edit "$NEW_VERSION"
# Get a nested value
VERSION=$(toml-query Cargo.toml dependencies.serde.version) # List keys in a table
for dep in $(toml-query Cargo.toml --keys dependencies); do echo "$dep"
done # Guard on a flag
if toml-query Cargo.toml --exists package.metadata.docs.rs; then cargo doc --no-deps
fi # Pipe into jq for transformations
toml-query Cargo.toml dependencies --format json | jq 'keys | length' # Bump a version in CI
toml-query Cargo.toml package.version --edit "$NEW_VERSION"
src/
βββ main.rs # CLI dispatch + exit codes
βββ cli.rs # clap Parser derive
βββ path.rs # "a.b[2].c" β [Key("a"), Key("b"), Index(2), Key("c")]
βββ query.rs # walk a toml::Value by path; get/set/exists/keys/length
βββ format.rs # render raw/json/toml output; parse --edit RHS
src/
βββ main.rs # CLI dispatch + exit codes
βββ cli.rs # clap Parser derive
βββ path.rs # "a.b[2].c" β [Key("a"), Key("b"), Index(2), Key("c")]
βββ query.rs # walk a toml::Value by path; get/set/exists/keys/length
βββ format.rs # render raw/json/toml output; parse --edit RHS
src/
βββ main.rs # CLI dispatch + exit codes
βββ cli.rs # clap Parser derive
βββ path.rs # "a.b[2].c" β [Key("a"), Key("b"), Index(2), Key("c")]
βββ query.rs # walk a toml::Value by path; get/set/exists/keys/length
βββ format.rs # render raw/json/toml output; parse --edit RHS
package.name
dependencies.serde.version
bin[0].name
workspace.members[2]
matrix[1][2]
package.name
dependencies.serde.version
bin[0].name
workspace.members[2]
matrix[1][2]
package.name
dependencies.serde.version
bin[0].name
workspace.members[2]
matrix[1][2]
pub enum Segment { Key(String), Index(usize),
} pub fn parse(input: &str) -> Result<Vec<Segment>, PathError> { let mut out = Vec::new(); if input.is_empty() { return Ok(out); } let bytes = input.as_bytes(); let mut i = 0usize; let mut expecting_key = true; while i < bytes.len() { if expecting_key { let start = i; while i < bytes.len() && bytes[i] != b'.' && bytes[i] != b'[' { i += 1; } if start == i { return Err(err(format!("empty key at position {}", start))); } let key = std::str::from_utf8(&bytes[start..i])?.to_string(); out.push(Segment::Key(key)); expecting_key = false; continue; } match bytes[i] { b'.' => { i += 1; expecting_key = true; } b'[' => { i += 1; let start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if i >= bytes.len() || bytes[i] != b']' { return Err(err("missing ']'")); } let digits = std::str::from_utf8(&bytes[start..i]).unwrap(); let n: usize = digits.parse()?; out.push(Segment::Index(n)); i += 1; } other => return Err(err(format!("unexpected '{}' at {}", other as char, i))), } } if expecting_key { return Err(err("trailing '.'")); } Ok(out)
}
pub enum Segment { Key(String), Index(usize),
} pub fn parse(input: &str) -> Result<Vec<Segment>, PathError> { let mut out = Vec::new(); if input.is_empty() { return Ok(out); } let bytes = input.as_bytes(); let mut i = 0usize; let mut expecting_key = true; while i < bytes.len() { if expecting_key { let start = i; while i < bytes.len() && bytes[i] != b'.' && bytes[i] != b'[' { i += 1; } if start == i { return Err(err(format!("empty key at position {}", start))); } let key = std::str::from_utf8(&bytes[start..i])?.to_string(); out.push(Segment::Key(key)); expecting_key = false; continue; } match bytes[i] { b'.' => { i += 1; expecting_key = true; } b'[' => { i += 1; let start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if i >= bytes.len() || bytes[i] != b']' { return Err(err("missing ']'")); } let digits = std::str::from_utf8(&bytes[start..i]).unwrap(); let n: usize = digits.parse()?; out.push(Segment::Index(n)); i += 1; } other => return Err(err(format!("unexpected '{}' at {}", other as char, i))), } } if expecting_key { return Err(err("trailing '.'")); } Ok(out)
}
pub enum Segment { Key(String), Index(usize),
} pub fn parse(input: &str) -> Result<Vec<Segment>, PathError> { let mut out = Vec::new(); if input.is_empty() { return Ok(out); } let bytes = input.as_bytes(); let mut i = 0usize; let mut expecting_key = true; while i < bytes.len() { if expecting_key { let start = i; while i < bytes.len() && bytes[i] != b'.' && bytes[i] != b'[' { i += 1; } if start == i { return Err(err(format!("empty key at position {}", start))); } let key = std::str::from_utf8(&bytes[start..i])?.to_string(); out.push(Segment::Key(key)); expecting_key = false; continue; } match bytes[i] { b'.' => { i += 1; expecting_key = true; } b'[' => { i += 1; let start = i; while i < bytes.len() && bytes[i].is_ascii_digit() { i += 1; } if i >= bytes.len() || bytes[i] != b']' { return Err(err("missing ']'")); } let digits = std::str::from_utf8(&bytes[start..i]).unwrap(); let n: usize = digits.parse()?; out.push(Segment::Index(n)); i += 1; } other => return Err(err(format!("unexpected '{}' at {}", other as char, i))), } } if expecting_key { return Err(err("trailing '.'")); } Ok(out)
}
pub fn get<'a>(root: &'a Value, segments: &[Segment]) -> Result<&'a Value, QueryError> { let mut cur = root; for (i, seg) in segments.iter().enumerate() { match seg { Segment::Key(k) => { let table = cur.as_table().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} (key '{}'): parent is not a table", i, k )) })?; cur = table.get(k).ok_or_else(|| QueryError::NotFound(format!("key '{}'", k)))?; } Segment::Index(n) => { let arr = cur.as_array().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} ([{}]): parent is not an array", i, n )) })?; cur = arr.get(*n).ok_or_else(|| QueryError::NotFound(format!("index {}", n)))?; } } } Ok(cur)
}
pub fn get<'a>(root: &'a Value, segments: &[Segment]) -> Result<&'a Value, QueryError> { let mut cur = root; for (i, seg) in segments.iter().enumerate() { match seg { Segment::Key(k) => { let table = cur.as_table().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} (key '{}'): parent is not a table", i, k )) })?; cur = table.get(k).ok_or_else(|| QueryError::NotFound(format!("key '{}'", k)))?; } Segment::Index(n) => { let arr = cur.as_array().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} ([{}]): parent is not an array", i, n )) })?; cur = arr.get(*n).ok_or_else(|| QueryError::NotFound(format!("index {}", n)))?; } } } Ok(cur)
}
pub fn get<'a>(root: &'a Value, segments: &[Segment]) -> Result<&'a Value, QueryError> { let mut cur = root; for (i, seg) in segments.iter().enumerate() { match seg { Segment::Key(k) => { let table = cur.as_table().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} (key '{}'): parent is not a table", i, k )) })?; cur = table.get(k).ok_or_else(|| QueryError::NotFound(format!("key '{}'", k)))?; } Segment::Index(n) => { let arr = cur.as_array().ok_or_else(|| { QueryError::TypeMismatch(format!( "segment {} ([{}]): parent is not an array", i, n )) })?; cur = arr.get(*n).ok_or_else(|| QueryError::NotFound(format!("index {}", n)))?; } } } Ok(cur)
}
$ toml-query Cargo.toml package.name
example $ toml-query Cargo.toml package.version
0.1.0 $ toml-query Cargo.toml package.edition
2021
$ toml-query Cargo.toml package.name
example $ toml-query Cargo.toml package.version
0.1.0 $ toml-query Cargo.toml package.edition
2021
$ toml-query Cargo.toml package.name
example $ toml-query Cargo.toml package.version
0.1.0 $ toml-query Cargo.toml package.edition
2021
VERSION=$(toml-query Cargo.toml package.version)
# VERSION=0.1.0, not "\"0.1.0\""
VERSION=$(toml-query Cargo.toml package.version)
# VERSION=0.1.0, not "\"0.1.0\""
VERSION=$(toml-query Cargo.toml package.version)
# VERSION=0.1.0, not "\"0.1.0\""
pub fn toml_to_json(value: &TomlValue) -> JsonValue { match value { TomlValue::String(s) => JsonValue::String(s.clone()), TomlValue::Integer(i) => JsonValue::Number((*i).into()), TomlValue::Float(f) => serde_json::Number::from_f64(*f) .map(JsonValue::Number) .unwrap_or(JsonValue::Null), TomlValue::Boolean(b) => JsonValue::Bool(*b), TomlValue::Datetime(d) => JsonValue::String(d.to_string()), TomlValue::Array(a) => JsonValue::Array(a.iter().map(toml_to_json).collect()), TomlValue::Table(t) => { let mut obj = serde_json::Map::new(); for (k, v) in t { obj.insert(k.clone(), toml_to_json(v)); } JsonValue::Object(obj) } }
}
pub fn toml_to_json(value: &TomlValue) -> JsonValue { match value { TomlValue::String(s) => JsonValue::String(s.clone()), TomlValue::Integer(i) => JsonValue::Number((*i).into()), TomlValue::Float(f) => serde_json::Number::from_f64(*f) .map(JsonValue::Number) .unwrap_or(JsonValue::Null), TomlValue::Boolean(b) => JsonValue::Bool(*b), TomlValue::Datetime(d) => JsonValue::String(d.to_string()), TomlValue::Array(a) => JsonValue::Array(a.iter().map(toml_to_json).collect()), TomlValue::Table(t) => { let mut obj = serde_json::Map::new(); for (k, v) in t { obj.insert(k.clone(), toml_to_json(v)); } JsonValue::Object(obj) } }
}
pub fn toml_to_json(value: &TomlValue) -> JsonValue { match value { TomlValue::String(s) => JsonValue::String(s.clone()), TomlValue::Integer(i) => JsonValue::Number((*i).into()), TomlValue::Float(f) => serde_json::Number::from_f64(*f) .map(JsonValue::Number) .unwrap_or(JsonValue::Null), TomlValue::Boolean(b) => JsonValue::Bool(*b), TomlValue::Datetime(d) => JsonValue::String(d.to_string()), TomlValue::Array(a) => JsonValue::Array(a.iter().map(toml_to_json).collect()), TomlValue::Table(t) => { let mut obj = serde_json::Map::new(); for (k, v) in t { obj.insert(k.clone(), toml_to_json(v)); } JsonValue::Object(obj) } }
}
if let Some(edit_raw) = &cli.edit { let mut doc: toml::Value = input.parse()?; let new_val = parse_edit_value(edit_raw, ty)?; query::set(&mut doc, &segments, new_val)?; let serialized = toml::to_string(&doc)?; fs::write(&cli.file, serialized)?; return Ok(ExitCode::SUCCESS);
}
if let Some(edit_raw) = &cli.edit { let mut doc: toml::Value = input.parse()?; let new_val = parse_edit_value(edit_raw, ty)?; query::set(&mut doc, &segments, new_val)?; let serialized = toml::to_string(&doc)?; fs::write(&cli.file, serialized)?; return Ok(ExitCode::SUCCESS);
}
if let Some(edit_raw) = &cli.edit { let mut doc: toml::Value = input.parse()?; let new_val = parse_edit_value(edit_raw, ty)?; query::set(&mut doc, &segments, new_val)?; let serialized = toml::to_string(&doc)?; fs::write(&cli.file, serialized)?; return Ok(ExitCode::SUCCESS);
}
pub fn set(root: &mut Value, segments: &[Segment], new_value: Value) -> Result<(), QueryError>
{ if segments.is_empty() { *root = new_value; return Ok(()); } let mut cur = root; for (i, seg) in segments.iter().enumerate() { let is_last = i == segments.len() - 1; match seg { Segment::Key(k) => { let table = cur.as_table_mut().ok_or_else(/* type mismatch */)?; if is_last { table.insert(k.clone(), new_value); return Ok(()); } // Auto-create intermediate table if missing. if !table.contains_key(k) { table.insert(k.clone(), Value::Table(toml::value::Table::new())); } cur = table.get_mut(k).unwrap(); } Segment::Index(n) => { let arr = cur.as_array_mut().ok_or_else(/* type mismatch */)?; if *n >= arr.len() { return Err(QueryError::NotFound(format!("index {}", n))); } if is_last { arr[*n] = new_value; return Ok(()); } cur = &mut arr[*n]; } } } Ok(())
}
pub fn set(root: &mut Value, segments: &[Segment], new_value: Value) -> Result<(), QueryError>
{ if segments.is_empty() { *root = new_value; return Ok(()); } let mut cur = root; for (i, seg) in segments.iter().enumerate() { let is_last = i == segments.len() - 1; match seg { Segment::Key(k) => { let table = cur.as_table_mut().ok_or_else(/* type mismatch */)?; if is_last { table.insert(k.clone(), new_value); return Ok(()); } // Auto-create intermediate table if missing. if !table.contains_key(k) { table.insert(k.clone(), Value::Table(toml::value::Table::new())); } cur = table.get_mut(k).unwrap(); } Segment::Index(n) => { let arr = cur.as_array_mut().ok_or_else(/* type mismatch */)?; if *n >= arr.len() { return Err(QueryError::NotFound(format!("index {}", n))); } if is_last { arr[*n] = new_value; return Ok(()); } cur = &mut arr[*n]; } } } Ok(())
}
pub fn set(root: &mut Value, segments: &[Segment], new_value: Value) -> Result<(), QueryError>
{ if segments.is_empty() { *root = new_value; return Ok(()); } let mut cur = root; for (i, seg) in segments.iter().enumerate() { let is_last = i == segments.len() - 1; match seg { Segment::Key(k) => { let table = cur.as_table_mut().ok_or_else(/* type mismatch */)?; if is_last { table.insert(k.clone(), new_value); return Ok(()); } // Auto-create intermediate table if missing. if !table.contains_key(k) { table.insert(k.clone(), Value::Table(toml::value::Table::new())); } cur = table.get_mut(k).unwrap(); } Segment::Index(n) => { let arr = cur.as_array_mut().ok_or_else(/* type mismatch */)?; if *n >= arr.len() { return Err(QueryError::NotFound(format!("index {}", n))); } if is_last { arr[*n] = new_value; return Ok(()); } cur = &mut arr[*n]; } } } Ok(())
}
docker build -t toml-query https://github.com/sen-ltd/toml-query.git # Query a field
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml package.name # List keys
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml --keys dependencies # Dump a sub-tree as JSON
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml dependencies --format json
docker build -t toml-query https://github.com/sen-ltd/toml-query.git # Query a field
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml package.name # List keys
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml --keys dependencies # Dump a sub-tree as JSON
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml dependencies --format json
docker build -t toml-query https://github.com/sen-ltd/toml-query.git # Query a field
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml package.name # List keys
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml --keys dependencies # Dump a sub-tree as JSON
docker run --rm -v "$PWD":/work toml-query /work/Cargo.toml dependencies --format json
git clone https://github.com/sen-ltd/toml-query
cd toml-query
cargo install --path .
git clone https://github.com/sen-ltd/toml-query
cd toml-query
cargo install --path .
git clone https://github.com/sen-ltd/toml-query
cd toml-query
cargo install --path .
alias tq='toml-query'
tq Cargo.toml package.version
alias tq='toml-query'
tq Cargo.toml package.version
alias tq='toml-query'
tq Cargo.toml package.version - State machines are still the right answer for tiny parsers. The expecting_key flag is the whole state of the parser. No peek, no lookahead, no nom dependency. It's 40 lines and fits in your head.
- Array indices after a key, not as a key. bin[0].name is three segments: Key("bin"), Index(0), Key("name"). Not two segments Key("bin[0]") then Key("name"). The latter is tempting because you can split on . and forget about brackets, but it pushes every consumer of the parser to re-parse.
- I deliberately don't support quoted keys. TOML lets you write "weird.key with dots" = 1, and that's a real feature, but the moment you support it in the path syntax, you need escaping rules, and suddenly the parser is 200 lines. For v0.1, dotted bare keys cover every real Cargo.toml I've ever seen. - Intermediate tables are auto-created. If you run toml-query Cargo.toml profile.release.opt-level --edit z on a file with no [profile.release] section, you get one. This is the path of least surprise for scripts.
- Intermediate arrays are NOT auto-extended. If bin has length 2 and you try to edit bin[5].name, you get an error, not four empty entries. Index-out-of-range is almost always a typo.
- Typed edits. The default --type is string, because that's what you want 90% of the time (package.version, package.name). But --type int, --type float, --type bool, and --type json let you write integers, floats, booleans, and arbitrary JSON structures. The last one is crucial for e.g. appending to a feature list: --type json --edit '["default", "async"]'. - There's a separate crate, toml_edit, that preserves formatting exactly. Switching to it is a future-work item; the API is different enough that it wasn't a one-evening change.
- For now, --edit is best for scratch files and cases where you control the formatting anyway. For upstream-quality version bumps, use cargo set-version or cargo-edit.