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
func_args () { : use TYPE to simulate declare -f; : limitation: QUOTED args may not be REFORMATTED; 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 "$@" } 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 ... .. 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 ~…~s 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.7 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 in 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 funtion 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:
- 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