0

Question:

Should I even write a script to undo the changes done by running a script in the first place?

If no, then how do I clean up/undo the changes done to the system so that I can run the script again (with changes) in a system state that it was in before I ran the script?

If yes, then what should be the approach to write a good undo script?


Script: Link to script on github

#!bin/bash

repos_dir="$HOME/repos"
if [ ! -d "$repos_dir" ]; then
  mkdir -p "$repos_dir"
fi

git clone https://github.com/bashonregardless/dotfiles.git "$repos_dir/dotfiles"

software_dir="$HOME/software"
# Create a directory to download nvim software.
if test ! -d "$software_dir"; then
  mkdir "$software_dir"
fi

(
cd "$software_dir"
# Download Neovim on Linux
if test $? = 0 && "$(command -v curl)" &> dev/null; then
  curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim.appimage
  chmod u+x nvim.appimage
else
  echo "Error: curl is not installed"
  echo
  exit 64
fi
)

# Create "$HOME/bin" dir if it does not exist
if test ! -d "$HOME/bin"
then
    mkdir "$HOME/bin"
fi

# Create a symbolic link in "$HOME/bin" dir to "$HOME/software/nvim"
ln "$software_dir/nvim.appimage" "$HOME/bin/nvim"

# Update vimrc at "$HOME/.vim/vimrc"
[ ! -d "$HOME/.vim" ] && mkdir "$HOME/.vim"
if [ ! -e "$HOME/.vim/vimrc" ]; then
    # create a new file by copying dotfile vimrc
    cp "$repos_dir/dotfiles/vimrc.template" "$HOME/.vim/vimrc"
else
    cat "$repos_dir/dotfiles/vimrc.template" >> "$HOME/.vim/vimrc" 
fi

# Update init.vim at "$HOME/.config/nvim/" (VIMCONFIG="$HOME/.config/nvim")
[ ! -d "$HOME/.config/nvim" ] && mkdir -p "$HOME/.config/nvim"
if [ ! -e "$HOME/.config/nvim/init.vim" ]; then
    # create a new file by copying dotfile vimrc
    cp "$repos_dir/dotfiles/init.vim.template" "$HOME/.config/nvim/init.vim"
else
    cat "$repos_dir/dotfiles/init.vim.template" >> "$HOME/.config/nvim/init.vim" 
fi

# add minpac vim plugin manager (See Modern vim craft)
[ ! -d "$HOME/.config/nvim/pack/minpac/opt" ] && mkdir -p "$HOME/.config/nvim/pack/minpac/opt"
cd "$HOME/.config/nvim/pack/minpac/opt"
git clone https://github.com/k-takata/minpac.git
cd -

# Update dotfiles (create, if non-existent)
if [ ! -e "$HOME/.bash_profile" ]; then
    # Copy and rename in the same time
    cp "$repos_dir/dotfiles/bash_profile" "$HOME/.bash_profile"
else
    cat "$repos_dir/dotfiles/bash_profile" >> "$HOME/.bash_profile" 
fi

if [ ! -e "$HOME/.bashrc" ]; then
    cp "$repos_dir/dotfiles/bashrc" "$HOME/.bashrc"
else
    cat "$repos_dir/dotfiles/bashrc" >> "$HOME/.bashrc" 
fi

This script sets up a vim development environment. I wrote the script and thought that it would be enough to setup things correctly, so I ran it for the first time and it worked. Then I made some changes to the script and wanted to test if it was running correctly. When I ran it this time, I encountered a few warnings/errors/logs like:

ln: failed to create hard link '/home/user/bin/nvim': File exists

fatal: destination path '/home/user/repos/dotfiles' already exists and is not an empty > directory.

I have used a few cat file >> another-file, that appends duplicate contents to the another-file every time I run the script. This I definitely want to undo.


Changes done to the system by running the script:

  1. creating ~/repos directory.
  2. cloning dotfiles repo inside repos.
  3. cloning compSc repo inside repos.
  4. creating software dir inside home.
  5. downloading nvim inside software dir.
  6. changing the permission of the downloaded file nvim.appimage.
  7. creating bin dir inside home.
  8. creating symlink of nvim.appimage with ~/bin/nvim
  9. create ~/.vim/vimrc (mkdir -p).
  10. copy/create or append to ~/.vim/vimrc.
  11. mkdir -p "$HOME/.config/nvim/pack/minpac/opt"
  12. clone k-takata/minpac indside "$HOME/.config/nvim/pack/minpac/opt"
  13. copy/create or append to "$HOME/.bash_profile"

These are the changes that happen to my system before even sourcing the ~/.bash_profile file. Sourcing which does further changes to my system, like Updating the PATH and other environment variables, etc.

I would like to undo these changes as well.


How I have been replicating the system state that it was in before running the script ( for testing purpose )?

I use multipass to launch an Ubuntu VM for every small change. After running the script, I delete this VM and launch a new one. This gives me a machine that is in a state where I can run my script for the first time. I do this for every small change. It gets tedious soon.

3
  • 2
    Rather than trying to write an undo script, write a script to create a backup of the files/directories your script modifies and another script to restore from that backup. Run that backup script before your current script runs, restore from the backup before running the script again.
    – Ed Morton
    Commented Jul 5, 2022 at 13:14
  • @EdMorton This makes sense. A question though - why is undoing not the right approach? Is it the complexity of the undo script or is it that this simply cannot be accomplished ( in general )? Commented Jul 5, 2022 at 15:10
  • It's much harder to write an undo script that gets you back to exactly the same point.
    – Ed Morton
    Commented Jul 5, 2022 at 15:30

1 Answer 1

3

In general, you cannot reliably undo the effect of a script. For example, the snippet at the very beginning of your script:

if [ ! -d "$repos_dir" ]; then
  mkdir -p "$repos_dir"
fi

cannot be reliably undone because you cannot determine afterwards how much of the directory path leading to "$repos_dir" already existed before.

If you want your script do be cleanly undoable then you need to record the original state of everything you touch, before you touch it, so that you can restore it if requested. This is what package managers try to do, with varying success.

An alternative approach is to make your script idempotent. This means that running it twice has exactly the same effect as running it once. This is the approach of desired-state configuration management tools like Ansible or Puppet.

The snippet above is a typical example of idempotent code: if the directory "$repos_dir" already exists from a previous run then it does simply nothing.

An example of code which is not idempotent would be your cat file >> another-file which appends duplicate contents to the another-file every time the script is run. Making it idempotent would involve checking whether the content of file (or perhaps a different version thereof) is already present in another-file, and then either removing that first or abstaining from adding it again. Alternatively, you could keep a base version of another-file without the contents of file and use that for concatenating, along the lines of:

if [ ! -e another-file.base ]; then
  mv another-file another-file.base
fi
cat another-file.base file > another-file
2
  • Thanks for the answer, just wondering if cat another-file.base file > another-file is a valid syntax? Commented Jul 5, 2022 at 15:03
  • 1
    @HasloVardos yes, concatenating multiple files is exactly the job that cat exists to do, but why not just try it and see?
    – Ed Morton
    Commented Jul 5, 2022 at 15:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .