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

Author: Marty McGowan

Created: 2021-03-11 Thu 10:19

Validate