Skip to main content
Learn how teams are building and operating AI systems at scale. Join us in Newcastle for AI in Production 2026 on 4–5 June.
items
Menu
  • About
    • Overview 
    • Join Us  
    • Community 
    • Contact 
  • Training
    • Overview 
    • Course Catalogue 
    • Public Courses 
  • Posit
    • Overview 
    • License Resale 
    • Managed Services 
    • Health Check 
  • Data Science
    • Overview 
    • Visualisation & Dashboards 
    • Open-source Data Science 
    • Data Science as a Service 
    • Gallery 
  • Engineering
    • Overview 
    • Cloud Solutions 
    • Enterprise Applications 
  • Our Work
    • Blog 
    • Case Studies 
    • R Package Validation 
    • diffify  
    • Pro Bono Support 

Ghost in the Shell Script

Author: Tim Brock

Published: May 28, 2026

tags: ai, llms, bash, unix, linux

I’ve been using Unix and Linux systems for nearly 20 years. When I found myself with a Windows laptop for a while I felt lost and quickly installed Windows Subsystem for Linux. And yet the number of commands I know and use regularly is surprisingly short: cd, ls, rm, pwd, mkdir, source, node, npm and, of course, git. Even then I don’t think I really “know” git.

In his excellent book “Don’t Make Me Think Revisited”, Steve Krug describes how

Faced with any sort of technology, very few people take the time to read instructions. Instead, we forge ahead and muddle through, making up our own vaguely plausible stories about what we’re doing and why it works.

Krug’s book is about web and mobile usability but the point he makes here applies across technological domains. And it is safe to say I’ve been “muddling through” with Unix. If you asked me whether something was a Unix command or part of the shell I’d probably just shrug and say something like “I… I don’t know that!?".

Until recently StackOverflow had always been my crutch to fall back on. From the frequency with which it appears in answers, I know that find is a very useful and versatile command. However, the odds of me remembering how to use it without looking it up are indistinguishable from 0.

But now we have alternatives in the form of various large language models or, to be more precise, multi-modal AI Assistants (like Claude and ChatGPT) and Agents (like Claude Code and Codex). I’d describe myself as AI-agnostic: I see benefits, I worry about issues. So it was with some (I’d say healthy) degree of scepticism I started using Claude to assist me in writing some shell functions to automate tasks I do or would like to do regularly.

I say “I” wrote. Reality is of course that I asked Claude to write and rewrite things for me and then I questioned why it did what it did. This probably makes it sound like I was assessing Claude for a job but it’s actually that I want to understand what has been written and, as we’ve already covered, I am not very good at this stuff.

Task One: fixing a common git mistake

One of my most common slip-ups with git is to commit changes to one branch (usually main) that were meant to go on a new feature branch. I know a way to recover from this: create the new branch I meant to create all along; do an interactive rebase to remove the commit from main; switch to the new feature branch I just created. There’s not that many steps to the process but it’s still a bit of a faff. Let’s ask Claude:

Tim
If you git [commit] to a wrong branch, is there a quick way to move the change to a new branch and remove it from the last commit from the current branch?
Claude

Yes — assuming you haven’t pushed yet:

# Create and switch to the correct branch from current state
git switch -c correct-branch

# Go back to the wrong branch and undo the commit (keeps changes staged)
git switch wrong-branch
git reset --soft HEAD~1

# Discard the changes
git restore --staged .
git restore .

Or if you want to keep it even simpler — if the commit is only on the wrong branch and not pushed:

git switch -c correct-branch
git switch wrong-branch
git reset --hard HEAD~1

The switch -c creates the new branch pointing at the same commit, so your work is preserved there. Then you just strip it off the wrong branch with reset –hard.

Here we hit a couple of issues.

  1. Neither of Claude’s suggestions did the final step of what I wanted: moving me back to the new, correct, branch. This is entirely on me. I forgot to ask (and didn’t notice for a bit). I do that kind of thing quite a lot.
  2. Even for simple tasks, Claude’s responses can be quite verbose.

The problem here is not a practical one regarding the use of Claude or an alternative agent, the real problem is it’s quite difficult to write a blog post of a sensible length that actually goes anywhere while accurately recounting the conversation you have with Claude. So what follows is a paraphrase rather than a transcript; shortened for the sake of your sanity and mine.

Tim
Can I use git switch - instead?
Claude
Yes, and it’s cleaner. git switch - saves you typing the wrong branch name out explicitly.
Tim
Seems like the sort of thing I should make into a Bash function
Claude

Agreed. Something like this (with an added safety check):

git-move-last() {
  if [ -z "$1" ]; then
    echo "Usage: git-move-last <new-branch-name>"
    return 1
  fi
  git switch -c "$1"
  git switch -
  git reset --hard HEAD~1
}
Tim
I’ve gone for git-behead. If I wanted to move to the new branch at the end I just add another git switch -?
Claude

Ha 🪓.

Exactly:

git-behead() {
  if [ -z "$1" ]; then
    echo "Usage: git-behead <new-branch-name>"
    return 1
  fi
  git switch -c "$1"
  git switch -
  git reset --hard HEAD~1
  git switch -
}
Tim
Ok, one more idea. Suppose I haven’t been looking what I’m doing and I want to remove more than just the last commit to a new branch. Can we recreate the above but with an optional commit count that defaults to 1?
Claude
git-behead() {
  if [ -z "$1" ]; then
    echo "Usage: git-behead <new-branch-name> [commit-count]"
    return 1
  fi
  local count="${2:-1}"
  git switch -c "$1"
  git switch -
  git reset --hard HEAD~"$count"
  git switch -
}
Tim
Could you comment anything you think isn’t self-explanatory?
Claude
git-behead() {
  if [ -z "$1" ]; then
    echo "Usage: git-behead <new-branch-name> [commit-count]"
    return 1
  fi
  local count="${2:-1}"  # default to 1 commit if not specified
  git switch -c "$1"     # create new branch at current HEAD
  git switch -           # switch back to original branch
  git reset --hard HEAD~"$count"  # remove commits from original branch
  git switch -           # switch to new branch
}

Putting this in my .bashrc file gives me a simple command that I can invoke whenever I make the mistake I regularly make and an optional argument to use whenever I haven’t been paying attention to the branch I’m on for an extended period. It’s also short enough that I think I follow every line, despite my limited knowledge of Bash, and there are comments there for when future me has forgotten. This conversation took about fifteen minutes and included more questions than I’ve included here, plus time I spent running various ad-hoc tests with the different iterations of the function. That seems like a pretty efficient use of my time.

Task Two: listing image files

I do a lot of data visualisation, I look after the artwork for this website and I take photographs for our conferences. In short, I deal with a lot of images in various formats. It would be nice to be able to list only images and their key properties in a specific location with a simple function. Let’s ask Claude:

Tim
Can you write me a Bash script for displaying a list of images in a directory that shows file name, width, height and size (in kB) and nothing else?
Claude

Here’s a clean script using identify from ImageMagick (widely available) with a du fallback for file size:

#!/usr/bin/env bash

DIR="${1:-.}"

if ! command -v identify &>/dev/null; then
  echo "Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)" >&2
  exit 1
fi

shopt -s nullglob nocaseglob
files=("$DIR"/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif})
shopt -u nullglob nocaseglob

if [[ ${#files[@]} -eq 0 ]]; then
  echo "No images found in: $DIR" >&2
  exit 0
fi

printf "%-40s %8s %8s %10s\n" "FILENAME" "WIDTH" "HEIGHT" "SIZE (kB)"
printf "%-40s %8s %8s %10s\n" "--------" "-----" "------" "---------"

for f in "${files[@]}"; do
  name="$(basename "$f")"
  read -r width height < <(identify -format "%w %h" "$f" 2>/dev/null | head -1)
  size_kb=$(awk "BEGIN { printf \"%.1f\", $(wc -c < "$f") / 1024 }")
  printf "%-40s %8s %8s %10s\n" "$name" "$width" "$height" "$size_kb"
done

Supported formats: jpg, jpeg, png, gif, webp, bmp, tiff, avif.

Example output:

FILENAME                                    WIDTH   HEIGHT  SIZE (kB)
--------                                    -----   ------  ---------
banner.png                                   1200      630      84.3
avatar.jpg                                    400      400      32.1
hero.webp                                    1920     1080     210.7

I asked for a script, I got a script. I wanted a function. This one is, again, on me.

Tim
Sorry, can we make this a function rather than a script?
Claude

The key changes: shebang removed, exit replaced with return, and variables are now local. To use it, source it in your shell and call it as normal:

imgls() {
  local dir="${1:-.}"

  if ! command -v identify &>/dev/null; then
    echo "Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)" >&2
    return 1
  fi

  shopt -s nullglob nocaseglob
  local files=("$dir"/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif})
  shopt -u nullglob nocaseglob

  if [[ ${#files[@]} -eq 0 ]]; then
    echo "No images found in: $dir" >&2
    return 0
  fi

  printf "%-40s %8s %8s %10s\n" "FILENAME" "WIDTH" "HEIGHT" "SIZE (kB)"
  printf "%-40s %8s %8s %10s\n" "--------" "-----" "------" "---------"

  local f name width height size_kb
  for f in "${files[@]}"; do
    name="$(basename "$f")"
    read -r width height < <(identify -format "%w %h" "$f" 2>/dev/null | head -1)
    size_kb=$(awk "BEGIN { printf \"%.1f\", $(wc -c < "$f") / 1024 }")
    printf "%-40s %8s %8s %10s\n" "$name" "$width" "$height" "$size_kb"
  done
}

The conversation goes on a bit from here and the function becomes more and more detailed. It would be tedious to show the whole conversation but to summarise, “we”:

  1. Add support for SVG, including extracting dimensions from the viewBox attribute.
  2. Fix a bug with GIF dimensions.
  3. Tweak the header text.
  4. Add support for recursive searching of a directory.
  5. Fix the layout when doing a recursive search.
  6. Round the values in the SIZE column
  7. Add comments.

The final result:

imgls() {
  local recursive=0

  # Parse flags — currently only -r (recursive) is supported
  while getopts ":r" opt; do
    case $opt in
      r) recursive=1 ;;
      *) echo "Usage: imgls [-r] [directory]" >&2; return 1 ;;
    esac
  done
  # Shift past any parsed flags so $1 is the directory argument
  shift $((OPTIND - 1))
  OPTIND=1

  # Use first argument as directory, default to current directory if omitted
  local dir="${1:-.}"

  # identify is provided by imagemagick and used to read raster image dimensions
  if ! command -v identify &>/dev/null; then
    echo "Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)" >&2
    return 1
  fi

  local exts=("jpg" "jpeg" "png" "gif" "webp" "bmp" "tiff" "tif" "avif" "svg")
  local files=()

  if [[ $recursive -eq 1 ]]; then
    # Build a find command matching all extensions, case-insensitively
    local find_args=("$dir" "-type" "f" "(")
    for i in "${!exts[@]}"; do
      [[ $i -gt 0 ]] && find_args+=("-o")
      find_args+=("-iname" "*.${exts[$i]}")
    done
    find_args+=(")")
    while IFS= read -r f; do
      files+=("$f")
    done < <(find "${find_args[@]}" | sort)
  else
    # nullglob: unmatched globs expand to nothing instead of being treated as literals
    # nocaseglob: match extensions case-insensitively (e.g. .JPG as well as .jpg)
    shopt -s nullglob nocaseglob
    files=("$dir"/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif,svg})
    shopt -u nullglob nocaseglob
  fi

  if [[ ${#files[@]} -eq 0 ]]; then
    echo "No images found in: $dir" >&2
    return 0
  fi

  # In recursive mode show the relative path, otherwise just the filename
  printf "%-40s %11s %12s %10s\n" "FILENAME" "WIDTH (px)" "HEIGHT (px)" "SIZE (kB)"
  printf "%-40s %11s %12s %10s\n" "--------" "----------" "-----------" "---------"

  local f name width height size_kb viewbox current_subdir="" subdir
  for f in "${files[@]}"; do
    if [[ $recursive -eq 1 ]]; then
      # Print a header line when we enter a new subdirectory
      subdir="$(dirname "${f#$dir/}")"
      if [[ "$subdir" != "$current_subdir" ]]; then
        [[ -n "$current_subdir" ]] && echo ""
        echo "${subdir}/"
        current_subdir="$subdir"
      fi
      name="$(basename "$f")"
    else
      name="$(basename "$f")"
    fi
    # wc -c counts raw bytes; awk converts to kB with one decimal place
    size_kb=$(awk "BEGIN { printf \"%d\", $(wc -c < "$f") / 1024 }")

    if [[ "${f,,}" == *.svg ]]; then
      # SVGs are XML so we can parse dimensions directly without imagemagick.
      # Prefer viewBox (e.g. "0 0 100 200") as it's the most reliable source
      # of the intended coordinate space; fall back to width/height attributes.
      viewbox=$(grep -oi 'viewBox="[^"]*"' "$f" 2>/dev/null | head -1 | grep -oi '"[^"]*"' | tr -d '"')
      if [[ -n "$viewbox" ]]; then
        # viewBox is "min-x min-y width height" — fields 3 and 4
        width=$(echo "$viewbox" | awk '{printf "%g", $3}')
        height=$(echo "$viewbox" | awk '{printf "%g", $4}')
      else
        width=$(grep -oi 'width="[^"]*"' "$f" 2>/dev/null | head -1 | grep -oi '"[^"]*"' | tr -d '"px"')
        height=$(grep -oi 'height="[^"]*"' "$f" 2>/dev/null | head -1 | grep -oi '"[^"]*"' | tr -d '"px"')
      fi
      # ${var:--} expands to "-" if var is empty or unset
      width="${width:--}"
      height="${height:--}"
    else
      # [0] selects only the first frame, preventing animated GIFs from
      # producing one line of output per frame
      read -r width height < <(identify -format "%w %h" "${f}[0]" 2>/dev/null)
    fi

    printf "%-40s %11s %12s %10s\n" "$name" "$width" "$height" "$size_kb"
  done
}

The “development time” here was longer and I’d be fibbing if I said I understood every line of this function. But I do get the general principles and understand what it can and can’t do. And being able to list all images (of nearly all relevant types) in a directory with just their key properties in a nice tabular format is a very nice productivity win for me.

Final Thoughts

Discussions about AI easily tend towards the extremes: ‘it’s amazing’, ‘it’s a disaster’, ‘it’s useless’. This is particularly unhelpful when focusing on how successfully or otherwise it executes a single task. All we’ve really done in this post is create a couple of Bash scripts. For me the AI in question, Claude, has been useful. No more, no less. Despite this, the process did make me think a lot about how the way we interact with machines is changing. While it’s easy to over-personify AI, this line from the film that gave this blog post its title seems now, in 2026, to have been prescient:

The advent of computers, and the subsequent accumulation of incalculable data has given rise to a new system of memory and thought parallel to your own.

— Puppet Master, Ghost in the Shell (1995)

Jumping Rivers Logo

Recent Posts

  • Ghost in the Shell Script 
  • Online Data Science Training Courses: R, Python, and Machine Learning in 2026 
  • What's new in R 4.6.0? 
  • Programming with LLMs in R & Python 
  • Using R to Teach R: Lessons for Software Development 
  • AI in Production 2026: Sponsors 
  • Why Learning R is a Good Career Move in 2026 
  • Reproducible Analytical Pipelines 
  • Three Posit Platform Features Worth Knowing About 
  • Is Your Dashboard User Friendly? 

Top Tags

  • R (254) 
  • Rbloggers (196) 
  • Python (98) 
  • Pybloggers (96) 
  • Shiny (65) 
  • Events (29) 
  • Machine-Learning (29) 
  • Training (29) 
  • Conferences (21) 
  • Tidyverse (17) 
  • Statistics (16) 
  • Packages (14) 

Keep Updated

Like data science? R? Python? Stan? Then you’ll love the Jumping Rivers newsletter. The perks of being part of the Jumping Rivers family are:

  • Be the first to know about our latest courses and conferences.
  • Get discounts on the latest courses.
  • Read news on the latest techniques with the Jumping Rivers blog.

We keep your data secure and will never share your details. By subscribing, you agree to our privacy policy.

Follow Us

  • GitHub
  • Bluesky
  • LinkedIn
  • YouTube
  • Eventbrite

Find Us

The Catalyst Newcastle Helix Newcastle, NE4 5TG
Get directions

Contact Us

  • hello@jumpingrivers.com
  • + 44(0) 191 432 4340

Newsletter

Sign up

Events

  • North East Data Scientists Meetup
  • Leeds Data Science Meetup
  • AI in Production
British Assessment Bureau, UKAS Certified logo for ISO 9001 - Quality management British Assessment Bureau, UKAS Certified logo for ISO 27001 - Information security management Cyber Essentials Certified Plus badge
  • Privacy Notice
  • |
  • Booking Terms

©2016 - present. Jumping Rivers Ltd