Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6 Techniques I Use to Create a Great User Experience for Shell Scripts #932

Open
1 task
ShellLM opened this issue Nov 4, 2024 · 1 comment
Open
1 task
Labels
CLI-UX Command Line Interface user experience and best practices shell-script shell scripting in Bash, ZSH, POSIX etc shell-tools Tools and utilities for shell scripting and command line operations software-engineering Best practice for software engineering

Comments

@ShellLM
Copy link
Collaborator

ShellLM commented Nov 4, 2024

6 Techniques I Use to Create a Great User Experience for Shell Scripts

Snippet

"Skip to content
Home
Blog
Jason on Twitter
Jason on GitHub
Toggle dark mode
6 Techniques I Use to Create a Great User Experience for Shell Scripts
Sep 11, 2024
"You should go and check out the shell script in the repo because it's very nice. It has colored output, it's super safe... it's really a masterclass in terms of writing shell scripts."

Thank you Gunnar Morling for the shout-out! 😊

In January 2024, I along with a few dozen performance-minded geeks got nerd-sniped into participating in Gunnar's One Billion Row Challenge 1️⃣🐝🏎️ ."

Content

In January 2024, I along with a few dozen performance-minded geeks got nerd-sniped into participating in Gunnar's One Billion Row Challenge 1️⃣🐝🏎️.

Gunnar quickly became overwhelmed being the (unpaid) evaluator of a constant stampede of entries. I jumped in to help him automate the evaluation steps with a shell script and received the above testimonial from Gunnar at his Javazone talk (check it out to hear all about the performance techniques used in the challenge: "# 1BRC–Nerd Sniping the Java Community - Gunnar Morling").

Here are 6 techniques I used in the #1BRC shell script to make it robust, safe and fun for Gunnar to use:

  1. Comprehensive Error Handling and Input Validation

I believe that clear error messages are crucial for a good user experience. That's why I implemented thorough error handling and input validation throughout the script. For example:

if [ -z "$1" ] 
  then 
    echo "Usage: evaluate.sh <fork name> (<fork name 2> ...)" 
    echo " for each fork, there must be a 'calculate_average_<fork name>.sh' script and an optional 'prepare_<fork name>.sh'." 
    exit 1 
fi

This approach helps users quickly identify and resolve issues, saving them time and frustration.

  1. Clear and Colorful Output

To make the script's output more readable and user-friendly, I used ANSI color codes to highlight important information, warnings, and errors. For instance:

BOLD_RED='\033[1;31m'
RESET='\033[0m'
echo -e "${BOLD_RED}ERROR${RESET}: ./calculate_average_$fork.sh does not exist." >&2

This visual distinction helps users quickly grasp the nature of each message.

  1. Detailed Progress Reporting

I wanted users to understand exactly what the script was doing at each step. To achieve this, I implemented a function that prints each command before executing it:

function print_and_execute() {
  echo "+ $@" >&2 
  "$@" 
}

This matches the output format of Bash's builtin set -x tracing, but gives the script author more granular control of what is printed.

This level of transparency not only keeps users informed but also aids in debugging if something goes wrong.

  1. Strategic Error Handling with "set -e" and "set +e"

I wanted to ensure that the script would exit immediately if there was an error in the script itself, but also allow it to continue running if individual forks encountered issues. To achieve this, I used the Bash options set -e and set +e strategically throughout the script. Here's how I implemented this technique:

# At the beginning of the script
set -eo pipefail

# Before running tests and benchmarks for each fork
for fork in "$@"; do
  set +e # we don't want prepare.sh, test.sh or hyperfine failing on 1 fork to exit the script early

  # Run prepare script (simplified)
  print_and_execute source "./prepare_$fork.sh"

  # Run the test suite (simplified)
  print_and_execute $TIMEOUT ./test.sh $fork

  # ... (other fork-specific operations)
done
set -e  # Re-enable exit on error after the fork-specific operations

This approach gives the script author fine-grained control over which errors cause the script to exit and which can be handled in other ways.

  1. Platform-Specific Adaptations

Knowing that users might run this script on different operating systems, I added logic to detect the OS and adjust the script's behavior accordingly:

if [ "$(uname -s)" == "Linux" ]; then 
  TIMEOUT="timeout -v $RUN_TIME_LIMIT" 
else # Assume MacOS 
  if [ -x "$(command -v gtimeout)" ]; then 
    TIMEOUT="gtimeout -v $RUN_TIME_LIMIT"
  else 
    echo -e "${BOLD_YELLOW}WARNING${RESET} gtimeout not available, install with \`brew install coreutils\` or benchmark runs may take indefinitely long." 
  fi
fi

This ensures a consistent experience across different environments. Many #1BRC participants were developing on MacOS while the evaluation machine ran linux for example.

  1. Timestamped File Outputs for Multiple Runs

To support multiple benchmark runs without overwriting previous results, I implemented a system of timestamped file outputs. This allows users to run the script multiple times and keep a historical record of all results. Here's how I did it:

filetimestamp=$(date +"%Y%m%d%H%M%S")

# ... (in the loop for each fork)
HYPERFINE_OPTS="--warmup 0 --runs $RUNS --export-json $fork-$filetimestamp-timing.json --output ./$fork-$filetimestamp.out"

# ... (after the benchmarks)
echo "Raw results saved to file(s):"
for fork in "$@"; do
  if [ -f "$fork-$filetimestamp-timing.json" ]; then
      cat $fork-$filetimestamp-timing.json >> $fork-$filetimestamp.out
      rm $fork-$filetimestamp-timing.json
  fi

  if [ -f "$fork-$filetimestamp.out" ]; then
    echo "  $fork-$filetimestamp.out"
  fi
done

By implementing these techniques, I aimed to create a user-friendly, informative, and robust shell script that provides a great experience for users running and analyzing benchmarks. I hope these ideas inspire you to enhance the user experience in your own shell scripts!

I'd love to hear your thoughts on these shell scripting techniques or any other tips you have for creating great user experiences in scripts. Feel free to join the discussion:

Suggested labels

None

@ShellLM ShellLM added CLI-UX Command Line Interface user experience and best practices shell-script shell scripting in Bash, ZSH, POSIX etc shell-tools Tools and utilities for shell scripting and command line operations software-engineering Best practice for software engineering labels Nov 4, 2024
@ShellLM
Copy link
Collaborator Author

ShellLM commented Nov 4, 2024

Related content

#741 similarity score: 0.89
#924 similarity score: 0.88
#743 similarity score: 0.88
#6 similarity score: 0.87
#746 similarity score: 0.86
#114 similarity score: 0.86

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLI-UX Command Line Interface user experience and best practices shell-script shell scripting in Bash, ZSH, POSIX etc shell-tools Tools and utilities for shell scripting and command line operations software-engineering Best practice for software engineering
Projects
None yet
Development

No branches or pull requests

1 participant