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