Mars Rovers X: The Grid Viz Solutions — Bash
This is part of the Mars Rovers series.
So after being able to plot The Grid Viz in Emacs Lisp, I went for Bash.
The Adaptation
Although I tried to keep as much of the same logic as possible, in a few places it made sense to change things — to simplify and adapt to the specifics of the language. With about 87 LoC, I was able to plot the exact same grids as the Lisps.
As with my Bash solution to the original problem, I translated it manually, from my Clojure solution, function by function. Predictably, translating it to Bash was less straightforward than translating it to Elisp. That’s ok. I believe this process of writing Bash while thinking from a Clojure perspective — and pushing it towards Clojure’s way of doing things — elevated my Bash.
There’re limits to this. Trying to force Bash to receive “a map of non-default string values to replace placeholder keywords” would be silly. That’s what command line flags are for. It seemed much more natural to put this whole logic inside main(), looping over key–value pairs of command line flags, which can be done concisely, with just this:
(-[pmvVzZNESWs]) declare "${1:1}"="$2"; shift ;;
as opposed to this much more usual 11-line block:
(-p) p="$2"; shift ;; (-m) m="$2"; shift ;; (-v) v="$2"; shift ;; # … (-s) s="$2"; shift ;;
Setting defaults comes just after that.
And then I let a 10-variable-replacement sed creep into main(). I thought twice about this. Yet decomplecting it into a separate function that parses 10 positional arguments to produce said sed seemed unnecessarily bureaucratic here.
The Code
#!/usr/bin/env bash ## Rover Grid --- Mars Rovers' Grid Viz Problem # SPDX-FileCopyrightText: © flandrew <https://flandrew.srht.site/listful> # SPDX-License-Identifier: GPL-3.0-or-later #---------------------# # Author: flandrew # # Updated: 2026-04-18 # #---------------------# # Version: 0.1.0 # #---------------------# ## Commentary # # My solutions to The Mars Rovers' Grid Viz Problem. # # Usage: # chmod +x rover-grid.sh # and then: # echo "$inputstring" | ./rover-grid.sh OPTS # or: # cat "$inputfile" | ./rover-grid.sh OPTS # # OPTS: [-(pmvVzZNESWs) VALUE] # ############################################################################# #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ## Code ### Settings and requires set -eo pipefail hash getopt grep sed || exit 127 source ./rover.sh : || exit 3 #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Functions #### Main main() { if ! piped; then usage; exit 2; fi # Parse command line args local p m v V z Z N E S W s opt=$(getopt -n rover-grid -o 'p:m:v:V:z:Z:N:E:S:W:s:h' -- "$@") eval set -- "$opt" while :; do case "$1" in (-[pmvVzZNESWs]) declare "${1:1}"="$2"; shift ;; (-h) usage; exit "$?" ;; (--) shift; break ;; (* ) usage; exit 1 ;; esac; shift; continue done # Set still-unset variables with default values : "${Z:=───} ${V:=│} ${N:=N} ${E:=E} ${z:= } ${v:= } ${S:=S} ${W:=W} ${m:= } ${p:=·} ${s:==}" # Receive input string (piped from stdin) output-keys-grids | sed -E "s/p/$p/g; s/v/$v/g; s/z/$z/g; s/N/$N/g; s/S/$S/g s/m/$m/g; s/V/$V/g; s/Z/$Z/g; s/E/$E/g; s/W/$W/g" | separate "$s" "$N$E$S$W" ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Extract path points and sides # Original points will be mapped to text grid with (x,y) → (2y+1,2x+1), # and then vertically flipped at the end for printing. stretch-switch() { local x y; read -r x y < <(tr , ' ') printf '%s\n' "$((2*y+1)),$((2*x+1))" ;} midpoint() { local x1 y1 x2 y2; read -r x1 y1 x2 y2 < <(tr , ' ') printf '%s\n' "$(((x1+x2)/2)),$(((y1+y2)/2))" ;} partition-2-1() { sed "2,$ s/.*/&\n&/" | sed '$ d' | sed 'N;s/\n/ /' ;} path-sides() { tr : '\n' | maplines stretch-switch | partition-2-1 | maplines midpoint ;} side-type() { local x y; read -r x y < <(tr , ' ') case "$((x%2))$((y%2))" in 10) : Z ;; 01) : V ;; * ) : out ;; # rover rotations esac; echo "$x,$y $_" ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Build grid ##### Make new grid-new() { local xM="$1" yM="$2" rowp rowm rowp="p$(rep "$xM" zp)" rowm="v$(rep "$xM" mv)" : "$rowp\n$rowm\n";: "$(rep "$yM" "$_")" printf '%s\n%s' "${_@E}" "$rowp" | grep . ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ##### Update with rover rover-make-input() { local init id x y endh endp begp amap path sides groupd Zs Vs read -r init # init is piped :; <<<"$init" read -r id x y _ _ begp=$( <<<"$x,$y" stretch-switch) amap=$( <<<"$init" rover-map-all) last=$( <<<"$amap" last-maps) :; <<<"$last" read -r _ x y endh _ endp=$( <<<"$x,$y" stretch-switch) path=$( <<<"$amap" mapcells xy) sides=$( <<<"$path" path-sides) groupd=$(<<<"$sides" maplines side-type) Zs=$( <<<"$groupd" grep Z$) Vs=$( <<<"$groupd" grep V$) printf '%s\n%s\n%s\n%s\n' "$begp $id" "$endp $endh" "$Zs" "$Vs" ;} rover-make-updates-from-map() { tr , ' ' | sed -E "s@([^ ]+) +([^ ]+) +(.) *@\1 s/./\3/\2@" ;} rover-init-to-grid-keys() { local xM="$1" yM="$2" # init is piped : "$(rover-make-input | rover-make-updates-from-map)" grid-new "$xM" "$yM" | sed -E "$_" | tac ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ##### Output keys grids output-keys-grids() { parse-input | (read -r _xm _ym xM yM maplines rover-init-to-grid-keys "$xM" "$yM") ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### From keys to strings separate() { local s="$1" NESW="$2" inp wid hei num sep inp=$(</dev/stdin) wid=$(<<<"$inp" wc -L) hei=$(<<<"$inp" wc -l) num=$(<<<"$inp" grep -c "[$NESW]") sep=$(rep "$wid" "$s" | grep -oE "^.{$wid}") <<<"$inp" sed "1~$((hei/num)) i$sep" printf -- '%s\n' "$sep" ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Utilities and help piped() [[ ! -t 0 ]] rep() if (("$1">0)); then printf "%*s\n" "$1" " " | sed -E "s/ /$2/g"; fi usage() { echo "echo INPUTSTRING | ./${0##*/} [-(pmvVzZNESWs) VALUE]" ;} #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Run it main "$@" exit 0 # Local Variables: # coding: utf-8 # indent-tabs-mode: nil # sentence-end-double-space: nil # outline-regexp: "###* " # End: ## Rover Grid ends here
The Tests
#!/usr/bin/env bash ## Rover Grid --- Mars Rovers' Grid problem: Tests #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Tests #### Our sample input good_input="\ 5 5 1 2 N LMLMLMLMM 3 3 E MMRMMRMRRM" run-tests-good() { echo -e "Good input:\n$good_input" while read -r cmd do echo -e "\n$ $cmd" eval -- "$cmd" done <<'CMDs' <<<"$good_input" ./rover-grid.sh <<<"$good_input" ./rover-grid.sh -p • -s ★ <<<"$good_input" ./rover-grid.sh -p ⋆ -s "★-☆~" <<<"$good_input" ./rover-grid.sh -p " " -V ┆ -Z "┄┄┄" <<<"$good_input" ./rover-grid.sh -N ⮙ -E ⮚ -S ⮛ -W ⮘ CMDs } #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Some tiny cases run-tests-tiny() { read -rd'\0' tiny <<'_' # Tiny input: tiny_0a="0 0" tiny_0b="1 1" tiny_1a="1 1 0 0 N MRMRMRML" # returns to origin tiny_1b="1 1 0 0 N " # never leaves origin — doesn't even rotate tiny_1c="2 1 0 0 N MRM 2 1 N LLMRMM" _ echo -e "$tiny" eval -- "$tiny" while read -r cmd do echo -e "\n$ $cmd" eval -- "$cmd" done <<'CMDs' for v in 0a 0b 1a 1b 1c; do echo; echo "tiny_$v"; <<<"${!_}" ./rover-grid.sh; done CMDs } #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### How is Mars? run-tests-news() { read -rd'\0' news <<'_' # Hey, rovers — how is Mars? Send us some news! news="3 1 0 0 N M 1 0 N MR 2 0 N ML 3 0 N MRR" _ echo -e "$news" eval -- "$news" while read -r cmd do echo -e "\n$ $cmd" eval -- "$cmd" done <<'CMDs' echo "$news" | ./rover-grid.sh CMDs } #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ #### Rover ponders big questions run-tests-life() { read -rd'\0' life <<'_' # Rover, what is the answer to life, the universe, and everything? life="7 6 1 5 S MMLMMLMMRRMMMMLMMMRRMMRMMRMMLMMLMMR" _ echo -e "$life" eval -- "$life" while read -r cmd do echo -e "\n$ $cmd" eval -- "$cmd" done <<'CMDs' echo "$life" | ./rover-grid.sh CMDs } #⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ ### Run it! run-tests-all() { run-tests-good; echo run-tests-tiny; echo run-tests-news; echo run-tests-life ;} run-tests() if [[ "$1" ]] then for test; do run-tests-"$test"; done else run-tests-all fi run-tests "$@" exit 0
Let’s now run the above:
Good
./tests-grid.sh good
Good input:
5 5
1 2 N
LMLMLMLMM
3 3 E
MMRMMRMRRM
$ <<<"$good_input" ./rover-grid.sh
=====================
· · · · · ·
· · · · · ·
· N · · · ·
│
·───1 · · · ·
│ │
·───· · · · ·
· · · · · ·
=====================
· · · · · ·
· · · · · ·
· · · 2───·───·
│
· · · · · ·
│
· · · · ·───E
· · · · · ·
=====================
$ <<<"$good_input" ./rover-grid.sh -p • -s ★
★★★★★★★★★★★★★★★★★★★★★
• • • • • •
• • • • • •
• N • • • •
│
•───1 • • • •
│ │
•───• • • • •
• • • • • •
★★★★★★★★★★★★★★★★★★★★★
• • • • • •
• • • • • •
• • • 2───•───•
│
• • • • • •
│
• • • • •───E
• • • • • •
★★★★★★★★★★★★★★★★★★★★★
$ <<<"$good_input" ./rover-grid.sh -p ⋆ -s "★-☆~"
★-☆~★-☆~★-☆~★-☆~★-☆~★
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
⋆ N ⋆ ⋆ ⋆ ⋆
│
⋆───1 ⋆ ⋆ ⋆ ⋆
│ │
⋆───⋆ ⋆ ⋆ ⋆ ⋆
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
★-☆~★-☆~★-☆~★-☆~★-☆~★
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
⋆ ⋆ ⋆ 2───⋆───⋆
│
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
│
⋆ ⋆ ⋆ ⋆ ⋆───E
⋆ ⋆ ⋆ ⋆ ⋆ ⋆
★-☆~★-☆~★-☆~★-☆~★-☆~★
$ <<<"$good_input" ./rover-grid.sh -p " " -V ┆ -Z "┄┄┄"
=====================
N
┆
┄┄┄1
┆ ┆
┄┄┄
=====================
2┄┄┄ ┄┄┄
┆
┆
┄┄┄E
=====================
$ <<<"$good_input" ./rover-grid.sh -N ⮙ -E ⮚ -S ⮛ -W ⮘
=====================
· · · · · ·
· · · · · ·
· ⮙ · · · ·
│
·───1 · · · ·
│ │
·───· · · · ·
· · · · · ·
=====================
· · · · · ·
· · · · · ·
· · · 2───·───·
│
· · · · · ·
│
· · · · ·───⮚
· · · · · ·
=====================
Tiny
./tests-grid.sh tiny
# Tiny input:
tiny_0a="0 0"
tiny_0b="1 1"
tiny_1a="1 1
0 0 N
MRMRMRML" # returns to origin
tiny_1b="1 1
0 0 N
" # never leaves origin — doesn't even rotate
tiny_1c="2 1
0 0 N
MRM
2 1 N
LLMRMM"
$ for v in 0a 0b 1a 1b 1c; do echo; echo "tiny_$v"; <<<"${!_}" ./rover-grid.sh; done
tiny_0a
tiny_0b
tiny_1a
=====
·───·
│ │
S───·
=====
tiny_1b
=====
· ·
N ·
=====
tiny_1c
=========
·───E ·
│
1 · ·
=========
· · 2
│
W───·───·
=========
News
./tests-grid.sh news
# Hey, rovers — how is Mars? Send us some news!
news="3 1
0 0 N
M
1 0 N
MR
2 0 N
ML
3 0 N
MRR"
$ echo "$news" | ./rover-grid.sh
=============
N · · ·
│
1 · · ·
=============
· E · ·
│
· 2 · ·
=============
· · W ·
│
· · 3 ·
=============
· · · S
│
· · · 4
=============
Life
./tests-grid.sh life
# Rover, what is the answer to life, the universe, and everything?
life="7 6
1 5 S
MMLMMLMMRRMMMMLMMMRRMMRMMRMMLMMLMMR"
$ echo "$life" | ./rover-grid.sh
=============================
· · · · · · · ·
· 1 · · N───·───· ·
│ │ │
· · · · · · · ·
│ │ │
· ·───·───· ·───·───· ·
│ │
· · · · · · · ·
│ │
· · · ·───·───·───· ·
· · · · · · · ·
=============================
And with this 7×6 grid we conclude our tests — and (maybe) this series.
📆 2026-W16-7📆 2026-04-19