Into a shell rabbit hole

How I got lost in a simple idea.

Disclaimer: I am addicted to using the shell for a lot of things, but most importantly working with git. I tried more than once using a graphic helper or a very well-known and used GUI for it, but eventually, I always go back to my beloved zsh.

The problem

Imagine being deep inside a project folder structure and for one reason or another, you want to change the directory to one of the files your git status -s reports as changed. You may or may not be in the correct directory already, so you have to start the cd .. cd ... cd <tab><tab> dance until you'll eventually find yourself in the right place. Since git already knows the full path of the modified files, my idea was to provide a simple CLI tool to list them, and then select where to.

I wanted to do everything in bash, so this is what I got as the "list the changed files, select one, and cd to its directory":

# Get the list of changed files as an array
files=($(git status -s | awk '{print $2}'))

# Exit when there are no changed files
if [ ${#files[@]} -eq 0 ];
then
  echo "No changes found"
  exit 0
fi

# Select a file and change to its dir
PS3="Select a file (or ^C): "
select opt in "${files[@]}"; do
  if [[ "${opt}" != "" ]]; then
    cd $(dirname ${opt}) # Here lies the issue
  fi
  break
done

All good then? Nope.

Consider the line where I tried to change the directory: cd $(dirname ${opt}). It is obvious (now!) that we are changing the working directory of the current bash session, which is the one in which the script itself is running and NOT of the one which runs the script. Exiting the script will send us back to the previous session and the working directory will be the original one. Nop.

There are a couple of "solutions" here: one is sourcing the script instead of running it; this way the script will run from within the current session, extending it - if you will. This is the way you add environment variables or define functions. To source the script one runs source ./script.sh or more concisely . ./script.sh. Another solution is to write a function, instead of a stand-alone script, to be installed at the start of each bash session, so your script will be actually part of the current session.

Neither of the two options really fitted my use case and my goal, to have an "external" command to just do that.

I wrote a small script in Go instead of bash, doing the same but - obviously again - I faced the same problem: when running a program, bash spawns a new session and any change of the working directory fails.

I finally settled on a solution that not only makes sense, but it's also more philosophically unix-like: one tool for each job. I basically run the same script as before but now I only output the selected directory (or ., when no directory is selected)

# ...
# All input validations are left out for clarity!
PS3="Select a file (or ^C): "
select opt in "${files[@]}"; do
  if [[ "${opt}" != "" ]]; then
    echo $(dirname ${opt})
  fi
  break
done
echo .

I now just need to use the output as the parameter for cd. The first instinct was to use xargs but that's just too much overhead (and it may not work, if cd is a built-in) so the final solution is simply cd $(./script.sh). Of course, for added usability, I can now use a bash function or an alias like alias gitgo='cd $(~/bin/z.sh)' (the single quotes are important here).

Life is good again!


Comments? Head to Mastodon then https://mastodon.social/@caludio/109859613820275151