A Pretty Shell Function
Table of Contents
1 What is it?
A pretty shell function is a special case of a canonical function, which is produced by using the declare shell built-in:
$ declare -f function ...
In this exercise, I've dressed it up to handle long lines from piped commands.
This paper is part of my Commonplace Book; the text of this paper, in this chapter.
1.1 The Challenge prettyprint
Long pipe sequences in functions can quickly run over the conventional page width, about a 72-character line length. I'd thought of trying to detect the extra-long lines and reformat on the fly. Rather than focus on automated discovery, I thought it better if the developer decides where to break the long lines. This feature provides the means.
What would a resulting re-format look like?
- it would have to retain it's execution semantics
- it shouldn't be more intrusive than a badly-wrapped line,
- it should be much more readable, and to foster that,
- it might allow a tab formatting.
On this last point, I recall reading shell scripts from my days at Bell Labs, which could have lines of long pipe sequences successively indented by two spaces. Since I've been committed to a shell function format given by the un-adorned:
declare -f ...
by itself, one might say it gratuitously reformats, some pain-staking pretty printing laid out by the developer. The idea is to preserve the simple functional interface, and supply underlying functionality to break lines at programmer discretion to produce a pleasing format.
Preserving the interface, and sacrificing a small bit of performance fosters the ease of understanding by breaking long pipes into phrases.
1.2 How to Proceed command
The bash shell has a command builtin. This has allowed script
writers the means of intercepting a call to the fixed commands and
dressing up the results. The help builtin facility uses an example
with the ls command. An ls
function would look like this:
function ls () { ... command ls ... # whatever default arguments you wanted to supply ... }
The inner ls
when prefaced by the command builtin says ignore any
defined alias and function names, and proceed directly to the command
on the users PATH. Thus avoiding recursive calls.
In our case, we want to capture the full features of the declare -f
command,
and dress up the resulting output. Our function will look something like:
function declare () { : capture the declare -f command, pretty-print : case $1 in -f ) command declare $@ ... # something with the remaining arguments # to achieve the desired pretty-print ;; *) command declare $@ # all other cases behave as before ;; esac }
1.3 The insight dots
Look at all the ...
lying around. The literary equivalent of "do whatever".
Can we turn a command line like this:
a command with lots of stuff before | and after a pipe with maybe | more pipes following
Into this:
a command with lots of stuff before | ... and after a pipe with maybe | ... ... more pipes following
Yes indeed, literally, "dot, dot, dot".
What does that function look like?
function ... () { : a place-holder, do what follows; : eval $@ }
So, in the example,
... after
executes after
with its arguments, and
... ... pipes
executes
... pipes, which ...
1.4 In practice fold pipes
I spent little time thinking about writing the code to automatically detect pipes in the command line. It would present these challenges:
- What is the shortest command sequence allowed?
- the longest?
- how to handle multiple pipes on a line and their length?
- …
The simple answer, "Let the programmer decide". Should the programmer want a different sized offset, or a single level (un-tabbed) indenting, the above example might be rendered:
a command with lots of stuff before | .. and after a pipe with maybe | .. more pipes following
All that's required is a different dot-dot function, and this is the important part, she must edit the original source to look like this:
a command with lots of stuff before | ... and after a pipe with maybe | ... ... more pipes following
or, for the second result:
a command with lots of stuff before | .. and after a pipe with maybe | .. more pipes following
1.5 Now to declare declare
$ declare -f declare declare () { : insert ... syntactic sugar into shell functions; : date: 2021-01-03; case $1 in -f) command declare -f ${@:2} | sed ' s/| *\([.][. ]*\) /|\ \1 /g' ;; *) command declare $@ ;; esac }
In the event the function is not called with the -f flag, proceed as if the built-in were called directly, otherwise, handle the dotted commands.
- the
-f ${@:2}
syntax repeats the -f flag, and passes the
subsequent function name arguments ${@:2}
to the declare builtin.
This returns the canonical (including long lines) functions to the
stream editor, which
- recognizes the pipe symbol
- followed by zero or more spaces
- then a single period,
- then an optional string containing only periods or spaces,
- and a single space
The stream editor remembers: \( ... \), which is the string from the first period to just before the last space, replacing the whole thing with
- the pipe symbol,
- a newline and four spaces,
- the remembered string
- and finally, a space,
this is repeated 'globally', i.e. for each instance of the pattern on the line.
1.6 A test case declare_test
Note the functions, in particular declare_test
:
This exercise was necessary, these lines were needed ahead of the command
for the -f
argument:
: test for function names; set -- $(func_args ${@:2}); read_devtty $#, $@; [[ $# -lt 1 ]] && return 1; :; : there are some, proceed;
The need arose when I realized that some uses of declare -f
,
particularly in the test for function arguments, would fail, since
here we have to simulate the behavior before reformatting the function
text.
1.6.1 Utilities declare func_args
Including the runtime functions:
- … – a no-op
- .. – "
- declare – intercepts the -f flag
- fold_pipedots – does the reformating
- func_args – simulates "is this a function"
and the testing tools:
- declare_test
- args_test
func_args () { : use TYPE to simulate declare -f; type -a ${@:-/dev/null} 2> /dev/null | awk '$4 ~ /function/ { print $1 }' } ... () { : synctactic sugar for long pipe sequences; : date: 2021-01-03; read_devtty "$@"; eval "$@" } .. () { : another synctactic sugar for long pipe sequences; : date: 2021-01-03; eval "$@" } args_test () { : testing limitation -- may QUOTED args be REFORMATTED?; function .... () { cat - }; type -a ${@:-$(myname)} 2> /dev/null | .... awk '$4 ~ /function/ { print $1 }'; : may need to simulate this; : unset .... } fold_pipedots () { : RE folds line with PIPE followed by ... s; : the RE allows zero or more spaces after the Pipe Symbol; : .. a Dot followed by zero or more Dots or Spaces,; : .. closed by a space, which separates .s from the command; sed ' s/| *\([.][. ]*\) /|\ \1 /g' } declare () { : insert ... syntactic sugar into shell functions; : date: 2021-01-03; case $1 in -f) : test for function names; set -- $(func_args ${@:2}); read_devtty $#, $@; [[ $# -lt 1 ]] && return 1; :; : there are some, proceed; command declare -f $@ | fold_pipedots ;; *) command declare $@ ;; esac } declare_test () { : date: 2021-01-03; ignore pushd $HOME/Dropbox/git/applemcg.github.io/reading; local format="\n%s\n==============================================\n"; :; printf $format UTILITIES; declare -f func_args ... .. args_test fold_pipedots declare{,_test}; :; printf $format "BEFORE - declare builtin"; command declare -f latestbook readhistory; :; printf $format "AFTER - declare Function"; declare -f latestbook readhistory }
1.6.2 Before and After
These are best looked at together. Before shows the results of the
builtin declare
. The programer has inserted the ...
where she
wants to fold the lines
The After view shows the results of installing the declare
function
and the ...
functions.
BEFORE - declare builtin ============================================== latestbook () { : date: 2020-12-05; table_history book_page.rdb | i_timeDate latest | ... jointable -1 book -2 book - author_book.rdb | ... ... column latest page author book | tee $(myname).rdb } readhistory () { : reading history with currentbooks on top; : date: 2020-12-30; : date: 2021-01-03; zcat .hry/book_page.rdb.Z | sorttable -r | select_first book | .. ncolumn delete_time | rdb_iDate | rename i_date latest | .. column latest page book | tiddlylink book | threeColumnTiddly } AFTER - declare Function ============================================== latestbook () { : date: 2020-12-05; table_history book_page.rdb | i_timeDate latest | ... jointable -1 book -2 book - author_book.rdb | ... ... column latest page author book | tee $(myname).rdb } readhistory () { : reading history with currentbooks on top; : date: 2020-12-30; : date: 2021-01-03; zcat .hry/book_page.rdb.Z | sorttable -r | select_first book | .. ncolumn delete_time | rdb_iDate | rename i_date latest | .. column latest page book | tiddlylink book | threeColumnTiddly }
1.6.3 Test data
The two reformatted functions report on my reading history, where I usr rdb to keep track of the books I'm reading. Here's a current copy the table /book_page.rdb followed by a report generated by this command:
page book ---- ---- 360 Life and Fate 150 How to Be an Antiracist 20 It Can't Happen Here 266 Say Nothing: A True Story of Murder and Memory ... 120 The Lake Wobegon Virus 3640 Last Seen Wearing
> latestbook | sorttable | justify
latest page author book ------ ---- ------ ---- 210102 20 Sinclair Lewis It Can't Happen Here 210102 120 Garrison Keillor The Lake Wobegon Virus 210102 150 Ibram X Kendi How to Be an Antiracist 210102 266 Patrick Radden Keefe Say Nothing: A True Story of Murder and Memory ... 210102 360 Vasily Grossman Life and Fate 210102 3640 Colin Dexter Last Seen Wearing
1.7 A bug discovered func_args bug read_devtty
The declare and func_args functions were introduced in 1.6.1.
Func_args in that instance had a bug. It wasn't sufficiently
specific. Since the return from type -a
looks like
someFunctionName is a function someFunctionName () { ... }
testing for the fourth field as function is incomplete. The test is
now more specific. Also, enforcing a convention in my practice, functions
with leading _
in their name are local to a function, function family, or
function library, and not generally discoverable.
The declare
code has been instrumented with diagnostic read_devtty
calls, which are quiet until a trace_on
.
func_args () { : use TYPE to simulate declare -f; : date: 2021-03-09 much more explict TYPE -A test in AWK; type -a ${@:-/dev/null} 2> /dev/null | tee .dot.0 | awk ' $2$3$4 ~ /isafunction/ && \ $1 ~ /^[a-zA-Z0-9][a-zA-Z0-9_]*$/ { print $1 } ' | sort -u } declare () { : insert ... syntactic sugar into shell functions; : date: 2021-01-03; : date: 2021-03-09 the BUG was in FUNC_ARGS; echo $* | sort | tpl > .dot.a; set -- $(args_uniq $*); read_devtty B: $#; case $1 in -f) : test for function names; set -- $(func_args ${@:2}); read_devtty C: $#, $@; [[ $# -lt 1 ]] && return 1; :; : there are some, proceed; command declare -f $@ | fold_pipedots ;; *) read_devtty D: $#, $@; command declare $@ ;; esac }
1.8 A reflection
The whole purpose of this exercise is shown in the difference in
the readable layout offered by the declare
function. The developer
need not ever look at long lines again from pipes too numerous to
fit on the printable page.
And, from a maintenance standpoint, the developer is free to add or
remove any ...
functions in the folded layout.
At the risk of over emphasis, the developer need only work on the folded copy of the function. I place so much emphasis on this because the canonical copy of the function is at the centerpiece of my library maintenance practice. Here's a typical sequence:
$ set -- someFunctionLib $ $EDITOR $1 $ declare -f $(functions $1) | tee $1.new $ diff $1 $1.new # satisfy yourself all is well $ mv $1.new $1 $ backup $1 # see the reference below
This is the most recent chapter in my Commonplace Book. As such it sets the future direction of that work. Today's challenge is the work to place it in that framework. The tasks:
k+ load it to the Web: mcgowans.org/pubs/marty3/commonplace
- place it in it's appropriate chapter
- set links to the chapter and back to this article