This is part 4 of a five-part series:
- Part One has examples for common daily tasks in Git.
- Part Two, similar examples for Rails.
- Part Three, miscellaneous cases.
- Part Four dissects an example of a failed attempt at a useful script.
- Part Five concludes with a brief discussion of when to use Bash as opposed to some other scripting language.
TL;DR for Part 4:
- Use comments (and proofread them!)
- Be mindful of potential future growth of files or directories you might be searching or
grep
ing - Get a substring:
substr="${string:int_start_pos:int_length}"
e.g.${string:0:1}
awk
‘s regex matching is super useful and can make one-liners simpler
Cadaver Dissection
The Anatomy Lesson of Dr. Nicolaes Tulp, Rembrandt, 1632
log-cleanup
This script is useless for a number of reasons:
- it takes too long to run
- it doesn’t show the correct output
- it’s all or nothing so you’d almost never say “yes” when prompted to complete the task
However, it does show some ideas about how to incorporate functions and other nifty tools into your bash scripts, as well as some good examples of mistakes and bad ideas.
#!/bin/bash function yes_no { string="$1" first_letter="${string:0:1}" if [[ "$first_letter" != "y" && "$first_letter" != "Y" ]]; then # default no nothing return 1 fi return 0 }
The “default no nothing"
comment on line 8 makes no sense. It’s probably supposed to be “default do nothing” but that barely makes sense. If the string argument’s first character is not 'y'
or 'Y'
, the function returns 1
(not a boolean true
, but an exit status 1
meaning an error condition or, in this case, a negative response; where exit status 0
is no errors or, in this case, an affirmative response). The comment should probably be something like “exit status zero for affirmative response”.
Because yes_no
exits with a standard 0
status code for “all good” and a non-zero status for the opposite, one can do if yes_no
or if ! yes_no
, which is nice.
The interesting part here is ${string:0:1}
to get a substring (the 0
is start position and the 1
is substring length) — that’s handy to know about.
function list_logs { find ~/dev -name '*.log' -or -name '*_log' -or -regex '.*\.log\.[0-9]*'; }
list_logs
is a disaster – finding anything that looks like a log in my dev
directory takes ages. This should probably be used on a per-project basis rather than everything.
The modification time on this suggests I wrote it very shortly after joining Beezwax a couple years ago, and at the time I was on very few projects and looking in all of ~/dev
was no big deal. Obviously terrible future-proofing.
function logs_size { size="$(while read -r file; do du -chs "$file"; done < <(list_logs) | tail -1 | awk '{print $1}')"; if [[ -z "$size" ]]; then echo "0b" else echo "$size" fi }
Do you see the bug here?
Line 18 calls list_logs
(which wraps that crazy find
of ~/dev
) and reads its output in a while
loop that does a du
(disk usage; -c
to include a grand total (not very useful when it’s getting things on a per-file basis), -h
for human readable, and -s
which is only useful when there are multiple paths as arguments).
The output from the loop gets piped to tail
where -1
gives us only the last line, and the awk
prints the first column, which is just the size. It looks like there’s a (wrong) assumption that du
would magically know about its use in a loop and magically get a total of everything that happens inside the loop. Too bad there’s no such thing as magic.
Instead I should have size="$(list_logs | xargs du -ch | tail -1 | awk '{print $1}')";
which, in addition to being correct, is simpler and easier to read.
The line can get even simpler by using awk
‘s regex matching: size="$(list_logs) | xargs -du | awk '/total/{print $1}'";
matching the line I care about from du
(“82M total
“) instead of using tail
to extract it.
echo -n "logs size $(logs_size). Clean this up? [y/N] "; read -r will_cleanup; if ! yes_no "$will_cleanup"; then exit fi list_logs echo -n "delete all these? [y/N] " read -r "delete_all" if ! yes_no "$delete_all"; then exit fi while read -r file; do rm "$file"; done < <(list_logs) logs_size
As above, line 42 could be list_logs | xargs rm
.