aStack

Table of Contents

The other papers

1 A stack

If you are new to computer science, you will be new to some of the routine data structures used in the practice. Decades ago, I felt the influence of the Forth computer language. It is a stack-based machine, using primitives whose API requires you to learn the stack. There are primitives to manipulate the calling stack directly. The API to each Forth word is expressed in its stack effect.

So, why do I think we need to talk about a stack in the shell? It's now thirty years ( 1984 ) since I invented the directory stack in the shell. The commands pushd, popd are now widely available in the the shell, and too under-used, if I may say. Since the current directory is an important part of a user's environment, it makes sense the shell has operations to save and recall it.

The problem which prompted my interest in a general stack is my use of the PS1 variable. My .profile default is

setenv PS1 '\W.$ '

which sets a prompt string to the last component ( \W ) of the working directory, followed by a dot (.) , a dollar sign ($) and a space ( ). Just occasionally, when working an exercise for the [[][ittoolbox]] , I like to set a default prompt, but before do, I'd like to save it. I could put it in a shell variable, but that's so unsatisfying. Now that I can save a prompt string for one task, I could for another, and so on. Well, a single variable isn't sufficient. And you could make up an array to hold the prompt string, or any other relevant change-of-state commodity. But an array is a poor substitute for what? The stack.

The code for the stack function follows in the User's Guide:

Last things first. The command:

$ stack test

executes the code following the test) case in the function body. It's an example of how to use the stack. The doit function echos its arguments onto the stderr before executing them as a command.

First you see a test of the stack size to make sure the user doesn't have an existing stack, since this test is built to see what the stack does when exhausted, and repeated attempts to pop elements from the stack.

Before we deal with the User's Guide, let's review the operations of the stack. If you are comfortable with your understanding, skip to the next section.

1.1 Stack operations

A stack is a data structure onto which you may place any number of items and retrieve them in a LIFO – **L**ast **I**n, **F**irst **O**ut – manner. The FIFO is a queue, and the subject of another discussion.

The normal stack operations are called: push and pop. /Push/ing places an item on the stack, and /pop/ing removes an item. In software the pop should return the item on the stack for use by the current program. In our usage, the push and pop will accept arbitrary text strings, with embedded blanks. e.g.

stack push This is an arbitrary text string.
stack push PS1=\"$PS1\"

The first example shows how to put a text string on the stack. The second saves the Environment variable, PS1 and it's value as a shell assignment statement. So before we push or pop another element OR if any number of /push/es are followed by and equal number of /pop/s, then the next pop statement might be:

eval $(stack pop)

and the environment variable is restored to it's prior state, even if it had been pushed and popped a balanced number of times in between.

Other usual operations on a stack are the ability to peek at the top of the stack, using as if you had *pop*ped it, but without consuming it. (It's still waiting to be pop**ped). You might like to see how deep the stack is. For that, you can ask for the **size. Some places call the the depth. But not here.

The one other operation I've added is the clear. It guarantees an empty stack. Since this implementation uses a text file to hold the stack, there is no practical limit to the stack size (or depth). Also, an attempt to pop the empty stack is completely benign and silent.

1.2 Stack user's guide

Well, there you have it. At this moment, the above operations describe the stack's behaviour. Let's make it a little more formal:

The basic command is stack. Load the stack from the C*o*m*man*d *lib*rary:

$ source cmdlib
$ fbdy stack         # shows the text of the function

All by itself:

$ stack

the function shows a USAGE or "help" message

  1. One argument

    The first argument is the stack command. It may take more. Here are the basic commands:

    • push, e.g. "stack push …" the following words are pushed onto the file storing the stack
    • pop – returns the top of the stack to the user, e.g.
    $ message=$(stack pop)
    

    saves the stack contents in the shell variable message and it is re-used in the shell by the normal means:

    $ echo this is the message: $message on the stack
    

    when the stack has been emptied by a sufficient number of pops, subsequent pops return nothing quietly. The user is free to test the stack size if you need to know the number of items on the stack.

    • clean – guarantees no other items are on the stack
    • size – returns an integer number of items on the stack. Here an "item" is the character string placed on the stack with a single "push"
    • test – runs a demonstation test case. Inspect the code to see examples of useful stack commands

1.3 The stack function

Here is the code of the stack function. It "speaks for itself". A sufficiently experienced shell programmer will read it with ease. There are a handful of functions, produced below, which I've built support my shell programming practice.

stack () 
{ 
    function n_1 () 
    { 
        trace_call $*;
        set -- $1 $(expr $(cat $1 | wc -l ) - 1);
        [[ $2 -gt 0 ]] && sed ${2}q $1
    };
    set -- $(needir $(home)/lib)/datastack.txt $*;
    trace_call $*;
    case $2 in 
        clear)
            cp /dev/null $1
        ;;
        push)
            echo $(shift 2; echo $*) | tee -a $1
        ;;
        peek)
            [[ -f $1 ]] || touch $1;
            tail -1 $1
        ;;
        pop)
            stack peek;
            set -- $(stack size) $1 /tmp/datastack.txt;
            trace_call $*;
            [[ $1 -lt 1 ]] && return;
            shift;
            n_1 $1 > $2;
            mv $2 $1
        ;;
        size)
            trace_call $*;
            [[ -f $1 ]] || touch $1;
            cat $1 | wc -l
        ;;
        test)
            [[ $(stack size) -gt 0 ]] && { 
                comment save your stack before proceending, then;
                comment stack clear;
                return
            };
            doit stack push this is Number 1.;
            doit stack peek;
            doit stack size;
            doit stack push how about no. 2;
            stack push PS1=\"$PS1\";
            doit stack size;
            doit PS1=$;
            comment PS1=$PS1;
            eval $(stack pop);
            comment PS1=$PS1;
            doit stack pop;
            doit stack pop;
            doit stack pop
        ;;
        *)
            trace_call $*;
            comment USAGE stack "[[clear | peek | pop | size | test] push words to push together]"
        ;;
    esac
}

1.4 the supporting functions.

Here's a list:

* doit
* comment
* trace_call
* needir

and their code, where the trace functions allow traceon and traceoff to show the function call traces on stderr or not.

comment     () { echo $* 1>&2; }
doit () 
{ 
    comment $*;
    eval $@
}
needir () 
{ 
    [[ -d $1 ]] || mkdir -p $1;
    echo $1
}
trace_call  () { printf "TRACE %s ( %s )\n" ${FUNCNAME[1]} "$*" 1>&2; }
trace_off   () { eval "trace_call () { return; }"; }
trace_on    () { eval "trace_call () { trace_stderr \"\$@\"; }"; }
trace_stderr        () { printf "TRACE %s ( %s )\n" ${FUNCNAME[2]} "$*" 1>&2; }

That's it for now.

Author: Marty

Created: 2016-02-20 Sat 15:47

Emacs 24.4.1 (Org mode 8.2.10)

Validate