Simulating command history and editing inside a looping bash script

249 views Asked by At

I'd like to have a bash script that implements some of the functionality of the bash command line itself: namely, command history and vi-style command editing.

The script would loop forever (until crtl/d) and read input from the user in terminal, treating each line as a command. The commands are actually a set of shell scripts that I have already written which are designed to support a photo work flow. The same edit and recall functionality should be available in this interpreted environment.

Having bash command history and command editing functions in this script would be very desirable.

1

There are 1 answers

0
MetalGodwin On

Was looking for a way to mimic command history within script as well, couldn't find much on it online, so built a simple one myself. Not exacly what you asked for, but might give you, or anyone else some references.

It really just is one big function that does nothing else than handle the prompt like behaviour and return the on screen string from pressing enter. It allows for browsing of appointed history file, while saving new input, to go back to. Auto indent or moving the marker is not implemented below. I think the script require bash version 4, with the arithmetic shells, but change to an older syntax and bash 3 should work. It's not fully tested yet.

Use it as:

./scriptname.sh /optional/path/to/history_file

The script

#!/bin/bash
# vim: ts=4:

function getInput() {

    local hist_file="${1:-.script_hist}";
    local result="";
    local escape_char=$(printf "\u1b")
    local tab=$(echo -e "\t");
    local backspace=$(cat << eof
0000000 005177
0000002
eof
);

    local curr_hist=0;
    local curr_cmd="";

    function browseHistory() {

        ! test -s "$hist_file" && return 1;

        local max_hist="$(cat "$hist_file" | wc -l || echo 0)";
    
        curr_hist=$((curr_hist + "$1"));

        (( curr_hist > max_hist )) && curr_hist=$max_hist;
        
        if (( curr_hist <= 0 )); then
            curr_hist=0;
            return 1;
        fi

        result="$(sed -n "$((max_hist - curr_hist + 1))p" < "$hist_file")";
    
        return 0;
    }


    ifs=$IFS;

    while true; do

        # empty IFS, read one char
        IFS= read -rsn1 input

        if [[ $input == $escape_char ]]; then
            # read two more chars, this is for non alphanumeric input
            read -rsn2 input
        fi


        # check special case for backspace or tab first
        # then move onto arrow keys or anything else in case
        if [[ $(echo "$input" | od) = "$backspace" ]]; then
        
            # delete last character of current on screen string
            result=${result%?};

        elif [ "$input" = "$tab" ]; then

            # replace with function call for autofill or something
            # it's unused but added in case it would be useful later on
            continue;

        else

            case $input in
                '[A')
                    ! browseHistory '1' && result=$curr_cmd;
                    ;;
                '[B')
                    ! browseHistory '-1' && result=$curr_cmd;
                    ;;
                '[D') continue ;; # left, does nothing right now
                '[C') continue ;; # right, this is still left to do
                *) 
                    # matches enter and returns on screen string
                    [[ "$input" == "" ]] && break;

                    result+=$input
                    ;;
            esac
        fi

        # store current command, for going back after browsing history
        (( curr_hist == 0 )) && curr_cmd="$result";
        
        echo -en "\r\033[K";
        echo -en "${result}"

    done

    IFS=$ifs;

    test -n "$result" && echo "$result" >> "$hist_file";

    return 0;
}

getInput $1