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

Author: Marty McGowan

Created: 2021-01-04 Mon 13:31

Validate