Git Product home page Git Product logo

shell-history's Introduction

Shell History

ci documentation pypi version

Inspired by bamos/zsh-history-analysis.

Visualize your usage of Bash/Zsh through a web app thanks to Flask and Highcharts!

Durationduration chart Lengthlength chart Typetype chart
Exit codeexit code chart Hourlyhourly chart Dailydaily chart
Over timeover time chart Markov chainmarkov chart Top commandstop chart

Post your charts ideas in this issue!

Requirements

Shell History requires Python 3.6 or above.

To install Python 3.6, I recommend using pyenv.
# install pyenv
git clone https://github.com/pyenv/pyenv ~/.pyenv

# setup pyenv (you should also put these three lines in .bashrc or similar)
export PATH="${HOME}/.pyenv/bin:${PATH}"
export PYENV_ROOT="${HOME}/.pyenv"
eval "$(pyenv init -)"

# install Python 3.6
pyenv install 3.6.12

# make it available globally
pyenv global system 3.6.12

Installation

With pip:

python3.6 -m pip install shellhistory

With pipx:

python3.6 -m pip install --user pipx

pipx install --python python3.6 shellhistory

Setup

shellhistory needs a lot of info to be able to display various charts. The basic shell history is not enough. In order to generate the necessary information, you have to enable the shell extension.

At shell startup, in .bashrc or .zshrc, put the following:

# only load it for interactive shells
if [[ $- == *i* ]] && command -v shellhistory-location &>/dev/null; then
    . $(shellhistory-location)
    shellhistory enable
fi

... and now use your shell normally!

If you want to stop shellhistory, simply run shellhistory disable.

Note: for performance reasons, you can also use the static, absolute path to the source file. Indeed, calling shellhistory-location spawns a Python process which can slow down your shell startup. Get the path once with shellhistory-location, and use . <ABS_PATH>. In my case it's . ~/.local/pipx/venvs/shellhistory/lib/python3.6/site-packages/shellhistory/shellhistory.sh.

Usage

Launch the web app with shellhistory-web. Now go to http://localhost:5000/ and enjoy!

You will need Internet connection since assets are not bundled.

Some technical info

How it works

When you enter a command, shellhistory will compute values before and after the command execution. In Bash, it uses a trap on DEBUG and the PROMPT_COMMAND variable (man bash for more information). For Zsh, it uses the preexec_functions and precmd_functions arrays (anyone knows where to find the official documentation for these? Some information in man zshmisc).

Before the command is executed, we start a timer, compute the command type, and store the current working directory and the command itself.

After the command has finished, we store the return code, and stop the timer.

History file format

Fields saved along commands are start and stop timestamps, hostname, username, uuid (generated), tty, process' parents, shell, shell level, command type, return code, and working directory (path), in the following format: :start:stop:uuid:parents:host:user:tty:path:shell:level:type:code:command.

  • multi-line commands are prepended with a semi-colon ; instead of a colon :, starting at second line
  • start and stop timestamps are in microseconds since epoch
  • process' parents and working directory are encoded in base64 to avoid delimiter corruption.

Example (multi-line command):

:1510588139930150:1510588139936608:40701d9b-1807-4a3e-994b-dde68692aa14:L2Jpbi9iYXNoCi91c3IvYmluL3B5dGhvbiAvdXNyL2Jpbi94LXRlcm1pbmFsLWVtdWxhdG9yCi91c3IvYmluL29wZW5ib3ggLS1zdGFydHVwIC91c3IvbGliL3g4Nl82NC1saW51eC1nbnUvb3BlbmJveC1hdXRvc3RhcnQgT1BFTkJPWApsaWdodGRtIC0tc2Vzc2lvbi1jaGlsZCAxMiAyMQovdXNyL3NiaW4vbGlnaHRkbQovc2Jpbi9pbml0Cg==:myhost:pawamoy:/dev/pts/1:L21lZGlhL3Bhd2Ftb3kvRGF0YS9naXQvc2hlbGxoaXN0Cg==:/bin/bash:1:builtin:0:echo "a
;b
;c" | wc -c

Note: later we could use CSV formatting, quoting strings and doubling double-quotes in those if any. It would make the file more readable for humans, and easily importable in other programs. See issue 26.

The previous example would look like this:

1510588139930150,1510588139936608,40701d9b-1807-4a3e-994b-dde68692aa14,"/bin/bash
/usr/bin/python /usr/bin/x-terminal-emulator
/usr/bin/openbox --startup /usr/lib/x86_64-linux-gnu/openbox-autostart OPENBOX
lightdm --session-child 12 21
/usr/sbin/lightdm
/sbin/init",myhost,pawamoy,/dev/pts/1,"/media/pawamoy/Data/git/shellhist",/bin/bash,1,builtin,0,"echo ""a
b
c"" | wc -c"

How we get the values

Start and stop time are obtained with date '+%s%N', return code is passed directly with $?, working directory is obtained with $PWD and command type with type for Bash and whence for Zsh.

Values for UUID, parents, hostname, and TTY are computed only once, when shellhistory.sh is sourced. Indeed they do not change during usage of the current shell process. Hostname and TTY are obtained through commands hostname and tty. UUID is generated with command uuidgen. Also note that UUID is exported in subshells so we know which shell is a subprocess of another, and so we are able to group shell processes by "sessions", a session being an opened terminal (be it a tab, window, pane or else). Parents are obtained with a function that iteratively greps ps result with PIDs (see shellhistory.sh).

Values for user, shell, and level are simply obtained through environment variables: $USER, $SHELL (though its use here is incorrect: see issue 24), and $SHLVL (also see issue 25).

The last command is obtained with the command fc. Using fc allows shellhistory to have the same behavior as your history:

  • if commands starting with spaces are ignored, they will be ignored in shellhistory as well.
  • same for duplicates (entering ls two or more times saves only the first instance). Note however that if you type the same command as the previous one in an other terminal, it will still be appended, unless you manage to synchronize your history between terminals, which is another story.

Additionally, if you enter an empty line, or hit Control-C before enter, nothing will be appended either. The trick behind this is to check the command number in the current history (see shellhistory.sh for technical details).

shell-history's People

Contributors

pawamoy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

shell-history's Issues

Trap on DEBUG not removed with shellhistory disable

It seems to be because trap - DEBUG is run inside of a function (_shellhistory_disable).

A quick fix for this is to use PROMPT_COMMAND to unset it instead:

new_prompt_command=${PROMPT_COMMAND//_shellhistory_after;}
PROMPT_COMMAND="trap '${trap:--}' DEBUG; PROMPT_COMMAND='${new_prompt_command}'"

Explanation:

  • first we compute the new prompt command (the one without shellhistory)
  • then we set PROMPT_COMMAND to:
    • a command that will update the trap (when $trap is not empty) or remove it (when $trap is empty, will be replaced by -)
    • and a command that will set it to the new prompt command

event not found: -0

my system

$ uname -a
Linux novo 4.15.0-39-generic #42-Ubuntu SMP Tue Oct 23 15:48:01 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)

my problem

when I open a shell (e.g gnome-terminal) 3 errors appear:

_shellhistory_last_command:fc:2: event not found: -0                            
_shellhistory_last_command_number:fc:1: event not found: -0
_shellhistory_last_command:fc:2: event not found: -0

I can keep using my shell, but no command data is logged.

solution

my solution

this PR

the question

why does fc not work with arg -0 ? Is there a problem with my shell setup?

@pawamoy thanks for your work

Use PS0 instead of a trap on DEBUG?

The value of this parameter is expanded (see PROMPTING below) and displayed by interactive shells after reading a command and before the command is executed.

Cons: must run commands in a subshell, therefore unable to set variables. Must write in a file and then retrieve contents after execution.

Pros: not subject to debug printing.

Don't append last command when hitting enter on empty line

When we enter an empty command, the last one (from builtin history 1) is appended to the extended history file. I have only one idea to fix this: record the BASH_COMMAND variable and check if it's empty (or composed of spaces). An issue with that is that the comments will be removed I think, so typing #just a comment would not be appended to history (is it really a problem?) ...

Use dichotomy instead of "one by one" when db commit fails

Write a recursive function using dichotomy to insert objects in the database.

  • If inserting all objects fails, split the list in two, recurse on both parts.
  • Stop condition: when there is only one object in the list.
  • Sum up results: return size of (successfully) inserted list.

This will allow faster database updates from big history files.

SQLite objects created in a thread can only be used in that same thread

my system

$ uname -a
Linux novo 4.15.0-39-generic #42-Ubuntu SMP Tue Oct 23 15:48:01 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

$ zsh --version
zsh 5.4.2 (x86_64-ubuntu-linux-gnu)

my problem

when I open a the admin page I get a 500 error due to SQLite objects created in a thread can only be used in that same thread.

solution

add a get_session like to db.py

Session = sessionmaker(bind=engine)

def get_session():
    _engine = create_engine("sqlite:///%s" % DB_PATH)
    session = sessionmaker(bind=_engine)
    return session()

in order to create a new SQLite session (that will be destroyed)

my solution

this PR

@pawamoy thanks for your work

Improve type getter

  • VAR=value command do something: assign + command type?
  • VAR=value: assign?
  • function f { ... }: define?
  • f() { ... }: define?

Any other?

Unable to install with pip or pipx

pip output:

% pip install shellhistory
Collecting shellhistory
  Could not find a version that satisfies the requirement shellhistory (from versions: )
No matching distribution found for shellhistory

Split the code in multiple modules

  1. shellhistory.sh: provide the command-line tool with commands to enable and disable the extended shell history.
  2. the database: database-related Python code
  3. the web app: flask app to visualize the data, making use the database-related code
  4. plugins: add subcommands to the CLI tool:
    • import data from file
    • export data to file
    • search database
    • CRUD rows in the database

Add install script

sudo make install or sudo ./install.sh

  • check if python3 is available, if not, exit with message
  • check if virtualenv is available, if not install with apt-get install -y python3-virtualenv
  • create the virtualenv somewhere, install the requirements
  • create a script as /usr/bin/run-shell-history or else, with the right env vars
  • output a message to tell user to source shellhistory.sh at startup (pastable line)

Also write an uninstall script:

  • delete the virtualenv
  • delete the run script

Charts ideas

Post your ideas here!

Reminder of the available fields for analysis:

  • start: command start time
  • stop: command stop time
  • duration: duration of the command
  • uuid: session id, shared with subshells
  • parents: process parents, newline-separated commands, something like:
    /bin/bash
    /usr/bin/python /usr/bin/x-terminal-emulator
    sh -c x-terminal-emulator
    tint2 -c /home/pawamoy/.config/tint2/tint2rc
    /sbin/init
    
  • host: hostname
  • user: username
  • tty: something like /dev/pts/0 or /dev/tty1
  • path: command working directory
  • shell: shell used (/bin/bash, /bin/zsh, etc.)
  • level: shell level (usually 2 in a GUI, 1 in a Ctrl-Alt-F1/6 TTY)
  • type: command type (builtin, alias, function, file, keyword, none)
  • code: command return code
  • cmd: the command itself

Don't append command if hitting Ctrl-C before Enter

Ctrl-C can be used to clear out the current typed content. But each time Ctrl-C is hit, shellhist is triggered with a return code of 130 and the typed content as command. I don't think people want these lines to be saved.

Make sure SHLVL is working as expected

If it's not, we can still rely on the parents field to compute it.

It seems to be working fine at first glance:

$ # in bash
$ echo $SHLVL
1
$ zsh
$ echo $SHLVL
2

...though SHLVL is defined in the Bash man page as follow:

SHLVL Incremented by one each time an instance of bash is started.

Maybe it's also used the same way by Zsh.

Cannot find bash history

As the title says it cannot find the history.

Failed to import current history. The following exception occured: <class 'ValueError'>: /root/.shell_history/history: no such file

I am running Raspbian GNU/Linux 9 (stretch)

Any idea how to solve it?

Handle using history on multiple hosts

Currently, in order to visualize data for multiple hosts, the user has to manually concatenate all his history files into one and import it, or import multiple files. The app should be able to do this automatically.

We must better handle duplicates and database updates.

Stop using SHELL variable

It seems to be set to the user's default shell (in /etc/passwd). Appears as /bin/bash even in Zsh. Fortunately, the parents field holds the correct information.

Follow the history file in real-time

While running the app, we could daemonize a history file reader which would insert the object in the database each time new lines are appended (new lines because of multiline commands).

The "sync data" button would then be deprecated in favor of an import feature (modal with file input for example). See #8.

Use CSV formatting instead of custom colon-separated + base64 encoded

The idea is to make the history file more readable for humans, but also easily importable by other tools.

I don't think there's a library for handling CSV files in Bash, and even if there was, I think it would slow down things a lot, which we don't want because shellhistory happens in interactive shells and we cannot let users wait for more than 200ms between each command. Appending a line to the history file must be as fast as possible.

But the CSV formatting is not hard either, so we could do it by hand. If I remember correctly, you can use any delimiter you want, as long as fields that contain it are enclosed in double-quotes. If these same fields also contain double-quotes, then you must simply double these.

Example with a comma as delimiter:

field 1,,field 3,"hey, I'm field 4!",,"he said ""4""? alright, then I'm gonna say ""6""!",field 7

Same string imported in GNumeric:

Screenshot_2019-05-01_15-58-35

It's working, yay!

So instead of encoding parents and working directory in base 64, we could simply enclose them in double-quotes, and use some sed 's/"/""/g on it. And instead of prepending a colon on normal lines, and a semi-colon on extra command lines, we could also simply do the same for the command.

:...:echo "a
;b
;c" | wc -c

would become

...:"echo ""a
b
c"" | wc -c"

Also, just to make sure, we could also enclose hostname and user in double-quotes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.