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"
#!/usr/bin/env bash: why would you want#!/usr/bin/env nodeor#!/usr/bin/env zx?- The single quotes around the HEREDOC
EOFmarker are necessary to avoid the backticks being expanded. - You can choose any string you'd like to delimit the start and end of the HEREDOC. EOF or END_OF_FILE are both common choices.
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.
- Creating
say_teeas a function in your~/.zshrcwon't work becausexargswon't be able toexecit! You'll get an error saying "No such file or directory" - /tmp will normally be cleared on reboot, so you'll need to store your
voice_indexsomewhere else if you want this to persist longer
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:
- I'd always meant to look into why
xargs my_zshrc_defined_functiondidn't work. It's something I've run into before, but had never had the chance to look it up. - I'd assumed that
gsed -r /pattern/dwould work similarly togsed -r s|pattern|replacement|where the delimiter will be any character follows thes. For deletes, you must use/. - I'm now marginally more comfortable with HEREDOCs. I won't ever use a HEREDOC in real code, but being more comfortable reading them is nice.
- I tried pasting some of this code into my shell to test it out, and it failed because
command not found: #. I hadn't realized thatsetopt INTERACTIVE_COMMENTSis needed for comments in an interactive shell. (I haven't customized my ~/.zshrc on the computer I'm writing this on yet, so I'm re-learning some of my own default settings)
Hopefully you encountered at least one thing you didn't know!