shyaml-rs 0.3.0

Command-line YAML processor - get values, set values, and transform YAML documents
shyaml-rs-0.3.0 is not a library.

SHYAML: YAML for the command line

Description

shyaml is a command-line tool for working with YAML files. It is designed to make YAML data accessible in shell scripts, with support for merging and modifying YAML documents.

This is a Rust rewrite of the original shyaml Python project. The command-line interface remains fully compatible with the original, allowing it to be used as a drop-in replacement.

Key features:

  • Full YAML specification support via the libfyaml C library (the Python version uses libyaml, last released in 2020)
  • Same command-line interface as the original Python version
  • Fast, standalone binary with no runtime dependencies
  • Support for YAML document streams
  • Ordered mapping preservation
  • YAML editing via apply, set-value, and del actions (new in Rust version)

Alternatives and Comparison

Several tools exist for command-line YAML processing. shyaml is intentionally simple and focused - it does a few things well: querying and modifying YAML data in shell scripts. If you need more, excellent alternatives exist.

yq (mikefarah)

yq by Mike Farah deserves a special mention as the de facto standard for command-line YAML processing. Written in Go, it's a fantastic, feature-rich tool:

  • jq-like syntax for powerful queries and transformations
  • Support for YAML, JSON, XML, CSV, TOML, and properties files
  • Read and write capabilities with in-place editing
  • 50M+ downloads and excellent documentation
  • Active development and great community support

If you need a full-featured YAML Swiss Army knife, yq is likely your best choice. It's what we'd recommend for most users who don't have existing shyaml scripts or specific simplicity requirements.

Comparison Table

About the Underlying Parsers

The choice of YAML parser matters for correctness, performance, and long-term maintenance:

  • libyaml (used by PyYAML, kislyuk/yq): The classic C library, but only supports YAML 1.1. Last release was 0.2.5 in 2020 - now in low-maintenance mode with limited activity.

  • go-yaml (used by mikefarah/yq): Pure Go implementation supporting YAML 1.2. The original author marked it "unmaintained" in 2025, though the YAML community has since taken over maintenance.

  • libfyaml (used by shyaml-rs): Modern C library with full YAML 1.2 support, actively maintained. Features zero-copy parsing, no artificial limits (libyaml has a 1024-char key limit), streaming support, and passes the complete YAML test suite.

Why shyaml / shyaml-rs?

shyaml was designed with a different philosophy: simplicity and shell-friendliness over features. There's no query language to learn - just intuitive dot-notation paths like config.database.port.

The feature set is deliberately minimal:

  • Query access with simple dot-notation paths
  • Value modification via set-value (set values at any path)
  • Key deletion via del (remove keys or sequence elements)
  • YAML merging via apply (overlay documents onto a base)
  • No format conversion
  • No complex filtering or transformations
  • No jq compatibility

Choose shyaml-rs if you:

  • Have existing scripts using the Python shyaml
  • Want the simplest possible interface - no learning curve
  • Need to extract, set, or delete values in YAML files
  • Need to merge YAML configurations (overlays, environments)
  • Need streaming support for multi-document YAML
  • Prefer null-terminated output (-0 options) for safe shell parsing

Choose yq or dasel if you:

  • Need in-place file editing
  • Want jq-compatible syntax for complex queries
  • Need to convert between formats (YAML/JSON/TOML/XML)
  • Require filters, transformations, or computed values

Implementation and Performance

shyaml-rs is built for performance:

  • Rust + libfyaml: Combines Rust's safety with libfyaml, a high-performance C library that fully implements the YAML 1.2 specification with zero-copy parsing
  • Zero runtime dependencies: Single static binary, no Python or other interpreters needed
  • No artificial limits: Unlike libyaml's 1024-character key limit, libfyaml handles arbitrarily large documents
  • Streaming support: Process multi-document YAML streams without loading everything into memory

Compared to the original Python shyaml (which uses PyYAML/libyaml), expect significantly faster startup and parsing times, especially noticeable when processing many files in a loop or large YAML documents.

  1. Benchmark Results

    shyaml-rs is faster than yq for simple queries due to libfyaml's efficient parsing and shyaml's minimal feature set (no expression language to parse). The musl static build adds ~4-20% overhead vs glibc, depending on file size.

Requirements

shyaml is a standalone binary that works on Linux and macOS. Pre-built binaries are available for common platforms.

Installation

From crates.io

If you have Rust installed:

cargo install shyaml-rs

From source

To build from source:

git clone https://github.com/0k/shyaml-rs
cd shyaml-rs
cargo build --release

The binary will be available at target/release/shyaml-rs.

Static binary from source

To build a fully static binary with no runtime dependencies (using musl):

rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl

The static binary will be at target/x86_64-unknown-linux-musl/release/shyaml-rs.

Pre-built binaries

Pre-built binaries may be available from the releases page.

Migrating from Python shyaml

This Rust version is designed as a drop-in replacement for the Python shyaml. Your existing shell scripts should work without modification.

Key differences:

  • Uses libfyaml for YAML parsing (the Python version used PyYAML/libyaml)
  • No Python runtime required
  • Slightly different output formatting in some edge cases
  • No Python API (command-line only)

If you were using the Python API, you'll need to use the command-line interface instead, or continue using the Python version.

Documentation

The following documented examples are tested for conformance with the implementation.

Since this version always uses libfyaml, some examples that had different behavior between Python YAML implementations may behave differently. Examples marked with docshtest: ignore-if LIBFYAML are skipped in testing as they had implementation-specific output in the Python version.

You can check the version and library information with:

$ shyaml -V | grep "^libfyaml used:"  ## docshtest: if-success-set LIBFYAML
libfyaml used: True

Usage

shyaml takes its YAML input file from standard input ONLY. So let's define here a common YAML input for the next examples:

$ cat <<EOF > test.yaml
name: "MyName !! héhé"  ## using encoding, and support comments !
subvalue:
    how-much: 1.1
    how-many: 2
    things:
        - first
        - second
        - third
    maintainer: "Valentin Lab"
    description: |
        Multiline description:
        Line 1
        Line 2
subvalue.how-much: 1.2
subvalue.how-much\more: 1.3
subvalue.how-much\.more: 1.4
EOF

General browsing struct and displaying simple values

Simple query of simple attribute:

$ cat test.yaml | shyaml get-value name
MyName !! héhé

Query nested attributes by using '.' between key labels:

$ cat test.yaml | shyaml get-value subvalue.how-much
1.1

Get type of attributes:

$ cat test.yaml | shyaml get-type name
str
$ cat test.yaml | shyaml get-type subvalue.how-much
float

Get length of structures or sequences:

$ cat test.yaml | shyaml get-length subvalue
5
$ cat test.yaml | shyaml get-length subvalue.things
3

But this won't work on other types:

$ cat test.yaml | shyaml get-length name
Error: get-length does not support 'str' type. Please provide or select a sequence or struct.

Parse structure

Get sub YAML from a structure attribute:

$ cat test.yaml | shyaml get-type subvalue
struct
$ cat test.yaml | shyaml get-value subvalue  ## docshtest: ignore-if LIBYAML,LIBFYAML
how-much: 1.1
how-many: 2
things:
- first
- second
- third
maintainer: Valentin Lab
description: 'Multiline description:

  Line 1

  Line 2

  '

Iteration through keys only:

$ cat test.yaml | shyaml keys
name
subvalue
subvalue.how-much
subvalue.how-much\more
subvalue.how-much\.more

Iteration through keys only (\0 terminated strings):

$ cat test.yaml | shyaml keys-0 subvalue | xargs -0 -n 1 echo "VALUE:"
VALUE: how-much
VALUE: how-many
VALUE: things
VALUE: maintainer
VALUE: description

Iteration through values only (\0 terminated string highly recommended):

$ cat test.yaml | shyaml values-0 subvalue |
  while IFS='' read -r -d $'\0' value; do
      echo "RECEIVED: '$value'"
  done ## docshtest: ignore-if LIBFYAML
RECEIVED: '1.1'
RECEIVED: '2'
RECEIVED: '- first
- second
- third
'
RECEIVED: 'Valentin Lab'
RECEIVED: 'Multiline description:
Line 1
Line 2
'

Iteration through keys and values (\0 terminated string highly recommended):

$ read-0() {
    while [ "$1" ]; do
        IFS=$'\0' read -r -d '' "$1" || return 1
        shift
    done
  } &&
  cat test.yaml | shyaml key-values-0 subvalue |
  while read-0 key value; do
      echo "KEY: '$key'"
      echo "VALUE: '$value'"
      echo
  done  ## docshtest: ignore-if LIBFYAML
KEY: 'how-much'
VALUE: '1.1'

KEY: 'how-many'
VALUE: '2'

KEY: 'things'
VALUE: '- first
- second
- third
'

KEY: 'maintainer'
VALUE: 'Valentin Lab'

KEY: 'description'
VALUE: 'Multiline description:
Line 1
Line 2
'
<BLANKLINE>

Notice, that you'll get the same result using get-values. get-values will support sequences and struct, and key-values support only struct. (for a complete table of which function support what you can look at the usage line)

And, if you ask for keys, values, key-values on non struct like, you'll get an error:

$ cat test.yaml | shyaml keys name
Error: keys does not support 'str' type. Please provide or select a struct.
$ cat test.yaml | shyaml values subvalue.how-many
Error: values does not support 'int' type. Please provide or select a struct.
$ cat test.yaml | shyaml key-values subvalue.how-much
Error: key-values does not support 'float' type. Please provide or select a struct.

Parse sequence

Query a sequence with get-value:

$ cat test.yaml | shyaml get-type subvalue.things
sequence
$ cat test.yaml | shyaml get-value subvalue.things
- first
- second
- third

And access individual elements with python-like indexing:

$ cat test.yaml | shyaml get-value subvalue.things.0
first
$ cat test.yaml | shyaml get-value subvalue.things.-1
third
$ cat test.yaml | shyaml get-value subvalue.things.5  ## docshtest: ignore-if LIBFYAML
Error: invalid path 'subvalue.things.5', index 5 is out of range (3 elements in sequence).

Note that this will work only with integer (preceded or not by a minus sign):

$ cat test.yaml | shyaml get-value subvalue.things.foo  ## docshtest: ignore-if LIBFYAML
Error: invalid path 'subvalue.things.foo', non-integer index 'foo' provided on a sequence.

More usefull, parse a list in one go with get-values:

$ cat test.yaml | shyaml get-values subvalue.things
first
second
third

Note that the action is called get-values, and that output is separated by newline char(s) (which is os dependent), this can bring havoc if you are parsing values containing newlines itself. Hopefully, shyaml has a get-values-0 to terminate strings by \0 char, which allows complete support of any type of values, including YAML. get-values outputs key and values for struct types and only values for sequence types:

$ cat test.yaml | shyaml get-values-0 subvalue |
  while IFS='' read -r -d '' key &&
        IFS='' read -r -d '' value; do
      echo "'$key' -> '$value'"
  done  ## docshtest: ignore-if LIBFYAML
'how-much' -> '1.1'
'how-many' -> '2'
'things' -> '- first
- second
- third
'
'maintainer' -> 'Valentin Lab'
'description' -> 'Multiline description:
Line 1
Line 2
'

Please note that, if get-values{,-0} actually works on struct, it's maybe more explicit to use the equivalent key-values{,0}. It should be noted that key-values{,0} is not completly equivalent as it is meant to be used with struct only and will complain if not.

You should also notice that values that are displayed are YAML compatible. So if they are complex, you can re-use shyaml on them to parse their content.

Of course, get-values should only be called on sequence elements:

$ cat test.yaml | shyaml get-values name
Error: get-values does not support 'str' type. Please provide or select a sequence or struct.

Parse YAML document streams

YAML input can be a stream of documents, the action will then be applied to each document:

$ i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 3)); then
          break
      fi
      echo "---"
done | shyaml get-value ingests.0.id | tr '\0' '&'
tag-1&tag-2&tag-3

Notice that NUL char is used by default for separating output iterations if not used in -y mode. You can use that to separate each output. -y mode will use conventional YAML way to separate documents (which is ---).

So:

$ i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 3)); then
          break
      fi
      echo "---"
done | shyaml get-value -y ingests.0.id  ## docshtest: ignore-if LIBYAML,LIBFYAML
tag-1
...
---
tag-2
...
---
tag-3
...

Notice that it is not supported to use any query that can output more than one value (like all the query that can be suffixed with *-0) with a multi-document YAML:

# i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 3)); then
          break
      fi
      echo "---"
done | shyaml keys ingests.0 >/dev/null
Error: Source YAML is multi-document, which doesn't support any other action than get-type, get-length, get-value

You'll probably notice also, that output seems buffered. The previous content is displayed as a whole only at the end. If you need a continuous flow of YAML document, then the command line option -L is required to force a non-buffered line-by-line reading of the file so as to ensure that each document is properly parsed as soon as possible. That means as soon as either a YAML document end is detected (--- or EOF):

Without the -L, if we kill our shyaml process before the end:

$ i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 2)); then
          break
      fi
      echo "---"
      sleep 10
done 2>/dev/null | shyaml get-value ingests.0.id & pid=$! ; sleep 2; kill $pid

With the -L, if we kill our shyaml process before the end:

$ i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 2)); then
          break
      fi
      echo "---"
      sleep 10
done 2>/dev/null | shyaml get-value -L ingests.0.id & pid=$! ; sleep 2; kill $pid
tag-1

Using -y is required to force a YAML output that will be also parseable as a stream, which could help you chain shyaml calls:

$ i=0; while true; do
      ((i++))
      echo "ingests:"
      echo " - data: xxx"
      echo "   id: tag-$i"
      if ((i >= 3)); then
          break
      fi
      echo "---"
      sleep 0.2
done | shyaml get-value ingests.0 -L -y | shyaml get-value id | tr '\0' '\n'
tag-1
tag-2
tag-3

An empty string will be still considered as an empty YAML document:

$ echo | shyaml get-value "toto"  ## docshtest: ignore-if LIBFYAML
Error: invalid path 'toto', can't query subvalue 'toto' of a leaf (leaf value is None).

An empty value in a mapping will be considered as "None":

$ echo "a: " | shyaml get-value "a"  ## docshtest: ignore-if LIBFYAML
None
$ echo "a: " | shyaml get-value -y "a"  ## docshtest: ignore-if LIBFYAML
null
$ echo "a: " | shyaml get-type "a"  ## docshtest: ignore-if LIBFYAML
NoneType
$ echo "a: " | shyaml key-values-0 "a" | tr '\0' '>'  ## docshtest: ignore-if LIBFYAML
a>None>
$ echo "a: " | shyaml key-values-0 -y "a" | tr '\0' '>'  ## docshtest: ignore-if LIBFYAML
a>null>

Keys containing '.'

Use and \\ to access keys with \ and \. to access keys with literal . in them. Just be mindful of shell escaping (example uses single quotes):

$ cat test.yaml | shyaml get-value 'subvalue\.how-much'
1.2
$ cat test.yaml | shyaml get-value 'subvalue\.how-much\\more'
1.3
$ cat test.yaml | shyaml get-value 'subvalue\.how-much\\.more' default
default

This last one didn't escape correctly the last ., this is the correct version:

$ cat test.yaml | shyaml get-value 'subvalue\.how-much\\\.more' default
1.4

empty string keys

Yep, shyaml supports empty stringed keys. You might never have use for this one, but it's in YAML specification. So shyaml supports it:

$ cat <<EOF > test.yaml
empty-sub-key:
    "":
       a: foo
       "": bar
"": wiz
EOF

$ cat test.yaml | shyaml get-value empty-sub-key..
bar
$ cat test.yaml | shyaml get-value ''
wiz

Please notice that one empty string is different than no string at all:

$ cat <<EOF > test.yaml
"":
   a: foo
   b: bar
"x": wiz
EOF
$ cat test.yaml | shyaml keys

x
$ cat test.yaml | shyaml keys ''
a
b

The first asks for keys of the root YAML, the second asks for keys of the content of the empty string named element located in the root YAML.

Handling missing paths

There is a third argument on the command line of shyaml which is the DEFAULT argument. If the given KEY was not found in the YAML structure, then shyaml would return what you provided as DEFAULT.

As of version < 0.3, this argument was defaulted to the empty string. For all version above 0.3 (included), if not provided, then an error message will be printed:

$ echo "a: 3" | shyaml get-value a mydefault
3

$ echo "a: 3" | shyaml get-value b mydefault
mydefault

$ echo "a: 3" | shyaml get-value b  ## docshtest: ignore-if LIBFYAML
Error: invalid path 'b', missing key 'b' in struct.

You can emulate pre v0.3 behavior by specifying explicitly an empty string as third argument:

$ echo "a: 3" | shyaml get-value b ''

Starting with version 0.6, you can also use the -q or --quiet to fail silently in case of KEY not found in the YAML structure:

$ echo "a: 3" | shyaml -q get-value b; echo "errlvl: $?"
errlvl: 1
$ echo "a: 3" | shyaml -q get-value a; echo "errlvl: $?"
3errlvl: 0

Ordered mappings

Currently, using shyaml in a shell script involves happily taking YAML inputs and outputting YAML outputs that will further be processed.

And this works very well.

Before version 0.4.0, shyaml would boldly re-order (sorting them alphabetically) the keys in mappings. If this should be considered harmless per specification (mappings are indeed supposed to be unordered, this means order does not matter), in practical, YAML users could feel wronged by shyaml when there YAML got mangled and they wanted to give a meaning to the basic YAML mapping.

Who am I to forbid such usage of YAML mappings ? So starting from version 0.4.0, shyaml will happily keep the order of your mappings:

$ cat <<EOF > test.yaml
mapping:
  a: 1
  c: 2
  b: 3
EOF

For shyaml version before 0.4.0:

# shyaml get-value mapping < test.yaml
a: 1
b: 3
c: 2

For shyaml version including and after 0.4.0:

$ shyaml get-value mapping < test.yaml
a: 1
c: 2
b: 3

Strict YAML for further processing

Processing yaml can be done recursively and extensively through using the output of shyaml into shyaml. Most of its output is itself YAML. Most ? Well, for ease of use, literal keys (string, numbers) are outputed directly without YAML quotes, which is often convenient.

But this has the consequence of introducing inconsistent behavior. So when processing YAML coming out of shyaml, you should probably think about using the --yaml (or -y) option to output only strict YAML.

With the drawback that when you'll want to output string, you'll need to call a last time shyaml get-value to explicitly unquote the YAML.

  1. Preserving types with -y

    In YAML, true is a boolean while 'true' is a string. Without -y, both would output as true, losing the type distinction:

    $ echo "a: true" | shyaml get-value a
    true
    $ echo "a: 'true'" | shyaml get-value a
    true
    

    With -y, the output preserves the original type so it can be correctly re-parsed:

    $ echo "a: true" | shyaml get-value -y a
    true
    $ echo "a: 'true'" | shyaml get-value -y a
    'true'
    

    The type is correctly preserved:

    $ echo "a: true" | shyaml get-type a
    bool
    $ echo "a: 'true'" | shyaml get-type a
    str
    

    The same applies to numbers and null values:

    $ echo "a: 123" | shyaml get-type a
    int
    $ echo "a: '123'" | shyaml get-type a
    str
    $ echo "a: 123" | shyaml get-value -y a
    123
    $ echo "a: '123'" | shyaml get-value -y a
    '123'
    
    $ echo "a: null" | shyaml get-type a
    NoneType
    $ echo "a: 'null'" | shyaml get-type a
    str
    $ echo "a: null" | shyaml get-value -y a
    null
    $ echo "a: 'null'" | shyaml get-value -y a
    'null'
    

Object Tag

YAML spec allows object tags which allows you to map local data to objects in your application.

When using shyaml, we do not want to mess with these tags, but still allow parsing their internal structure.

get-type will correctly give you the type of the object:

$ cat <<EOF > test.yaml
%TAG !e! tag:example.com,2000:app/
---
- !e!foo "bar"
EOF

$ shyaml get-type 0 < test.yaml
tag:example.com,2000:app/foo

get-value with -y (see section Strict YAML) will give you the complete yaml tagged value:

$ shyaml get-value -y 0 < test.yaml  ## docshtest: ignore-if LIBYAML,LIBFYAML
!<tag:example.com,2000:app/foo> 'bar'

Another example:

$ cat <<EOF > test.yaml
%TAG ! tag:clarkevans.com,2002:
--- !shape
  # Use the ! handle for presenting
  # tag:clarkevans.com,2002:circle
- !circle
  center: &ORIGIN {x: 73, y: 129}
  radius: 7
- !line
  start: *ORIGIN
  finish: { x: 89, y: 102 }
- !label
  start: *ORIGIN
  color: 0xFFEEBB
  text: Pretty vector drawing.
EOF
$ shyaml get-type 2 < test.yaml
tag:clarkevans.com,2002:label

And you can still traverse internal value:

$ shyaml get-value -y 2.start < test.yaml  ## docshtest: ignore-if LIBFYAML
x: 73
y: 129

Note that all global tags will be resolved and simplified (as !!map, !!str, !!seq), but not unknown local tags:

$ cat <<EOF > test.yaml
%YAML 1.1
---
!!map {
  ? !!str "sequence"
  : !!seq [ !!str "one", !!str "two" ],
  ? !!str "mapping"
  : !!map {
    ? !!str "sky" : !myobj "blue",
    ? !!str "sea" : !!str "green",
  },
}
EOF

$ shyaml get-value < test.yaml  ## docshtest: ignore-if LIBYAML,LIBFYAML
sequence:
- one
- two
mapping:
  sky: !myobj 'blue'
  sea: green

Empty documents

When provided with an empty document, shyaml will consider the document to hold a null value:

$ echo | shyaml get-value -y  ## docshtest: ignore-if LIBYAML,LIBFYAML
null
...

Version information

You can get useful information about the version and underlying library with shyaml --version (or -V):

# shyaml -V      ## Example of possible output
version: 0.1.0
libfyaml used: True
libfyaml available: 0.9.1-alpha
Rust: rustc 1.75.0 (82e1608df 2023-12-21)

Apply: Merging YAML Documents

The apply action merges overlay YAML files into a base document from stdin:

$ cat <<EOF > base.yaml
database:
  host: localhost
  port: 5432
paths:
  - /var/log
  - /var/cache
EOF

$ cat <<EOF > overlay.yaml
database:
  port: 3306
  user: admin
paths:
  - /var/data
EOF

$ cat base.yaml | shyaml apply overlay.yaml
database:
  host: localhost
  port: 3306
  user: admin
paths:
- /var/log
- /var/cache
- /var/data

Mappings are merged recursively. Sequences are appended with deduplication.

  1. Sequence Deduplication

    Sequences merge with deduplication. Duplicates are moved to their last occurrence position:

    $ cat <<EOF > base.yaml
    items:
      - !foo a
      - b
      - c
    EOF
    
    $ cat <<EOF > overlay.yaml
    items:
      - b
      - !bar d
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    items:
    - !foo a
    - c
    - b
    - !bar d
    

    In this example, b was in both base and overlay, so it moves to the end (where overlay placed it), and d is appended.

  2. Null Removes Keys

    Setting a key to null in an overlay removes it from the result:

    $ cat <<EOF > base.yaml
    keep: !foo 1
    remove: 2
    EOF
    
    $ cat <<EOF > overlay.yaml
    remove: null
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    keep: !foo 1
    

    This works recursively in nested mappings as well.

  3. Null Base Values

    When a base value is null (empty), the overlay value replaces it entirely. This allows overlays to populate initially empty sections:

    $ cat <<EOF > base.yaml
    config:
    database:
      host: localhost
    EOF
    
    $ cat <<EOF > overlay.yaml
    config:
      port: 8080
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    config:
      port: 8080
    database:
      host: localhost
    

    In this example, config: in the base has a null value, so the overlay's config mapping completely replaces it.

  4. Tagged Values

    Tagged values are preserved when not overlayed, and replaced when overlayed:

    $ cat <<EOF > base.yaml
    keep: !foo 1
    change: !bar old
    EOF
    
    $ cat <<EOF > overlay.yaml
    change: new
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    keep: !foo 1
    change: new
    

    Tags are also considered in sequence deduplication, so !foo 1 and 1 are treated as different values:

    $ cat <<EOF > base.yaml
    items:
      - !foo 1
      - 1
    EOF
    
    $ cat <<EOF > overlay.yaml
    items:
      - 1
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    items:
    - !foo 1
    - 1
    
  5. Merge Policies

    You can override the default merge behavior for specific paths using -m or --merge-policy. The format is PATH=POLICY where POLICY is one of:

    • merge - deep recursive merge (default): mappings are merged, sequences are appended
    • replace - overlay completely replaces base
    • prepend - overlay sequence is prepended to base sequence (falls back to replace for non-sequences)

    Multiple policies can be specified comma-separated or with multiple flags:

    $ cat <<EOF > base.yaml
    config:
      host: localhost
      port: 5432
    items:
      - a
      - b
    EOF
    
    $ cat <<EOF > overlay.yaml
    config:
      port: 3306
      user: admin
    items:
      - c
    EOF
    
    $ cat base.yaml | shyaml apply -m "config=replace" overlay.yaml
    config:
      port: 3306
      user: admin
    items:
    - a
    - b
    - c
    
    $ cat base.yaml | shyaml apply -m "items=prepend" overlay.yaml
    config:
      host: localhost
      port: 3306
      user: admin
    items:
    - c
    - a
    - b
    
    $ cat base.yaml | shyaml apply -m "config=replace,items=prepend" overlay.yaml
    config:
      port: 3306
      user: admin
    items:
    - c
    - a
    - b
    

    Policies apply to the specified path only; descendants use default merge.

  6. Inline Merge Directives

    You can specify merge behavior directly in the overlay YAML using tags in the merge: namespace:

    $ cat <<EOF > base.yaml
    paths:
      - /var/log
      - /var/cache
    config:
      host: localhost
      port: 5432
    EOF
    
    $ cat <<EOF > overlay.yaml
    paths: !merge:replace
      - /opt/data
    config: !merge:replace
      port: 3306
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    paths:
    - /opt/data
    config:
      port: 3306
    

    Available directives:

    • !merge:replace - completely replace the base value
    • !merge:append - append to sequence (this is the default for sequences)
    • !merge:prepend - prepend to sequence

    Prepend example:

    $ cat <<EOF > base.yaml
    search-paths:
      - /usr/lib
      - /lib
    EOF
    
    $ cat <<EOF > overlay.yaml
    search-paths: !merge:prepend
      - /opt/custom/lib
    EOF
    
    $ cat base.yaml | shyaml apply overlay.yaml
    search-paths:
    - /opt/custom/lib
    - /usr/lib
    - /lib
    
    1. Compound Tags

      Combine merge directives with other tags using semicolon concatenation. The merge directive is processed and stripped; other tags are preserved:

      $ cat <<EOF > base.yaml
      data: old-value
      EOF
      
      $ cat <<EOF > overlay.yaml
      data: !custom;merge:replace new-value
      EOF
      
      $ cat base.yaml | shyaml apply overlay.yaml
      data: !custom new-value
      
    2. CLI Policy Overrides Inline Tags

      When both a CLI merge policy (-m) and an inline tag are specified for the same path, the CLI policy takes precedence:

      $ cat <<EOF > base.yaml
      items:
        - a
        - b
      EOF
      
      $ cat <<EOF > overlay.yaml
      items: !merge:prepend
        - x
      EOF
      
      $ cat base.yaml | shyaml apply -m "items=replace" overlay.yaml
      items:
      - x
      
    3. Type Validation

      The !merge:append and !merge:prepend directives are only valid on sequences. Using them on other types produces an error:

      $ cat <<EOF > base.yaml
      config:
        host: localhost
      EOF
      
      $ cat <<EOF > overlay.yaml
      config: !merge:append
        port: 3306
      EOF
      
      $ cat base.yaml | shyaml apply overlay.yaml
      Error: Invalid merge directive at 'config': !merge:append can only be used on sequences, got mapping
      
      $ cat <<EOF > base.yaml
      name: alice
      EOF
      
      $ cat <<EOF > overlay.yaml
      name: !merge:prepend bob
      EOF
      
      $ cat base.yaml | shyaml apply overlay.yaml
      Error: Invalid merge directive at 'name': !merge:prepend can only be used on sequences, got string
      

      Note: CLI merge policies (-m) do not perform this validation since users may intentionally specify policies that fall back to other behaviors for non-sequence types.

Preserving Comments and Formatting

When using set-value, del, or apply, shyaml preserves the original document's formatting, including comments and style choices (flow vs block):

$ cat <<'EOF' | shyaml set-value database.port 3306
# Database configuration
database:
  host: localhost  # primary host
  port: 5432
EOF
# Database configuration
database:
  host: localhost # primary host
  port: 3306

Block-style sequences preserve comments and formatting:

$ cat <<'EOF' | shyaml set-value items.1 "modified"
# My items list
items:
  - first   # the first one
  - second
  - third
EOF
# My items list
items:
- first # the first one
- modified
- third
  1. Flow Style Limitation

    Flow-style collections ([...] and {...}) remain in flow style when modified, but are reformatted to multi-line output. This is a limitation of the underlying libfyaml library, which preserves the flow-vs-block distinction but not the one-line compactness of flow collections:

    $ echo "tags: [alpha, beta, gamma]" | shyaml del tags.1
    tags: [
      alpha,
      gamma
      ]
    

    If you need compact output, consider using block style in your source documents, which is preserved correctly:

    $ cat <<'EOF' | shyaml del tags.1
    tags:
      - alpha
      - beta
      - gamma
    EOF
    tags:
    - alpha
    - gamma
    

Set-Value: Modifying YAML Documents

The set-value action sets a value at a given path in a YAML document from stdin:

$ echo "name: old" | shyaml set-value name "new"
name: new

By default, the value is treated as a literal string:

$ echo "config:" | shyaml set-value config.host "localhost"
config:
  host: localhost

Nested paths are created automatically if they don't exist:

$ echo "" | shyaml set-value a.b.c "deep"
a:
  b:
    c: deep

Null values are automatically converted to mappings when setting nested paths:

$ echo "config:" | shyaml set-value config.host "localhost"
config:
  host: localhost

This also works for deeply nested paths through null values:

$ cat <<'EOF' | shyaml set-value a.b.c "value"
a:
  b:
EOF
a:
  b:
    c: value
  1. Interpreting Value as YAML

    Use -y to interpret the value as YAML instead of a literal string:

    $ echo "data:" | shyaml set-value data.items "[1, 2, 3]" -y
    data:
      items:
      - 1
      - 2
      - 3
    

    This allows setting complex structures:

    $ echo "config:" | shyaml set-value config.database "{host: localhost, port: 5432}" -y
    config:
      database:
        host: localhost
        port: 5432
    

    Without -y, the same value would be stored as a literal string:

    $ echo "config:" | shyaml set-value config.database "{host: localhost, port: 5432}"
    config:
      database: "{host: localhost, port: 5432}"
    
  2. Working with Sequences

    You can set values at sequence indices:

    $ cat <<'EOF' | shyaml set-value items.1 "changed"
    items:
      - a
      - b
      - c
    EOF
    items:
    - a
    - changed
    - c
    

    Negative indices work from the end:

    $ cat <<'EOF' | shyaml set-value items.-1 "last"
    items:
      - a
      - b
      - c
    EOF
    items:
    - a
    - b
    - last
    

Del: Removing Keys from YAML Documents

The del action removes a key or sequence element at a given path:

$ cat <<EOF | shyaml del b
a: 1
b: 2
c: 3
EOF
a: 1
c: 3

Nested paths work as expected:

$ cat <<EOF | shyaml del config.db.port
config:
  db:
    host: localhost
    port: 5432
EOF
config:
  db:
    host: localhost
  1. Deleting from Sequences

    You can delete elements from sequences by index:

    $ cat <<'EOF' | shyaml del items.1
    items:
      - a
      - b
      - c
    EOF
    items:
    - a
    - c
    

    Negative indices work from the end:

    $ cat <<'EOF' | shyaml del items.-1
    items:
      - a
      - b
      - c
    EOF
    items:
    - a
    - b
    
  2. Error Handling

    Attempting to delete a non-existent key produces an error:

    $ echo "a: 1" | shyaml del nonexistent
    Error: invalid path 'nonexistent', missing key 'nonexistent' in struct.
    

Compound Actions: Chaining Multiple Commands

Multiple commands can be chained together using the \; separator. Commands are executed sequentially, with each command operating on the result of the previous one. Only the final result is output.

$ echo "a: 1" | shyaml set-value b 2 \; set-value c 3
a: 1
b: 2
c: 3

This is more efficient than piping multiple shyaml calls, as the YAML is parsed only once.

  1. Combining Different Actions

    Any combination of actions can be chained:

    $ cat <<EOF | shyaml set-value new-key added \; del remove-me
    keep: 1
    remove-me: 2
    EOF
    keep: 1
    new-key: added
    
  2. Multi-step Transformations

    Build complex transformations step by step:

    $ echo "x: 0" | shyaml set-value a 1 \; set-value b 2 \; del x \; set-value c 3
    a: 1
    b: 2
    c: 3
    
  3. With Multi-document YAML

    Compound commands work with multi-document YAML streams. The command chain is applied to each document independently:

    $ cat <<EOF | shyaml set-value b 10 \; del a | tr '\0' '&'
    a: 1
    ---
    a: 2
    EOF
    b: 10
    &b: 10
    

Contributing

Any suggestions or issues are welcome. Push requests are very welcome, please check out the guidelines.

Push Request Guidelines

You can send any code. I'll look at it and will integrate it myself in the code base and leave you as the author. This process can take time and it'll take less time if you follow the following guidelines:

  • Run cargo fmt before committing
  • Run cargo clippy and address any warnings
  • Separate your commits per smallest concern
  • Each commit should pass the tests (to allow easy bisect)
  • Each functionality/bugfix commit should contain the code, tests, and doc
  • Prior minor commit with typographic or code cosmetic changes are very welcome. These should be tagged in their commit summary with !minor
  • The commit message should follow gitchangelog rules (check the git log to get examples)
  • If the commit fixes an issue or finished the implementation of a feature, please mention it in the summary

If you have some questions about guidelines which is not answered here, please check the current git log, you might find previous commit that shows you how to deal with your issue.

License

Copyright (c) 2024-2026 Valentin Lab.

Licensed under the MIT License.

Legacy Python Version

The original Python implementation of shyaml is available at github.com/0k/shyaml and can be installed via pip:

pip install shyaml

The Python version includes a Python API for programmatic access to YAML data, which is not available in this Rust version.

Changelog

0.3.0 (2026-01-24)

New

  • Preserve comments and formatting during mutations. [Valentin Lab]

    set-value, del, and apply now preserve original document formatting including comments and quote styles. Previously, mutations would reformat the entire document.

    Also fixes multi-document YAML streams with compound commands incorrectly including --- markers in non-yaml output mode.

    Additional improvements:

    • Null values automatically convert to mappings when setting nested paths
    • Better handling of null base values in apply

0.2.0 (2026-01-24)

New

  • Add compound actions for chaining multiple commands. [Valentin Lab]

    Multiple commands can now be chained using ; separator (escaped as \; in shell). Commands execute sequentially, each operating on the result of the previous one, with only the final result output.

    Example: echo "a: 1" | shyaml set-value b 2 \; set-value c 3

    This change includes a major internal refactoring from streaming Node-based processing to a Value-based API that passes YAML documents through the command chain.

    Also adds SIGPIPE handling to prevent broken pipe errors when output is piped to commands like head.

  • Add del action for removing keys from YAML documents. [Valentin Lab]

    Supports:

    • Deleting keys from mappings: shyaml del config.db.port
    • Deleting sequence elements by index: shyaml del items.1
    • Negative indices: shyaml del items.-1

    Proper error handling for missing keys, out-of-range indices, and invalid paths.

  • Add set-value action for modifying YAML documents. [Valentin Lab]

    Adds a new set-value action that sets a value at a given path:

    • Takes key (path) and value arguments
    • Value is treated as literal string by default
    • With -y flag, value is parsed as YAML

    Features:

    • Supports nested paths with dot-notation (e.g., a.b.c)
    • Auto-creates intermediate mappings for non-existent paths
    • Works with sequence indices (positive and negative)
    • Handles path escaping consistent with other actions

    Updates README.org with:

    • New set-value documentation section with examples
    • Updated feature descriptions and comparison table
  • Add inline merge directive tags. [Valentin Lab]

    Support !merge:replace, !merge:append, and !merge:prepend tags directly in overlay YAML files. This allows fine-grained merge control without CLI arguments.

    Tags can be combined with other tags using semicolon concatenation (e.g., !custom;merge:replace). CLI -m policies still take precedence over inline directives. Type validation ensures append/prepend are only used on sequences.

Changes

  • Rewrite musl-build for true static linking. [Valentin Lab]

    Rename to musl-static-build and switch from Alpine native build to Debian-based cross-compilation to x86_64-unknown-linux-musl target.

    The Alpine approach produced dynamically linked musl binaries. Cross-compiling from glibc host to musl target produces fully static binaries with musl libc embedded.

Fix

  • Correct dev version format to follow semver pre-release syntax. [Valentin Lab]

    Changes {version}.dev{tag} to {version}-dev.{tag} to comply with SemVer specification where pre-release versions use hyphen as separator (e.g., 1.0.0-dev.12345 instead of 1.0.0.dev12345).

0.1.0 (2024-01-30)

Other

  • First import. [Valentin Lab]