History in shell programs
Shell programs save commands on the prompt to history files, the default history file for bash is ~/.bash_history.
I use the history to remember commands I want to reuse.
In bash, I use this configuration in ~/.bashrc:
# append to the history file, don't overwrite it
shopt -s histappend
# don't put duplicate lines in the history. See bash(1) for more options
export HISTCONTROL=ignoredups
# for setting history length see HISTSIZE and HISTFILESIZE in bash(1)
export HISTSIZE=100000
export HISTFILESIZE=100000
export HISTTIMEFORMAT="%y-%m-%d %T "
export PROMPT_COMMAND="history -a"
The history shows the time when the command was executed and the new lines are added immediatly to the history file. This allows keeping the whole history when multiple shells are opened.
I have an environment (RHEL 7-8+Extra) which ignores $HISTSIZE and $HISTFILESIZE and the lines older than 3 days are deleted.
So I setup my own history which saves the exit code, date and the current path and filters out commands matching a pattern.
The exit code is wrong in the history when a command is suspended, $PROMPT_COMMAND is executed before the command is finished.
export PROMPT_COMMAND='rs=$? ; lastCommand=`history 1 | cut -c 26-` ; ~/bin/saveHistory $rs "$lastCommand"'
bind -x '"\C-r": READLINE_LINE=`~/bin/revShowMyHistory | fzf --height=20`'
alias h='showMyHistory | less +G'
bind -x '"\C-f": READLINE_LINE=`~/bin/atMyHistory ${READLINE_LINE}`'
rs=$?saves the exit code to a variable before it is lost- "lastCommand=
history 1 | cut -c 26-" saves the last command line without the date to a variable. The command line is saved in a variable because the history command doesn't work when it is in saveHistory code. lastCommand could be: 'lastCommand=HISTTIMEFORMAT= history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//"' ~/bin/saveHistory $rs "$lastCommand"appends exit code, date, pwd and command line to the history file (~/myhistory.bin). The date saved in history is when the command ends- I use fzf to search in the history file, invoke with control+r
- showMyHistory prints the command history with exit code, date and working directory, the lines are highlighted in red when the exit code is not 0
- bind
\C-fenters a command from the history to the prompt, type command number (line number) in history file and then press control+f.${READLINE_LINE}reads the line number from the readline buffer (prompt, it works only inbind -x) and sed writes the command from history to the readline buffer
2024-11-29: Now the history is saved in one binary file instead of 3 text files.
saveHistorysaves path, exit code, date, command in~/myhistory.binshowMyHistoryis likehistoryrevShowMyHistoryshows the command in reverse order for fuzzy searchatMyHistoryprints the command at the given index in argument.atMyHistory 1prints the first command in history
Things I could add in the future:
- I could save the execution time of each command
- I could group commands by shell sessions
- When a command in a git repository, git status could be saved in the history (branch name and commit)
- I could add hostname and have a common history for multiple machine by sending the command lines to a server.
saveHistory source
saveHistory.c source code, compile with sheepy -c saveHistory.c (see below the install instructions):
#! /usr/bin/env sheepy
#include "libsheepyObject.h"
// ARG 1: exit code
// ARG 2: last command
// export PROMPT_COMMAND='rs=$? ; lastCommand=`history 1 | cut -c 26-` ; ~/bin/saveHistory $rs "$lastCommand"'
#define H "~/myhistory.txt"
#define HE "~/myhistoryExit.txt"
#define HP "~/myhistoryPath.txt"
#define HB "~/myhistory.bin"
// command is equal to
char *filter[] = {
"fg",
"bg",
"jobs",
"ls",
null
};
// command starts with
char *start[] = {
"fg ",
"ls ",
null
};
int main(int ARGC, char** ARGV) {
cleanCharP(newCmd) = trimS(ARGV[2]);
// filter out commands
forEachS(filter, s) {
if (eqG(newCmd, s)) ret 0;
}
forEachS(start, s) {
if (startsWithG(ARGV[2], s)) ret 0;
}
initLibsheepy(ARGV[0]);
cleanCharP(hbp) = expandHomeG(HB);
// check if last command is duplicate
// check only if history file exists
// don't save duplicate commands
// export PROMPT_COMMAND='rs=$? ; pwd >> ~/myhistoryPath.txt ; d=`date +"%F %T"` ; printf "%s %s\n" "$rs" "$d" >> ~/myhistoryExit.txt ; history 1 | cut -c 26- >> ~/myhistory.txt'
if (isPath(hbp)) {
// ignore duplicate command
u8 *buf = null;
size_t hlen = readFile(hbp, (void**)&buf);
// get last command
cleanAllocateSmallJson(a);
u32 len = *(u32*)(buf + hlen - 4);
cleanAllocateSmallBytes(b);
o(b,set, buf + hlen - len + 4, len-8);
deserialG(a, b);
// a = [path,exit code, date, command]
// this is a duplicate, stop
cleanCharP(l) = trimS(getG(a, rtChar, 3));
if (eqG(newCmd, l)) ret 0;
}
char path[8192] = init0Var;
char *res = bLGetCwd(path, sizeof(path));
char date[128] = init0Var;
pErrorNULL(bGetCurrentDateYMD(date));
cleanAllocateSmallJson(a);
// save history as binary
// a = [path,exit code, date, command]
pushG(a, path);
pushG(a, ARGV[1]);
pushG(a, date);
pushG(a, newCmd);
smallBytest *b = serialG(a);
u8 buf[65536];
u32 len = lenG(b) + 4*2;
memcpy(buf, &len, 4);
memcpy(buf+4, getValO(b), lenG(b));
memcpy(buf+4+lenG(b), &len, 4);
pError0(appendFile(hbp, buf, len));
ret 0;
}
// vim: set expandtab ts=2 sw=2:
showMyHistory source
showMyHistory.c source code, compile with sheepy -c showMyHistory.c (see below the install instructions):
#! /usr/bin/env sheepy
#include "libsheepyObject.h"
#define H "~/myhistory.txt"
#define HE "~/myhistoryExit.txt"
#define HP "~/myhistoryPath.txt"
#define HB "~/myhistory.bin"
#define maxPathLen 50
int main(int ARGC, char** ARGV) {
initLibsheepy(ARGV[0]);
cleanCharP(hbp) = expandHomeG(HB);
u8 *buf = null;
size_t hlen = readFile(hbp, (void**)&buf);
// count commands
// search for longest path
u8 *e = buf;
cleanAllocateSmallJson(a);
u64 commandCount = 0;
u16 max = 0;
do {
u32 len = *(u32*)e;
cleanAllocateSmallBytes(b);
o(b,set, e+4, len-8);
deserialG(a, b);
// a = [path,exit code, date, command]
max = maxV(max, lenG(getG(a, rtChar, 0)));
inc commandCount;
e += len;
} while(e < buf + hlen);
bool ellipsis = (max > maxPathLen) ? yes : no;
if (ellipsis) max = maxPathLen;
// show history
e = buf;
u64 count = 0;
// compute length of commandCount to align columns
char counts[32] = init0Var;
pErrorNULL(bIntToS(counts,commandCount));
u32 countLen = strlen(counts);
cleanAllocateSmallBytes(b);
char ellip[256];
do {
inc count;
u32 len = *(u32*)e;
o(b,set, e+4, len-8);
deserialG(a, b);
// a = [path,exit code, date, command]
u32 exitCode = parseIntG(getG(a, rtChar, 1));
char *res = bEllipsisStartS(ellip /*dest*/, getG(a, rtChar, 0)/*path string*/, max /*targetLen*/, "]"/*ellipsisString*/);
char *path = ellipsis ? ellip : getG(a, rtChar, 0);
if (exitCode) {
printf("%*d "RED"%3d"RST" %s "YLW"%*s"RST" "RED"%s"RST"\n", countLen, (u32)count, exitCode, /*date*/getG(a, rtChar, 2), max, path, /*cmd*/getG(a, rtChar, 3));
}
else {
printf("%*d "GRN"%3d"RST" %s "YLW"%*s"RST" %s\n", countLen, (u32)count, exitCode, /*date*/getG(a, rtChar, 2), max, path, /*cmd*/getG(a, rtChar, 3));
}
e += len;
} while(e < buf + hlen);
// vim: set expandtab ts=2 sw=2:
revShowMyHistory source
revShowMyHistory.c source code, compile with sheepy -c revShowMyHistory.c (see below the install instructions):
#! /usr/bin/env sheepy
#include "libsheepyObject.h"
#define H "~/myhistory.txt"
#define HE "~/myhistoryExit.txt"
#define HP "~/myhistoryPath.txt"
#define HB "~/myhistory.bin"
int main(int ARGC, char** ARGV) {
initLibsheepy(ARGV[0]);
cleanCharP(hbp) = expandHomeG(HB);
u8 *buf = null;
size_t hlen = readFile(hbp, (void**)&buf);
u8 *e = buf + hlen;
cleanAllocateSmallJson(a);
do {
u32 len = *(u32*)(e - 4);
cleanAllocateSmallBytes(b);
o(b,set, e - len + 4, len-8);
deserialG(a, b);
// a = [path,exit code, date, command]
puts(getG(a, rtChar, 3));
e -= len;
} while(e > buf);
ret 0;
}
// vim: set expandtab ts=2 sw=2:
atMyHistory source
atMyHistory.c source code, compile with sheepy -c atMyHistory.c (see below the install instructions):
#! /usr/bin/env sheepy
#include "libsheepyObject.h"
#define H "~/myhistory.txt"
#define HE "~/myhistoryExit.txt"
#define HP "~/myhistoryPath.txt"
#define HB "~/myhistory.bin"
int main(int ARGC, char** ARGV) {
initLibsheepy(ARGV[0]);
if (not ARGV[1]) ret 0;
u64 index = parseInt(ARGV[1]);
cleanCharP(hbp) = expandHomeG(HB);
u8 *buf = null;
size_t hlen = readFile(hbp, (void**)&buf);
u8 *e = buf;
cleanAllocateSmallJson(a);
u64 count = 1;
do {
u32 len = *(u32*)e;
if (count == index) {
cleanAllocateSmallBytes(b);
o(b,set, e+4, len-8);
deserialG(a, b);
// a = [path,exit code, date, command]
puts(getG(a, rtChar, 3));
break;
}
e += len;
inc count;
} while(e < buf + hlen);
ret 0;
}
// vim: set expandtab ts=2 sw=2:
Sheepy
Sheepy is a build system for using C as a scripting language like python. It takes less than 5 minutes to install sheepy on a machine:
apt-get install gcc git
git clone https://spartatek.se/git/sheepy.git
cd sheepy
sudo -H ./install.sh
The readme has more details. Sheepy Readme
#shell