DEV Community

Cover image for Why I Built yesh: Linux Automation You Can Explain
Roman Belshevitz
Roman Belshevitz

Posted on Edited on

Why I Built yesh: Linux Automation You Can Explain

Small Linux automation scripts tend to grow in a predictable way: they start as a few commands and gradually accumulate validation, quoting, branching, file replacement, service restarts, and error handling.

Bash is a remarkable tool. It is fast to start, available on almost every
Linux system, and excellent when the job is shell work: interactive commands, short glue, and composition of existing Unix tools. I use it and respect it.

The job changes when a script becomes an operational procedure. A small script that combines preflight checks, file publication, service changes, and failure handling must also explain what it will check, change, and leave untouched. That is where Bash's historical rules can make the script harder to review than the underlying commands themselves.

I built yesh for that gap: scripts that stay close to Linux commands while making their arguments, ordering, failure boundaries, and planned changes easy to review.

The goal is not to replace Bash, Python, or Ansible. Each tool has a different job. yesh is for the awkward middle: a small administrative script that is too important to hide behind shell tricks, but too close to the Linux command line to justify a larger automation framework.

The question is not “can it run?”

For operational code, a successful exit status is only one part of the story.

Before running a script, an operator should be able to answer:

  • What does it check first?
  • What exact arguments reach each external process?
  • Which files or services can change?
  • What happens if a command fails halfway through?
  • What remains unchanged after a failed run?

That is the standard behind yesh. The rest of the design follows from it: keep Linux utilities visible, make the surrounding procedure explicit, and make the failure boundary reviewable before execution.

The same intent, with a visible boundary

Consider a configuration update:

set -euo pipefail

if grep -q '^mode=production$' config.next; then
    install -m 0640 -- config.next /etc/myapp.conf.next
    mv -T /etc/myapp.conf.next /etc/myapp.conf
    systemctl restart myapp.service
    systemctl is-active --quiet myapp.service
fi
Enter fullscreen mode Exit fullscreen mode

Here is the same operational intent in yesh:

if grep -q '^mode=production$' config.next:
    install -m 0640 -- config.next /etc/myapp.conf.next
    publish /etc/myapp.conf.next to /etc/myapp.conf
    service restart myapp.service until active within 10s
Enter fullscreen mode Exit fullscreen mode

The yesh version earns its keep because the sequence reads like an operational procedure: validate, prepare, publish, restart, and verify a bounded postcondition. It does not need to be dramatically shorter. This is the kind of procedure yesh is designed to make readable.

publish uses a same-filesystem rename and does not silently fall back to
copying. The typed service operation has a finite deadline and checks that the exact unit becomes active. The script shows these boundaries directly instead of leaving a reviewer to reconstruct them.

Bash is excellent when the job is shell work

Bash remains excellent for interactive work, one-off commands, and the
enormous body of existing Unix tooling. yesh is not an argument against using Bash for those jobs.

The boundary appears when the main risk is not writing the command, but
reviewing what the procedure will do. Bash's flexibility can make that review harder: implicit word splitting, multiple quoting rules, several rounds of shell expansion, several condition syntaxes, and failure behavior that depends on context.

yesh makes a narrower set of choices to reduce that review burden:

message = "backup finished"
hosts = [web-1, web-2]

echo $message

for host in $hosts:
    echo $host
Enter fullscreen mode Exit fullscreen mode

A string remains one value. A list expands into separate arguments. A variable is expanded only with the explicit $name form; a bare word remains a literal. Values are not reparsed as code and are not split implicitly by whitespace.

Those rules remove a category of shell reasoning from the review. They do not make the external command safe or guarantee that it will succeed.

Python solves a different problem

Python is often the right answer when the problem needs data structures,
libraries, APIs, concurrency, or substantial application logic. But many Linux administration tasks are primarily sequences of existing commands:

check a mount
inspect a directory
run rsync with explicit arguments
publish a completed result
restart one service
verify its state
Enter fullscreen mode Exit fullscreen mode

For this class of task, a language that keeps Linux commands visible can be easier to audit than a Python wrapper that recreates process execution,
quoting, timeouts, and error reporting around every command.

That does not make Python a poor choice. It means yesh has a different center of gravity: a small language surface and a command-oriented runtime for procedures built from existing Linux tools.

Ansible coordinates a different layer

Ansible is an orchestration system with inventory, modules, roles, and a
desired-state model for many hosts. yesh is a small Linux runbook and preflight language for transparent local workflows and command-level steps inside larger automation.

yesh keeps ssh, rsync, systemctl, and other tools visible instead of
hiding them behind a new module model. Use Ansible for fleet orchestration and idempotent desired state. Use yesh when a Bash-like Unix workflow needs clearer syntax, explicit arguments, and reviewable execution.

The relationship is complementary. Ansible can select hosts and coordinate a rollout; a yesh script can perform or inspect one carefully bounded Linux operation on a host. An administrator can also run that runbook directly when a full inventory and role model would add more machinery than the task needs.

With that boundary established, the important question becomes: what does
yesh make visible inside one procedure?

Commands remain commands

yesh does not turn a line into one shell string and pass it to sh -c.
Commands are parsed into an argument vector and executed directly by the Go runtime.

install -m 0640 -- config.next /etc/myapp.conf.next
Enter fullscreen mode Exit fullscreen mode

The process receives these separate arguments:

["install", "-m", "0640", "--", "config.next", "/etc/myapp.conf.next"]
Enter fullscreen mode Exit fullscreen mode

This keeps argument boundaries visible when values contain spaces, commas, or characters that would have a special meaning to a shell. It also means that yesh does not reinterpret a value as a new command or operator.

Linux tools remain visible and retain their native semantics. rsync is still rsync; systemctl is still systemctl; ssh is still ssh. yesh provides the surrounding checks, ordering, and failure behavior.

--plan makes review practical

The argument boundary is only half of the review problem. Before a script
changes state, yesh can also show the expanded work:

./yesh --plan examples/service-restart.yesh
Enter fullscreen mode Exit fullscreen mode

The plan shows expanded external commands and possible branches without
starting processes or opening redirect files:

PLAN ["grep" "-q" "^mode=production$" "config.next"]
PLAN ["install" "-m" "0640" "--" "config.next" "/etc/myapp.conf.next"]
PLAN publish "/etc/myapp.conf.next" to "/etc/myapp.conf"
PLAN ["systemctl" "restart" "myapp.service"] until active within 10s
Enter fullscreen mode Exit fullscreen mode

Bracketed lines are external argv. Named lines are yesh operations with
their own semantics. The output is not a transaction and does not eliminate races between checking and acting. It is a concrete description of the work the script would attempt, suitable for review.

Failure is part of the design

The plan describes intended work. Execution still needs a clear failure rule. An ordinary command failure stops the script by default. A non-zero result used as an external condition can select an explicit else branch. A missing executable, startup failure, cancellation, signal, or timeout remains an execution error; yesh does not quietly turn it into a normal branch.

source = "/srv/releases/current"

if exists $source:
    echo "release is present"
else:
    echo "release is missing"
    exit 2
Enter fullscreen mode Exit fullscreen mode

If any pipeline stage fails, the pipeline fails. On POSIX systems, cancellation terminates the owned process group and waits for child processes instead of only killing the first visible process.

This is not a claim that external programs are safe. rsync can still delete data if given destructive arguments. systemctl can still interrupt a service. Direct argument execution prevents shell reinterpretation; it does not give an arbitrary executable good intentions.

The runtime has to earn that promise

The runtime is written in Go, so the hard part is not only parsing commands. It is also making cancellation and cleanup predictable when several processes and goroutines are involved.

The project has been tested with more than 200 automated tests and four fuzz targets. The POSIX/Linux tests cover pipeline failure, SIGPIPE, process groups, timeouts, cancellation, child-process reaping, redirect cleanup, and repeated cancellation. The race-instrumented lifecycle tests also watch for file-descriptor and goroutine leaks after interrupted jobs.

I also ran ten integration tests in disposable Debian and Rocky Linux environments. These tests exercised the documented Linux workflows under controlled conditions. The results support the tested systems and scenarios; they do not prove universal compatibility.

That work matters because killing the first process is not the same as stopping a pipeline. Returning an error is not enough if a child or a cleanup goroutine remains behind. The runtime must stop the work it owns and wait for the processes it started.

A backup makes the boundary concrete

Backups expose whether a language makes failure boundaries clear. “The command returned zero” is not enough. The sysadmin also needs to know whether the source existed, whether the archive was complete, whether a partial result became visible, and whether a rerun remains understandable.

source = "source"
partial = "backup.tar.gz.partial"
archive = "backup.tar.gz"

if exists $source:
    tar -cf - -- $source | gzip > $partial
    mv -- $partial $archive
else:
    echo "source directory is missing"
    exit 2
Enter fullscreen mode Exit fullscreen mode

The producer writes the private name first. The published name changes only after the producer succeeds. That is a visible publication boundary, not a claim of power-loss durability.

The same reasoning applies to incremental snapshots. yesh can make the
preflight, lock, staging, and publication sequence explicit while leaving
rsync and rsnapshot semantics alone. It does not silently add --delete, rewrite their option models, or pretend that a backup engine has become part of the language.

What yesh does not promise

yesh is not an interactive shell, a Bash-compatible replacement, a Python
competitor, an Ansible alternative, or a backup engine.

There is no command substitution, implicit background execution, eval,
unbounded while loop, hidden sudo, or general-purpose object model in the small core. Those omissions are deliberate. A feature belongs only when it makes an administrative script easier to read, review, and safely repeat.

The current prototype supports assignments, strings, lists, control flow,
pipelines, redirections, bounded process execution, locks, same-filesystem
publication, --plan, read-only storage inspection, and bounded systemd and journal operations. Practical Linux scenarios cover backup, restore, rsync, rsnapshot, SSH, services, processes, and configuration publication.

It is still a prototype, not a production safety boundary. External programs, privileges, paths, filesystems, kernels, remote hosts, and service behavior remain real operational concerns.

The narrower claim is the useful one: yesh makes a script's intent and failure boundaries easier to inspect.

That is why I built it.

Explore yesh on GitHub

Top comments (0)