Shtack
======
 
Clemens Lahme
 
2026-08-27
 
Table of Contents
-----------------
  1. The Shell with a Stack
  2. System Requirements
  3. License
  4. Setup
    1. Addition to PATH
    2. Source 'scd' in Your Shell
    3. Verify the installation
  5. Use Case Example
    1. Advantages & Extended Use Cases
    2. Extended Examples
    3. Naming
  6. Shtack Pros
    1. Cons
  7. timestamp
  8. First Scripts
  9. scd
  10. Drop
    1. Pop
  11. Swap
  12. Rotation
    1. nrrot and nlrot
  13. Release History
 
1. The Shell with a Stack
-------------------------
 
A stack is my shtick. So let's use it with a shell.
 
You can always add a stack to your environment, on your desk, with papers, or
books, put something on top, take something from the top - and there you
go. Same in any programming language that has a datastructure, where you can
store values. Bash has variables, but it has also the file system. So we use
that for a stack and off we go.
 
Shtack is a collection of shell or scripts in other languages, that use a
global stack on the file system. So the user can use the stack from different
terminal and share information and e.g. use it to jump quickly to different
locations.
 
This tool makes it convenient to exchange paths or other data clipboard like
between different terminals.
 
2. System Requirements
----------------------
 
- Linux with GNU coreutils for 'date' with nanoseconds.
- Bash 4.0 or later for 'mapfile/readarray'.
 
3. License
----------
 
Gossip is licensed under the GNU Public License (GPL) version 2, see:
 
file://COPYING.txt
 
4. Setup
--------
 
4.1. Addition to PATH
'''''''''''''''''''''
 
The new Shtack commands ('push', 'pop', 'drop', 'swap', 'rrot', 'lrot',
'timestamp', etc.) are standalone scripts. Add the 'bin' directory to your
PATH by appending the following line to your shell configuration file
('~/.bashrc' or '~/.bash_profile'):
 
export PATH="$HOME/shtack/bin:$PATH"
 
Replace '$HOME/shtack/bin' with the actual installation path.
 
4.2. Source 'scd' in Your Shell
'''''''''''''''''''''''''''''''
 
Unlike the other commands, 'scd' is a shell function designed to modify the
current working directory. It must be sourced, not executed. Add this line to
your shell configuration file:
 
source "$HOME/shtack/bin/scd"
 
This ensures 'scd' is available in your interactive shell session and can
directly alter '$PWD'.
 
Again, adapt to the real location of your Shtack installation.
 
Reload your shell configuration:
 
source ~/.bashrc  # or ~/.bash_profile
 
The scripts will automatically create '$HOME/.cache/shtack/' if it doesn't
exist. Ensure your user has write permissions to this directory. No manual
initialization is required.
 
4.3. Verify the installation
''''''''''''''''''''''''''''
 
# Confirm commands are in PATH
which timestamp push pop swap rrot lrot
type scp
 
5. Use Case Example
-------------------
 
# Test basic stack operations
push
push /tmp
dup
.s
pop
scd
scd
 
Those commands do the following:
 
1. push: pushes your current path to the stack.
2. push /tmp: pushes the /tmp location on the stack.
3. dup: Duplicates the /tmp location on the stack.
4. .s: Print the stack, should be somthing like: /home/me/shtack /tmp /tmp
5. pop: Prints the last /tmp and removes it from the stack.
6. scd: Uses the last /tmp on the stack and changes the directory to
   there. /tmp is also removed from the stack.
6. scd: We go back where we started, e.g. to /home/me/shtack. The stack is now
   empty.
 
5.1. Advantages & Extended Use Cases
''''''''''''''''''''''''''''''''''''
 
Unlike Bash's 'pushd'/'popd' or shell variables, Shtack persists across
terminal sessions, processes, and multiple concurrent terminals. While 'pushd'
only remembers the last directory and hides the rest of the stack, Shtack keeps
a full, visible history via '.s'.
 
The rotation commands ('nlrot', 'nrrot', 'lrot', 'rrot') transform Shtack from
a simple LIFO structure into a flexible navigation tool. You can reorder items
to bring any directory to the top/end without losing the rest of the
stack. Combined with 'dup', you can inspect or use a value without removing it.
 
Because the stack lives in '$HOME/.cache/shtack/', it can theoretically be
shared across machines via a mounted network drive (NFS, SMB, SSHFS), enabling
synchronized navigation contexts across your entire workflow.
 
You can of course also put other "strings" on the stack than directory
locations, be it todo items, short reminder, or your grocery store buylist.
 
5.2. Extended Examples
''''''''''''''''''''''
 
# 1. Cross-terminal persistence
# Terminal A:
push
push /usr/share/consolefonts
.s
# Terminal B (opened later):
.s  # Shows the exact same stack!
scd # Works seamlessly without re-typing paths
.s  # We are now in /usr/share/consolfonts too and that path got removed from
    # the stack.
 
# 2. Reordering with nlrot/nrrot
push
push /opt
push /usr
push /var
push /tmp
push /etc
.s        # Output: /home/user /opt /usr /var /tmp /etc
nlrot 4   # Left-rotates the last 4 (/usr, /var, /tmp, /etc) -> /usr becomes
          # newest, the other three shift left/up.
.s        # Output: /home/user /opt /var/ /tmp /usr
          # Now 'pop' or 'scd' will take /usr first.
 
# 3. Non-destructive access with dup
push /etc
push /boot
.s         # /etc /boot
dup        # Stack: /etc /boot /boot
scd        # Changes to /boot, removes top /boot
.s         # /etc /boot (original /boot remains)
 
# 4. Practical workflow: Navigating a project tree
push
push ~/project
push ~/project/src
push ~/project/docs
push ~/project/tests
.s                   # /home/user ~/project ~/project/src ~/project/docs
                     # ~/project/tests
lrot                 # Bring ~/project/src to the last element.
.s                   # /home/user ~/project ~/project/docs ~/project/tests
                     # ~/project/src
scd                  # Jump to src.
# ... work ...
cd $(pop)            # Return to tests, if scd is not setup.
 
5.3. Naming
'''''''''''
 
The dot '.' is used in Forth to print the last element on the stack. '.s' is
used to print the whole stack in Forth. Therefore we reuse this, easing life
for all the proficient Forth users.
 
scd stands for "stack -> cd".
 
If it turns out you regularly use the last element from the stack but you want
to keep the path on the stack for future use, you can create a Bash alias:
 
alias dcd='dup && scd'   # For "(stack) dup -> cd".
 
6. Shtack Pros
--------------
 
* Persistent & Shared: Unlike 'pushd' or environment variables, Shtack survives
  terminal closures and can be accessed from any terminal where Shtack is
  sourced. With a shared '.cache/shtack' directory, it can even sync navigation
  state across machines.
* Visible Stack: '.s' gives you a numbered view of the entire stack,
  eliminating the guesswork of 'pushd''s hidden history.
* Arbitrary Reordering: 'nlrot'/'nrrot' let you pick any item in the top 'n'
  positions and rotate it to the top.
* Zero Overhead: All operations are lightweight file reads/writes. No
  background daemons, no complex state management, just pure POSIX/Bash
  compatibility.
 
6.1. Cons
'''''''''
 
Shtack is intended for interactive use, not for use in scripts, possibly even
in a multi-threaded environment.
 
It uses nano second timestamps for internal storage, if used in parallel in
scripts - whatever comes out or gets overwritten is a feature.
 
Did I say, the persistance layer is not made for high performance computing
(but then maybe bash is also not the right solution).
 
Now on to the actual implementation...
 
7. timestamp
------------
 
For keys we want to use <YYMMDD_HHMMSS_NNNNNNNNN> strings without any
space.
 
cat > ./bin/timestamp <<EOT
#! /bin/sh
# Do not edit this file, as it gets automatically generated by lp.
 
date '+%Y%m%d_%H%M%S_%N'
 
# End of: timestamp
EOT
 
8. First Scripts
----------------
 
cat > ./bin/push <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
mkdir -p "$STACK_DIR"
 
# Use $1 if provided, otherwise fall back to $PWD
value="${1:-$PWD}"
 
echo "$value" > "$STACK_DIR/s_$(timestamp).csv"
 
# End of: push
EOT
 
cat > ./bin/dup <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
mkdir -p "$STACK_DIR"
 
latest=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 1)
[ -n "$latest" ] && cat "$latest" > "$STACK_DIR/s_$(timestamp).csv"
 
# End of: dup
EOT
 
cat > ./bin/.s <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
 
ls -1 ${STACK_DIR}/s_*.csv 2>/dev/null | while read file
do
  cat $file
done | nl
EOT
 
9. scd
------
 
scd changes the current directory. Therefore 'scd' must be a function itself,
because inside a script, the directory change is only valid inside the script,
and the previous path location gets restored upon exit of the shell script
process.
 
So as a user, source this file once somewhere in your .bash_profile or .bashrc
setup file.
 
cat > ./bin/scd <<EOT
# To be sourced by /bin/bash once!
# Do not edit this file, as it gets automatically generated by lp.
 
function scd {
  latest=$(ls -1 $HOME/.cache/shtack/s_*.csv 2>/dev/null | tail -n 1)
  if [ -n "$latest" ]; then
    target=$(cat "$latest")
    rm "$latest"
    cd "$target" || exit 1
  fi
}
 
# End of: scd
EOT
 
10. Drop
--------
 
Here is the `drop` script, it uses an optional parameter n, the number of stack
items to drop, which defaults to 1:
 
cat > ./bin/drop <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
 
n="${1:-1}"
# Ensure n is a positive integer; default to 1 if invalid
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n=1
 
mapfile -t files < <(ls -1 "$STACK_DIR"/s_*.csv 2>/dev/null)
total=${#files[@]}
 
# If stack is empty, nothing to drop
[ "$total" -eq 0 ] && exit 0
 
# Determine how many to actually drop (cannot exceed stack size)
drop_count=$(( n < total ? n : total ))
 
# Remove the top n files (newest ones)
for ((i=0; i<drop_count; i++)); do
    rm -f "${files[$((total - 1 - i))]}"
done
 
# End of: drop
EOT
 
The '2>/dev/null' part of 'ls' is there to prevent error messages when the
stack is empty.
We use `[ -n "$latest" ] && rm "$latest"` to safely skip deletion if no files
exist.
 
10.1. Pop
'''''''''
 
Yes, pop is very similar to drop, except it will print the value at the same
time. The value goes to standard output. If there is nothing on the stack,
there is no error message and there is no output.
 
cat > ./bin/pop <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
 
n="${1:-1}"
# Ensure n is a positive integer; default to 1 if invalid
[[ "$n" =~ ^[1-9][0-9]*$ ]] || n=1
 
mapfile -t files < <(ls -1 "$STACK_DIR"/s_*.csv 2>/dev/null)
total=${#files[@]}
 
# If stack is empty, nothing to drop
[ "$total" -eq 0 ] && exit 0
 
# Determine how many to actually drop (cannot exceed stack size)
drop_count=$(( n < total ? n : total ))
 
# Remove the top n files (newest ones)
for ((i=0; i<drop_count; i++)); do
  cat "${files[$((total - 1 - i))]}"
  rm -f "${files[$((total - 1 - i))]}"
done
 
# End of: pop
EOT
 
11. Swap
--------
 
If there are not enough files (2) to be swapped, nothing changes on the stack.
 
cat > ./bin/swap <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
STACK_DIR="$HOME/.cache/shtack"
 
latest=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 1)
before=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 2 | head -n 1)
 
[ -z "$latest" ] && exit 0
[ -z "$before" ] && exit 0
 
mv "$latest" "$STACK_DIR/temp.csv" || exit 1
mv "$before" "$latest" || exit 1
mv "$STACK_DIR/temp.csv" "$before" || exit 1
 
# End of: swap
EOT
 
12. Rotation
------------
 
The right rotation rrot script performs e.g. this operation on the stack list:
 
[A, B, C] -> [C, A, B]
 
And  the left rotation lrot script performs analoguous e.g. this operation:
 
[A, B, C] -> [B, C, A]
 
rrot  -     (top 3 items) to the right rotation.
lrot  -     (top 3 items) to the left  rotation.
arrot - all (whole stack) to the right rotation.
alrot - all (whole stack) to the left  rotation.
 
The files on the stack are oldest to newest in an order from left to right
(imaginary). rrot takes the newest 3 items/files on the stack and changes
the order, the last file becomes the third last one and the two files below
move one step over to the right.
 
The arrot file takes the top item and moves it to the bottom/left most, and all
other items move one step in the order to the right/top. It would be best if
maximum of 2 file names change in this process.
 
The lrot and alrot scripts move to the left direction. If the task and
description is unclear, ask a question instead of implementing right
away. Otherwise, I wish you a good coding time!
 
Since the stack relies on lexicographical sorting of filenames (`ls -1 ... |
tail -n 1`), changing the logical order of stack items inherently requires
changing their filenames. To guarantee correct chronological ordering without
complex timestamp arithmetic, these scripts read the affected values, remove
the old files, and write new files with fresh timestamps in the correct
order. This is robust, predictable, and aligns with your note that "How to deal
with the file names is one thing to decide."
 
cat > ./bin/rrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
 
latest=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 1)
before=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 2 | head -n 1)
third=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 3 | head -n 1)
 
[ -z "$latest" ] && exit 0
[ -z "$before" ] && exit 0
[ -z "$third"  ] && { mv "${before}" "${STACK_DIR}/s_$(timestamp).csv"; exit 0; }
 
# Read top 3 values (oldest to newest)
v1=$(cat "${third}")
v2=$(cat "${before}")
v3=$(cat "${latest}")
 
# Right rotation of top 3: v2 -> top, v3 -> 2nd, v1 -> 3rd
echo "$v3" > "${third}"
echo "$v1" > "${before}"
echo "$v2" > "${latest}"
 
# End of: rrot
EOT
 
cat > ./bin/lrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
 
latest=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 1)
before=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 2 | head -n 1)
third=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | tail -n 3 | head -n 1)
 
[ -z "$latest" ] && exit 0
[ -z "$before" ] && exit 0
[ -z "$third"  ] && { mv "${before}" "${STACK_DIR}/s_$(timestamp).csv"; exit $?; }
 
# Read top 3 values (oldest to newest)
v1=$(cat "${third}")
v2=$(cat "${before}")
v3=$(cat "${latest}")
 
# Right rotation of top 3: v2 -> top, v3 -> 2nd, v1 -> 3rd
echo "$v2" > "${third}"
echo "$v3" > "${before}"
echo "$v1" > "${latest}"
 
# End of: lrot
EOT
 
cat > ./bin/alrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
mkdir -p "$STACK_DIR"
 
first=$(ls -1 $STACK_DIR/s_*.csv 2>/dev/null | head -n 1)
[ -z "$first" ] && exit 0
# The -i in the mv is just in case and for curiousity.
mv -i "${first}" "${STACK_DIR}/s_$(timestamp).csv"
 
# End of: alrot
EOT
 
cat > ./bin/arrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
 
mapfile -t files < <(ls -1 "$STACK_DIR"/s_*.csv 2>/dev/null)
n=${#files[@]}
[ "$n" -eq 2 ] && { mv -i "${files[0]}" "${STACK_DIR}/s_$(timestamp).csv"; exit $?; }
 
# Read all values.
vals=()
for f in "${files[@]}"; do
    vals+=("$(cat "$f")")
done
 
# Right rotation of whole stack: move top (last) to bottom (first)
# Order: vals[1..n-1], vals[0]
for ((i=0; i<n-1; i++)); do
  j=$(expr $i + 1)
  #echo $j
  echo "${vals[$i]}" > "${files[$j]}"
done
#echo $j
echo "${vals[$j]}" > "${files[0]}"
 
# End of: arrot
EOT
 
12.1. nrrot and nlrot
'''''''''''''''''''''
 
While we are at it, here are two more rotation scripts:
 
nrrot <n>
nlrot <n>
 
Both take a parameter n, the number of the last right most stack items that are
to be rotated. The script then rotates like alrot and arrot the n last right
most items. alrot and arrot rotate all items on the stack, but these two new n
rotate scripts use n items instead. If the user omitted the n parameter, we do
the same as the 'a' scripts and rotate all stack items!
 
cat > ./bin/nrrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
 
mapfile -t files < <(ls -1 "$STACK_DIR"/s_*.csv 2>/dev/null)
N=${#files[@]}
[ "$N" -eq 0 ] && exit 0
[ "$N" -eq 1 ] && exit 0
n="${1:-}"
# If n is omitted or larger than stack size, rotate all
if [ -z "$n" ] || [ "$n" -ge "$N" ]
then
  n=$N
fi
 
# Rotation of 0 or 1 item does nothing
[ "$n" -lt 2 ] && exit 0
 
#echo "n=${n}"
#echo "N=${N}"
#echo "${files[@]}"
index=$(expr $N '-' $n)
#echo "index=${index}"
[ "$n" -eq 2 ] && { mv -i "${files[$index]}" "${STACK_DIR}/s_$(timestamp).csv"; exit $?; }
 
start=$((N - n))
 
# Read values of the top n items
seg=()
for ((i=start; i<N; i++)); do
    seg+=("$(cat "${files[$i]}")")
done
#echo "${seg[@]}"
last="${seg[$((n-1))]}"
#echo "${last}"
seg2=()
seg2+=("${last}")
for ((i=0; i<n-1; i++)); do
    seg2+=("${seg[$i]}")
done
#echo "${seg2[@]}"
 
for ((i=0; i<n; i++)); do
    echo "${seg2[$i]}" > "${files[$((start+i))]}"
done
 
# End of: nrrot
EOT
 
cat > ./bin/nlrot <<EOT
#! /bin/bash
# Do not edit this file, as it gets automatically generated by lp.
 
set -e
 
STACK_DIR="$HOME/.cache/shtack"
 
n="${1:-}"
mapfile -t files < <(ls -1 "$STACK_DIR"/s_*.csv 2>/dev/null)
N=${#files[@]}
 
[ "$N" -eq 0 ] && exit 0
[ "$N" -eq 1 ] && exit 0
 
# If n is omitted or larger than stack size, rotate all
if [ -z "$n" ] || [ "$n" -ge "$N" ]; then
    n=$N
fi
 
# Rotation of 0 or 1 item does nothing
[ "$n" -lt 2 ] && exit 0
 
start=$((N - n))
 
# Read values of the top n items
seg=()
for ((i=start; i<N; i++)); do
    seg+=("$(cat "${files[$i]}")")
done
 
# Left rotation: first element moves to the end
first="${seg[0]}"
for ((i=0; i<n-1; i++)); do
    seg[$i]="${seg[$((i+1))]}"
done
seg[$((n-1))]="$first"
#echo "${seg[@]}"
 
# Write back with fresh timestamps
for ((i=0; i<n; i++)); do
    echo "${seg[$i]}" > "${files[$((start+i))]}"
done
 
# End of: nlrot
EOT
 
13. Release History
-------------------
 
Version 1 | 2026-09-24 13:18:15 | timestamp, push, dup, .s, scd, drop, pop,
                                  swap, rrot, lrot, alrot, arrot, nrrot, nlrot