Useless shell commands

Fixing echo

When there's an echo, you only hear the last few syllables of the sound. This makes the unix echo incredibly confusing! Rather than doing the natural, useful thing and repeating the last "sound" of the previous command, it instead shouts out the arguments that you pass to it. Thankfully, it's simple to update your shell to fix this problem:

true_echo () {
  history | `# get all previous commands` \
  tail -n 1 | `# choose only the most recent one` \
  awk '{ print $NF }' `# prints last field. $NF = number of fields. awk is 1-indexed, and $0 refers to the full line`
}
shout () {
  # we overrode `echo`, but we can still reference it by using `builtin X` # this technique can be useful if you want to extend a command like `cd` to do extra things!
  builtin echo $@
}

alias echo=true_echo
alias shout=shout
$ echo hello world
world

Writing shell scripts with HEREDOCs and file redirection

One of my favorite parts of working in the shell is the ability to customize it with my own shell scripts by creating a file /usr/local/bin and chmod +x it to make it runnable. One problem I run into pretty often is that I want to write these scripts with Node or Python rather than bash, but HEREDOCs can make it ergonomic to work around that problem:

#!/usr/bin/env bash

script=$(
cat <<'EOF'
const { execSync } = require("child_process");

execSync(`echo "hello, world"`, { stdio: "inherit" });
EOF
)
node -e $script # prints "hello, world"

If you've forgotten the right option to use with your programming language to provide a string script, the easiest way to work around this—far easier than searching online or using --help—is to use file redirection with a HEREDOC:

node <(cat <<'EOF'
console.log("hello, world");
EOF
)

<() tells the shell to treat the output of your command as if it were a file. file <(echo "hello") will show you a file descriptor like /dev/fd/11: fifo (named pipe). Some files only take a file path as an input, and in those cases it can sometimes be slightly more ergonomic to use <() as an input rather than creating a temporary file.

Using cowsay on stderr

cowsay (brew install cowsay) is a commonly used tool to help process logs, but it's important to note that it only highlights information from stdout. Many processes will pipe their informational logging to stderr, so if you want to use cowsay with a tool like that, you'll need to redirect stderr (2) to stdout (1): 2>&1. Example—brew install sl 2>&1 | cowsay.

You might also need to redirect stderr to stdout if you're doing something like grepping for an error message from a test or script.

Making say behave more like tee

One of the problems I run into most often with say is that it doesn't behave like tee and continue piping its output after it's finished reading a line. This keeps me from using it in the middle of pipelines. Thankfully, it's easy to add a function to your shell that narrates your pipeline. Create a file at /usr/local/bin/say_tee with the following:

#!/usr/bin/env zsh

set -e

# we don't want the same voice every time!
# so we need to persist the voice we used last somewhere
if [[ ! -f /tmp/voice_index ]]; then
  echo -1 > /tmp/voice_index
fi
function choose_voice () {
  local voice_index=$(cat /tmp/voice_index)
  local voice_list=$(say -v'?' | gsed -rn 's|^(.*)\s+en_US.*|\1|p' | gsed -r '/\s+$/d')
  local voice_count=$(echo $voice_list | wc -l)
  voice_index=$(( (voice_index + 1) % voice_count ))
  echo $voice_index > /tmp/voice_index

  # zsh and bash differ in how they set up arrays
  # so I'm choosing the voice in a silly way that should work for both 
  # ...and if do you ever need to use arrays in bash, I'm sorry
  echo $voice_list | awk "NR == $(( voice_index + 1))"
}

function say_tee () {
  say -v "$(choose_voice)" "$@"
  echo "$@"
}

say_tee "$@"

Usage will look like tail -n3 /usr/share/dict/words | xargs -n1 say_tee. It will read out "zythum, Zyzomys, Zyzzogeton" and output each word to the command line after it finishes reading it.

Are these commands truly useless?

Yes.

But playing around with things is how you learn. I personally learned a few small things while messing around for this post:

Hopefully you encountered at least one thing you didn't know!