PK!2!! README.md# Shell History [![pipeline status](https://gitlab.com/pawamoy/shell-history/badges/master/pipeline.svg)](https://gitlab.com/pawamoy/shell-history/commits/master) Inspired by [bamos/zsh-history-analysis](https://github.com/bamos/zsh-history-analysis). Visualize your usage of Bash/Zsh through a web app thanks to [Flask](http://flask.pocoo.org/) and [Highcharts](https://www.highcharts.com/)!
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](#requirements) - [Installation](#installation) - [Setup](#setup) - [Usage](#usage) - [Some technical info](#some-technical-info) - [How it works](#how-it-works) - [History file format](#history-file-format) - [How we get the values](#how-we-get-the-values) - [License](#license) ## Requirements `shellhistory` requires Python 3.6 or above.
To install Python 3.6, I recommend using pyenv. ```bash # install pyenv git clone https://github.com/pyenv/pyenv ~/.pyenv # setup pyenv (you should also put these two lines in .bashrc or similar) export PATH="${HOME}/.pyenv/bin:${PATH}" eval "$(pyenv init -)" # install Python 3.6 pyenv install 3.6.7 # make it available globally pyenv global system 3.6.7 ```
## Installation With `pip`: ```bash python3.6 -m pip install shellhistory ``` With [`pipx`](https://github.com/pipxproject/pipx): ```bash python3 -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: ```bash # 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 `. `. 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/](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](https://github.com/pawamoy/shell-history/issues/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](https://github.com/pawamoy/shell-history/issues/24)), and `$SHLVL` (also see [issue 25](https://github.com/pawamoy/shell-history/issues/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). ## License Software licensed under the [ISC](https://www.isc.org/downloads/software-support-policy/isc-license/) license. See the [LICENSE](/LICENSE) file. PK!Q5pyproject.toml[build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry] name = "shellhistory" version = "0.2.3" description = " Visualize your shell usage with Highcharts!" license = "ISC" authors = ["Timothée Mazzucotelli "] readme = 'README.md' repository = "https://github.com/pawamoy/shell-history" homepage = "https://github.com/pawamoy/shell-history" keywords = ['shell', 'bash', 'zsh', 'history', 'analysis', 'visualization'] packages = [ { include = "shellhistory", from = "src" } ] include = [ "README.md", "pyproject.toml" ] [tool.poetry.dependencies] python = "^3.6" flask = "^1.0" Flask-Admin = "^1.5" sqlalchemy = "^1.3" tqdm = "^4.28" [tool.poetry.dev-dependencies] pytest = "*" pytest-cov = "*" pytest-sugar = "*" ipython = "^7.2" [tool.poetry.scripts] shellhistory-cli = 'shellhistory.cli:main' shellhistory-location = 'shellhistory.cli:location' shellhistory-web = 'shellhistory.cli:web' [tool.black] line-length = 120 PK!shellhistory/__init__.pyPK!Bshellhistory/__main__.py""" Entry-point module, in case you use `python -m shellhistory`. Why does this file exist, and why __main__? For more info, read: - https://www.python.org/dev/peps/pep-0338/ - https://docs.python.org/2/using/cmdline.html#cmdoption-m - https://docs.python.org/3/using/cmdline.html#cmdoption-m """ import sys from shellhistory.cli import main if __name__ == "__main__": sys.exit(main(sys.argv[1:])) PK!'55shellhistory/app.py# -*- coding: utf-8 -*- import time from collections import Counter, defaultdict from datetime import datetime, time as dt_time import statistics from flask import Flask, jsonify, render_template from flask_admin import Admin from flask_admin.contrib.sqla import ModelView from sqlalchemy import extract, func, desc from sqlalchemy.sql import func as sqlfunc from . import db # Initialization and constants ------------------------------------------------ app = Flask(__name__) app.secret_key = "2kQOLbr6NtfHV0wIItjHWzuwsgCUXA4CSSBWFE9yELqrkSZU" db.create_tables() # Flask Admin stuff ----------------------------------------------------------- class HistoryModelView(ModelView): can_create = False can_delete = True can_view_details = True create_modal = True edit_modal = True can_export = True page_size = 50 list_template = "admin/history_list.html" column_exclude_list = ["parents"] column_searchable_list = [ "id", "start", "stop", "duration", "host", "user", "uuid", "tty", "parents", "shell", "level", "type", "code", "path", "cmd", ] column_filters = ["host", "user", "uuid", "tty", "parents", "shell", "level", "type", "code", "path", "cmd"] column_editable_list = ["host", "user", "uuid", "tty", "shell", "level", "type", "code", "path", "cmd"] form_excluded_columns = ["start", "stop", "duration"] # form_widget_args = { # 'start': {'format': '%Y-%m-%d %H:%M:%S.%f'}, # 'stop': {'format': '%Y-%m-%d %H:%M:%S.%f'}, # 'duration': {'format': '%Y-%m-%d %H:%M:%S.%f'} # } admin = Admin(app, name="Shell History", template_mode="bootstrap3") admin.add_view(HistoryModelView(db.History, db.get_session())) # Utils ----------------------------------------------------------------------- def since_epoch(date): return time.mktime(date.timetuple()) def fractional_year(start, end): this_year = end.year this_year_start = datetime(year=this_year, month=1, day=1) next_year_start = datetime(year=this_year + 1, month=1, day=1) time_elapsed = since_epoch(end) - since_epoch(start) year_duration = since_epoch(next_year_start) - since_epoch(this_year_start) return time_elapsed / year_duration # Special views --------------------------------------------------------------- @app.route("/") def home_view(): return render_template("home.html") @app.route("/update") def update_call(): data = {"message": None, "class": None} try: report = db.update() except Exception as e: data["class"] = "danger" data["message"] = "%s\n%s: %s" % ( "Failed to import current history. The following exception occurred:", type(e), e, ) else: if report.inserted: data["class"] = "success" data["message"] = ( "Database successfully updated (%s new items), refresh the page to see the change." % report.inserted ) if report.duplicates: data["class"] = "info" data["message"] += "\n%s duplicates were not imported." % report.duplicates else: data["class"] = "default" data["message"] = "Database already synchronized, nothing changed." return jsonify(data) # Simple views rendering templates -------------------------------------------- @app.route("/codes") def codes_view(): return render_template("codes.html") @app.route("/daily") def daily_view(): return render_template("daily.html") @app.route("/daily_average") def daily_average_view(): return render_template("daily_average.html") @app.route("/duration") def duration_view(): return render_template("duration.html") @app.route("/hourly") def hourly_view(): return render_template("hourly.html") @app.route("/hourly_average") def hourly_average_view(): return render_template("hourly_average.html") @app.route("/length") def length_view(): return render_template("length.html") @app.route("/markov") def markov_view(): return render_template("markov.html") @app.route("/markov_full") def markov_full_view(): return render_template("markov_full.html") @app.route("/monthly") def monthly_view(): return render_template("monthly.html") @app.route("/monthly_average") def monthly_average_view(): return render_template("monthly_average.html") @app.route("/over_time") def over_time_view(): return render_template("over_time.html") @app.route("/top_commands_full") def top_commands_full_view(): return render_template("top_commands_full.html") @app.route("/top_commands") def top_commands_view(): return render_template("top_commands.html") @app.route("/trending") def trending_view(): return render_template("trending.html") @app.route("/type") def type_view(): return render_template("type.html") @app.route("/yearly") def yearly_view(): return render_template("yearly.html") # Routes to return JSON contents ---------------------------------------------- @app.route("/codes_json") def codes_json(): session = db.Session() results = session.query(db.History.code, func.count(db.History.code)).group_by(db.History.code).all() # total = sum(r[1] for r in results) data = [{"name": r[0], "y": r[1]} for r in sorted(results, key=lambda x: x[1], reverse=True)] return jsonify(data) @app.route("/daily_json") def daily_json(): session = db.Session() results = defaultdict(int) results.update( session.query(func.strftime("%w", db.History.start).label("day"), func.count("day")).group_by("day").all() ) data = [results[str(day)] for day in range(1, 7)] # put sunday at the end data.append(results["0"]) return jsonify(data) @app.route("/daily_average_json") def daily_average_json(): session = db.Session() mintime = session.query(func.min(db.History.start)).first()[0] maxtime = session.query(func.max(db.History.start)).first()[0] number_of_weeks = (maxtime - mintime).days / 7 + 1 results = defaultdict(int) results.update( session.query(func.strftime("%w", db.History.start).label("day"), func.count("day")).group_by("day").all() ) data = [float("%.2f" % (results[str(day)] / number_of_weeks)) for day in range(1, 7)] # put sunday at the end data.append(float("%.2f" % (results["0"] / number_of_weeks))) return jsonify(data) @app.route("/duration_json") def duration_json(): session = db.Session() results = session.query(db.History.duration).all() flat_values = [r[0].seconds + round(r[0].microseconds / 1000) for r in results] counter = Counter(flat_values) data = { "average": float("%.2f" % statistics.mean(flat_values)), "median": statistics.median(flat_values), "series": [counter[duration] for duration in range(1, max(counter.keys()) + 1)], } return jsonify(data) @app.route("/hourly_json") def hourly_json(): session = db.Session() results = defaultdict(lambda: 0) results.update( session.query(extract("hour", db.History.start).label("hour"), func.count("hour")).group_by("hour").all() ) data = [results[hour] for hour in range(0, 24)] return jsonify(data) @app.route("/hourly_average_json") def hourly_average_json(): session = db.Session() mintime = session.query(func.min(db.History.start)).first()[0] maxtime = session.query(func.max(db.History.start)).first()[0] number_of_days = (maxtime - mintime).days + 1 results = defaultdict(lambda: 0) results.update( session.query(extract("hour", db.History.start).label("hour"), func.count("hour")).group_by("hour").all() ) data = [float("%.2f" % (results[hour] / number_of_days)) for hour in range(0, 24)] return jsonify(data) @app.route("/length_json") def length_json(): session = db.Session() results = defaultdict(lambda: 0) results.update( session.query(func.char_length(db.History.cmd).label("length"), func.count("length")).group_by("length").all() ) if not results: return jsonify({}) flat_values = [] for length, number in results.items(): flat_values.extend([length] * number) data = { "average": float("%.2f" % statistics.mean(flat_values)), "median": statistics.median(flat_values), "series": [results[length] for length in range(1, max(results.keys()) + 1)], } return jsonify(data) @app.route("/markov_json") def markov_json(): session = db.Session() words_2 = [] w2 = None words = session.query(db.History.cmd).order_by(db.History.start).all() for word in words: w1, w2 = w2, word[0].split(" ")[0] words_2.append((w1, w2)) counter = Counter(words_2).most_common(40) unique_words = set() for (w1, w2), count in counter: unique_words.add(w1) unique_words.add(w2) unique_words = list(unique_words) data = { "xCategories": unique_words, "yCategories": unique_words, "series": [[unique_words.index(w2), unique_words.index(w1), count] for (w1, w2), count in counter], } return jsonify(data) @app.route("/markov_full_json") def markov_full_json(): session = db.Session() words_2 = [] w2 = None words = session.query(db.History.cmd).order_by(db.History.start).all() for word in words: w1, w2 = w2, word[0] words_2.append((w1, w2)) counter = Counter(words_2).most_common(40) unique_words = set() for (w1, w2), count in counter: unique_words.add(w1) unique_words.add(w2) unique_words = list(unique_words) data = { "xCategories": unique_words, "yCategories": unique_words, "series": [[unique_words.index(w2), unique_words.index(w1), count] for (w1, w2), count in counter], } return jsonify(data) @app.route("/monthly_json") def monthly_json(): session = db.Session() results = defaultdict(lambda: 0) results.update( session.query(extract("month", db.History.start).label("month"), func.count("month")).group_by("month").all() ) data = [results[month] for month in range(1, 13)] return jsonify(data) @app.route("/monthly_average_json") def monthly_average_json(): session = db.Session() mintime = session.query(func.min(db.History.start)).first()[0] maxtime = session.query(func.max(db.History.start)).first()[0] number_of_years = fractional_year(mintime, maxtime) + 1 results = defaultdict(lambda: 0) results.update( session.query(extract("month", db.History.start).label("month"), func.count("month")).group_by("month").all() ) data = [float("%.2f" % (results[month] / number_of_years)) for month in range(1, 13)] return jsonify(data) @app.route("/over_time_json") def over_time_json(): session = db.Session() results = session.query(db.History.start).order_by(db.History.start).all() def datetime_to_milliseconds(dt): return int(datetime(dt.year, dt.month, dt.day).timestamp() * 1000) counter = Counter([datetime_to_milliseconds(r[0]) for r in results]) data = [(k, v) for k, v in counter.items()] return jsonify(data) @app.route("/top_commands_full_json") def top_commands_full_json(): session = db.Session() results = ( session.query(db.History.cmd, func.count(db.History.cmd).label("count")) .group_by(db.History.cmd) .order_by(desc("count")) .limit(20) .all() ) data = {"categories": [r[0] for r in results], "series": [r[1] for r in results]} return jsonify(data) @app.route("/top_commands_json") def top_commands_json(): session = db.Session() # Tried to do this with SQL only. Failed. POSITION is not a function. # results = ( # session.query( # sqlfunc.substr(db.History.cmd, 1, sqlfunc.position(" ", db.History.cmd)), # func.count(db.History.cmd).label("count"), # ) # .group_by(db.History.cmd) # .order_by(desc("count")) # .limit(20) # .all() # ) results = session.query(db.History.cmd).all() counter = Counter([r[0].split(" ")[0] for r in results]).most_common(20) data = {"categories": [c[0] for c in counter], "series": [c[1] for c in counter]} return jsonify(data) @app.route("/trending_json") def trending_json(): session = db.Session() data = None return jsonify(data) @app.route("/type_json") def type_json(): session = db.Session() results = session.query(db.History.type, func.count(db.History.type)).group_by(db.History.type).all() # total = sum(r[1] for r in results) data = [{"name": r[0] or "none", "y": r[1]} for r in sorted(results, key=lambda x: x[1], reverse=True)] return jsonify(data) @app.route("/wordcloud_json") def wordcloud_json(): session = db.Session() results = session.query(db.History.cmd).order_by(func.random()).limit(100) text = " ".join(r[0] for r in results.all()) return jsonify(text) @app.route("/yearly_json") def yearly_json(): session = db.Session() minyear = session.query(extract("year", func.min(db.History.start))).first()[0] maxyear = session.query(extract("year", func.max(db.History.start))).first()[0] results = defaultdict(lambda: 0) results.update( session.query(extract("year", db.History.start).label("year"), func.count("year")).group_by("year").all() ) data = [(year, results[year]) for year in range(minyear, maxyear + 1)] return jsonify(data) PK!shellhistory/cli.py""" Module that contains the command line application. Why does this file exist, and why not put this in __main__? You might be tempted to import things from __main__ later, but that will cause problems: the code will get executed twice: - When you run `python -m shellhistory` python will execute ``__main__.py`` as a script. That means there won't be any ``shellhistory.__main__`` in ``sys.modules``. - When you import __main__ it will get executed again (as a module) because there's no ``shellhistory.__main__`` in ``sys.modules``. Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration """ import argparse from pathlib import Path from . import db def get_parser(): parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("--location", dest="location", action="store_true") group.add_argument("--web", dest="web", action="store_true") group.add_argument("--import", dest="import_file", action="store_true") return parser def main(args=None): parser = get_parser() args = parser.parse_args(args=args) if args.location: return location() elif args.web: return web() elif args.import_file: report = db.update() return 0 def location(): print(Path(__file__).parent / "shellhistory.sh") return 0 def web(): from .app import app app.run() PK!۳tshellhistory/db.pyimport os from base64 import b64decode from collections import namedtuple from datetime import datetime from pathlib import Path from sqlalchemy import ( Column, DateTime, Integer, Interval, String, Text, UnicodeText, UniqueConstraint, create_engine, exc, ) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from tqdm import tqdm DEFAULT_DIR = Path.home() / ".shellhistory" DB_PATH = os.getenv("SHELLHISTORY_DB") HISTFILE_PATH = os.getenv("SHELLHISTORY_FILE") if (DB_PATH is None or HISTFILE_PATH is None) and not DEFAULT_DIR.exists(): DEFAULT_DIR.mkdir() if DB_PATH is None: DB_PATH = DEFAULT_DIR / "db.sqlite3" else: DB_PATH = Path(DB_PATH) if HISTFILE_PATH is None: HISTFILE_PATH = DEFAULT_DIR / "history" else: HISTFILE_PATH = Path(HISTFILE_PATH) Base = declarative_base() engine = create_engine("sqlite:///%s?check_same_thread=False" % DB_PATH) def create_tables(): Base.metadata.create_all(engine) if not DB_PATH.exists(): create_tables() Session = sessionmaker(bind=engine) def get_session(): _engine = create_engine("sqlite:///%s" % DB_PATH) session = sessionmaker(bind=_engine) return session() class History(Base): __tablename__ = "history" __table_args__ = (UniqueConstraint("start", "uuid"), {"useexisting": True}) Tuple = namedtuple("HT", "start stop uuid parents host user tty path shell level type code cmd") id = Column(Integer, primary_key=True) start = Column(DateTime) stop = Column(DateTime) duration = Column(Interval) host = Column(String) user = Column(String) uuid = Column(String) tty = Column(String) parents = Column(Text) shell = Column(String) level = Column(Integer) type = Column(String) code = Column(Integer) path = Column(String) cmd = Column(UnicodeText) def __repr__(self): return "" % (self.path, self.cmd) @staticmethod def line_to_tuple(line): return History.Tuple(*line.split(":", 12)) @staticmethod def tuple_to_db_object(nt): start = datetime.fromtimestamp(float(nt.start) / 1000000.0) stop = datetime.fromtimestamp(float(nt.stop) / 1000000.0) duration = stop - start return History( start=start, stop=stop, duration=duration, host=nt.host, user=nt.user, path=b64decode(nt.path).decode().rstrip("\n"), uuid=nt.uuid, tty=nt.tty, parents=b64decode(nt.parents).decode().rstrip("\n"), shell=nt.shell, level=nt.level, type=nt.type, code=nt.code, cmd=nt.cmd, ) @staticmethod def from_line(line): return History.tuple_to_db_object(History.line_to_tuple(line)) def flush(): session = Session() session.query(History).delete() session.commit() def delete_table(table=History): table.__table__.drop(engine) def yield_db_object_blocks(path, size=512): block = [] with codecs.open(path, encoding="utf-8", errors="ignore") as stream: num_lines = sum(1 for _ in stream) with codecs.open(path, encoding="utf-8", errors="ignore") as stream: current_obj = None for i, line in enumerate(tqdm(stream, total=num_lines, unit="lines"), 1): first_char, line = line[0], line[1:].rstrip("\n") if first_char == ":": # new command if current_obj is not None: block.append(current_obj) current_obj = History.from_line(line) elif first_char == ";": # multi-line command if current_obj is None: continue # orphan line current_obj.cmd += "\n" + line else: # would only happen if file is corrupted raise ValueError("invalid line %s starting with %s" % (i, first_char)) if len(block) == size: yield block block = [] if current_obj is not None: block.append(current_obj) if block: yield block InsertionReport = namedtuple("Report", "inserted duplicates") def insert(obj_list, session, one_by_one=False): if obj_list: if one_by_one: duplicates, inserted = 0, 0 for obj in obj_list: try: session.add(obj) session.commit() inserted += 1 except exc.IntegrityError: session.rollback() duplicates += 1 return InsertionReport(inserted, duplicates) else: session.add_all(obj_list) session.commit() return InsertionReport(len(obj_list), 0) return InsertionReport(0, 0) def import_file(path): session = Session() reports = [] for obj_list in yield_db_object_blocks(path): try: reports.append(insert(obj_list, session)) except exc.IntegrityError: session.rollback() reports.append(insert(obj_list, session, one_by_one=True)) final_report = InsertionReport(sum([r.inserted for r in reports]), sum([r.duplicates for r in reports])) return final_report def import_history(): if not HISTFILE_PATH.exists(): raise ValueError("%s: no such file" % HISTFILE_PATH) return import_file(HISTFILE_PATH) def update(): return import_history() PK! Oshellhistory/shellhistory.sh# FUNCTIONS -------------------------------------------------------------------- if [ -n "${ZSH_VERSION}" ]; then _shellhistory_command_type() { whence -w "$1" | cut -d' ' -f2 } _shellhistory_last_command() { # multi-line commands have prepended ';' (starting at line 2) # shellcheck disable=SC2154 echo "${history[$HISTCMD]}" | sed -e '2,$s/^/;/' } _shellhistory_last_command_number() { # shellcheck disable=SC2086 echo $HISTCMD } elif [ -n "${BASH_VERSION}" ]; then _shellhistory_command_type() { type -t "$1" } _shellhistory_last_command() { # multi-line commands have prepended ';' (starting at line 2) fc -lnr -0 | sed -e '1s/^\t //;2,$s/^/;/' } _shellhistory_last_command_number() { fc -lr -0 | head -n1 | cut -f1 } fi # shellcheck disable=SC2120 _shellhistory_parents() { local list pid list="$(ps -eo pid,ppid,command | tr -s ' ' | sed 's/^ //g')" pid=$$ while [ "${pid}" -ne 0 ]; do echo "${list}" | grep "^${pid} " | cut -d' ' -f3- pid=$(echo "${list}" | grep "^${pid} " | cut -d' ' -f2) done } _shellhistory_time_now() { local now now=$(date '+%s%N') echo "${now:0:-3}" } _shellhistory_start_timer() { _SHELLHISTORY_START_TIME=${_SHELLHISTORY_START_TIME:-$(_shellhistory_time_now)} } _shellhistory_stop_timer() { _SHELLHISTORY_STOP_TIME=$(_shellhistory_time_now) } _shellhistory_set_command() { _SHELLHISTORY_COMMAND="$(_shellhistory_last_command)" } _shellhistory_set_command_type() { # FIXME: what about "VAR=value command do something"? # See https://github.com/Pawamoy/shell-history/issues/13 _SHELLHISTORY_TYPE="$(_shellhistory_command_type "${_SHELLHISTORY_COMMAND%% *}")" } _shellhistory_set_code() { _SHELLHISTORY_CODE=$? } _shellhistory_set_pwd() { _SHELLHISTORY_PWD="${PWD}" _SHELLHISTORY_PWD_B64="$(base64 -w0 <<<"${_SHELLHISTORY_PWD}")" } _shellhistory_can_append() { local last_number # shellcheck disable=SC2086 [ ${_SHELLHISTORY_BEFORE_DONE} -ne 1 ] && return 1 last_number=$(_shellhistory_last_command_number) if [ -n "${_SHELLHISTORY_PREVCMD_NUM}" ]; then # shellcheck disable=SC2086 [ "${last_number}" -eq ${_SHELLHISTORY_PREVCMD_NUM} ] && return 1 _SHELLHISTORY_PREVCMD_NUM=${last_number} else _SHELLHISTORY_PREVCMD_NUM=${last_number} fi } _shellhistory_append() { if _shellhistory_can_append; then _shellhistory_append_to_file fi } _shellhistory_append_to_file() { printf ':%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s:%s\n' \ "${_SHELLHISTORY_START_TIME}" \ "${_SHELLHISTORY_STOP_TIME}" \ "${_SHELLHISTORY_UUID}" \ "${_SHELLHISTORY_PARENTS_B64}" \ "${_SHELLHISTORY_HOSTNAME}" \ "${USER}" \ "${_SHELLHISTORY_TTY}" \ "${_SHELLHISTORY_PWD_B64}" \ "${SHELL}" \ "${SHLVL}" \ "${_SHELLHISTORY_TYPE}" \ "${_SHELLHISTORY_CODE}" \ "${_SHELLHISTORY_COMMAND}" >> "${SHELLHISTORY_FILE}" } _shellhistory_before() { # shellcheck disable=SC2086 [ ${_SHELLHISTORY_BEFORE_DONE} -gt 0 ] && return _shellhistory_set_command _shellhistory_set_command_type _shellhistory_set_pwd _shellhistory_start_timer _SHELLHISTORY_AFTER_DONE=0 _SHELLHISTORY_BEFORE_DONE=1 } _shellhistory_after() { _shellhistory_set_code # must always be done first _shellhistory_stop_timer [ ${_SHELLHISTORY_BEFORE_DONE} -eq 2 ] && _SHELLHISTORY_BEFORE_DONE=0 [ ${_SHELLHISTORY_AFTER_DONE} -eq 1 ] && return _shellhistory_append unset _SHELLHISTORY_START_TIME _SHELLHISTORY_BEFORE_DONE=0 _SHELLHISTORY_AFTER_DONE=1 return ${_SHELLHISTORY_CODE} } _shellhistory_get_debug_trap() { local trap trap="$(trap -p | grep ' DEBUG$')" || return 0 trap=${trap:9} trap=${trap:0:-7} case ${trap} in *';') ;; *) trap+=";" ;; esac echo "${trap}" } _shellhistory_enable() { _SHELLHISTORY_BEFORE_DONE=2 _SHELLHISTORY_AFTER_DONE=1 if [ "${ZSH_VERSION}" ]; then preexec_functions+=(_shellhistory_before) precmd_functions=(_shellhistory_after "${precmd_functions[@]}") elif [ "${BASH_VERSION}" ]; then PROMPT_COMMAND="_shellhistory_after;${PROMPT_COMMAND}" # shellcheck disable=SC2064 trap "$(_shellhistory_get_debug_trap)_shellhistory_before;" DEBUG fi } _shellhistory_disable() { local trap _SHELLHISTORY_AFTER_DONE=1 if [ "${ZSH_VERSION}" ]; then # shellcheck disable=SC2206 preexec_functions=(${preexec_functions:#_shellhistory_before}) # shellcheck disable=SC2206 precmd_functions=(${precmd_functions:#_shellhistory_after}) elif [ "${BASH_VERSION}" ]; then trap="$(_shellhistory_get_debug_trap)" trap=${trap//_shellhistory_before;} if [ -n "${trap}" ]; then # shellcheck disable=SC2064 trap "${trap}" DEBUG else trap - DEBUG fi PROMPT_COMMAND="${PROMPT_COMMAND//_shellhistory_after;}" fi } _shellhistory_usage() { echo "usage: shellhistory " } _shellhistory_help() { _shellhistory_usage echo echo "Commands:" echo " disable disable shellhistory" echo " enable enable shellhistory" echo " help print this help and exit" } # GLOBAL VARIABLES ------------------------------------------------------------- _SHELLHISTORY_CODE= _SHELLHISTORY_COMMAND= _SHELLHISTORY_HOSTNAME="$(hostname)" _SHELLHISTORY_PARENTS="$(_shellhistory_parents)" _SHELLHISTORY_PARENTS_B64="$(echo "${_SHELLHISTORY_PARENTS}" | base64 -w0)" _SHELLHISTORY_PWD= _SHELLHISTORY_PWD_B64= _SHELLHISTORY_START_TIME= _SHELLHISTORY_STOP_TIME= _SHELLHISTORY_TTY="$(tty)" _SHELLHISTORY_TYPE= _SHELLHISTORY_UUID="${_SHELLHISTORY_UUID:-$(uuidgen)}" _SHELLHISTORY_AFTER_DONE= _SHELLHISTORY_BEFORE_DONE= _SHELLHISTORY_PREVCMD_NUM= SHELLHISTORY_FILE="${SHELLHISTORY_FILE:-$HOME/.shellhistory/history}" export SHELLHISTORY_FILE export _SHELLHISTORY_UUID # MAIN COMMAND ----------------------------------------------------------------- shellhistory() { case "$1" in disable) _shellhistory_disable ;; enable) _shellhistory_enable ;; help) _shellhistory_help ;; *) _shellhistory_usage >&2; return 1 ;; esac } PK!*p shellhistory/static/css/home.cssfooter { margin-bottom: 20px; } #sync { min-height: 50px; } #sync-close { font-size: 21px; font-weight: 700; line-height: 1; color: #000; text-shadow: 0 1px 0 #fff; opacity: .2; cursor: pointer; } #sync-close:hover { text-decoration: none; } #welcome-title { /*position: relative;*/ /*left: 40px;*/ text-align: center; } #sync-alert { padding: 15px; margin-bottom: 20px; border: 1px solid transparent; border-radius: 4px; } #sync-alert.danger { color: #a94442; background-color: #f2dede; border-color: #ebccd1; } #sync-alert.info { color: #31708f; background-color: #d9edf7; border-color: #bce8f1; } #sync-alert.success { color: #3c763d; background-color: #dff0d8; border-color: #d6e9c6; } #sync-alert.warning { color: #8a6d3b; background-color: #fcf8e3; border-color: #faebcc; } #sync-alert.default { color: grey; background-color: #f5f5f5; border-color: #e3e3e3; }PK!shellhistory/static/js/codes.js$(document).ready(function () { $.getJSON('/codes_json', function (data) { Highcharts.chart('container', { chart: { plotBackgroundColor: null, plotBorderWidth: null, plotShadow: false, type: 'pie' }, title: { text: 'Commands by exit code' }, tooltip: { pointFormat: '{series.name}: {point.y}' }, legend: {}, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: true, format: '{point.name}: {point.percentage:.1f} %', style: { color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black' } }, showInLegend: true } }, series: [{ name: 'Commands', colorByPoint: true, data: data }] }); }); }); PK!һ.bbshellhistory/static/js/daily.js$(document).ready(function () { $.getJSON('/daily_json', function (data) { Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Daily Commands' }, xAxis: { title: { text: 'Day of the week' }, categories: [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], crosshair: true }, yAxis: { min: 0, title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number of commands', data: data }] }); }); }); PK!]jj'shellhistory/static/js/daily_average.js$(document).ready(function () { $.getJSON('/daily_average_json', function (data) { Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Daily Commands' }, xAxis: { title: { text: 'Day of the week' }, categories: [ 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], crosshair: true }, yAxis: { min: 0, title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number of commands', data: data }] }); }); }); PK!I"shellhistory/static/js/duration.js$(document).ready(function () { $.getJSON('/duration_json', function (data) { Highcharts.chart('container', { title: { text: 'Commands by duration' }, xAxis: { title: { text: 'Command duration in milliseconds' }, type: 'logarithmic', minorTickInterval: 1, plotLines: [{ value: data.average, color: 'orange', width: 2, label: { text: 'Average: ' + data.average + ' milliseconds', align: 'left', style: { color: 'gray' } } }, { value: data.median, color: 'magenta', width: 2, label: { text: 'Median: ' + data.median + ' milliseconds', align: 'left', style: { color: 'gray' } } }] }, yAxis: { title: { text: 'Number of commands' } }, plotOptions: { series: { pointStart: 1 } }, series: [{ name: 'Commands', data: data.series }], }); }); }); PK!X(uaashellhistory/static/js/home.js$(document).ready(function () { $('#sync-close').click(function (e) { $(this).parent().fadeOut(500); }); $("#sync-button").click(function () { $.getJSON('/update', function (data) { $('#sync-message').text(data.message); $('#sync-alert') .removeClass() .addClass(data.class) .show(); }); }); }); PK!P' + this.series.xAxis.categories[this.point.y] + ' to ' + this.series.yAxis.categories[this.point.x] + ': ' + this.point.value; } }, series: [{ name: 'Occurrences', borderWidth: 1, data: data.series, dataLabels: { enabled: true, color: '#000000' } }] }); }); }); PK!X%shellhistory/static/js/markov_full.js$(document).ready(function () { $.getJSON('/markov_full_json', function (data) { Highcharts.chart('container', { chart: { type: 'heatmap', marginTop: 40, marginBottom: 80, plotBorderWidth: 1 }, title: { text: 'Markov chain (full commands)' }, xAxis: { categories: data.xCategories }, yAxis: { categories: data.yCategories, title: null }, colorAxis: { min: 0, minColor: '#FFFFFF', maxColor: Highcharts.getOptions().colors[0] }, legend: { align: 'right', layout: 'vertical', margin: 0, verticalAlign: 'top', y: 25, symbolHeight: 530 }, tooltip: { formatter: function () { return '' + this.series.xAxis.categories[this.point.y] + ' to ' + this.series.yAxis.categories[this.point.x] + ': ' + this.point.value; } }, series: [{ name: 'Occurrences', borderWidth: 1, data: data.series, dataLabels: { enabled: true, color: '#000000' } }] }); }); }); PK!2!shellhistory/static/js/monthly.js$(document).ready(function () { $.getJSON('/monthly_json', function (data) { Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Monthly Commands' }, xAxis: { title: { text: 'Month' }, categories: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], crosshair: true }, yAxis: { min: 0, title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number of commands', data: data }] }); }); }); PK!wq)shellhistory/static/js/monthly_average.js$(document).ready(function () { $.getJSON('/monthly_average_json', function (data) { Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Average Monthly Commands' }, xAxis: { title: { text: 'Month' }, categories: [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], crosshair: true }, yAxis: { min: 0, title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number of commands', data: data }] }); }); }); PK!kq#shellhistory/static/js/over_time.js$(document).ready(function() { $.getJSON('/over_time_json', function(data) { Highcharts.chart('container', { chart: { zoomType: 'x' }, title: { text: 'Commands over time' }, subtitle: { text: document.ontouchstart === undefined ? 'Click and drag in the plot area to zoom in' : 'Pinch the chart to zoom in' }, xAxis: { type: 'datetime' }, yAxis: { min: 0, title: { text: 'Number of commands' } }, legend: { enabled: false }, plotOptions: { area: { fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [ [0, Highcharts.getOptions().colors[0]], [1, Highcharts.Color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] ] }, marker: { radius: 2 }, lineWidth: 1, states: { hover: { lineWidth: 1 } }, threshold: null } }, series: [{ type: 'area', name: 'Commands', data: data }] }); }); }); PK!WAB&shellhistory/static/js/top_commands.js$(document).ready(function () { $.getJSON('/top_commands_json', function (data) { Highcharts.chart('container', { chart: { type: 'bar' }, title: { text: 'Top commands' }, xAxis: { categories: data.categories, min: 0, title: { text: 'Commands' } }, yAxis: { title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number', data: data.series }] }); }); }); PK!&+shellhistory/static/js/top_commands_full.js$(document).ready(function () { $.getJSON('/top_commands_full_json', function (data) { Highcharts.chart('container', { chart: { type: 'bar' }, title: { text: 'Top commands (full lines)' }, xAxis: { categories: data.categories, min: 0, title: { text: 'Commands' } }, yAxis: { title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number', data: data.series }] }); }); }); PK!"shellhistory/static/js/trending.jsPK!qBshellhistory/static/js/type.js$(document).ready(function () { $.getJSON('/type_json', function (data) { Highcharts.chart('container', { chart: { plotBackgroundColor: null, plotBorderWidth: null, plotShadow: false, type: 'pie' }, title: { text: 'Commands by type' }, tooltip: { pointFormat: '{series.name}: {point.y}' }, legend: {}, plotOptions: { pie: { allowPointSelect: true, cursor: 'pointer', dataLabels: { enabled: true, format: '{point.name}: {point.percentage:.1f} %', style: { color: (Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black' } }, showInLegend: true } }, series: [{ name: 'Commands', colorByPoint: true, data: data }] }); }); }); PK!Q>>#shellhistory/static/js/wordcloud.js$(document).ready(function () { $.getJSON('/wordcloud_json', function (text) { var lines = text.split(/[,\. ]+/g), data = Highcharts.reduce(lines, function (arr, word) { var obj = Highcharts.find(arr, function (obj) { return obj.name === word; }); if (obj) { obj.weight += 1; } else { obj = { name: word, weight: 1 }; arr.push(obj); } return arr; }, []); Highcharts.chart('container', { title: { text: null }, series: [{ type: 'wordcloud', data: data, name: 'Occurrences', enableMouseTracking: false }], exporting: { enabled: false }, credits: { enabled: false } }); }); }); PK!  shellhistory/static/js/yearly.js$(document).ready(function () { $.getJSON('/yearly_json', function (data) { Highcharts.chart('container', { chart: { type: 'column' }, title: { text: 'Yearly Commands' }, xAxis: { title: { text: 'Year' }, crosshair: true }, yAxis: { min: 0, title: { text: 'Number of commands' } }, tooltip: { shared: true, useHTML: true }, plotOptions: { column: { pointPadding: 0.2, borderWidth: 0 } }, series: [{ name: 'Number of commands', data: data }] }); }); }); PK!Wp.shellhistory/templates/admin/history_list.html{% extends 'admin/model/list.html' %} {% block main_menu %} {% endblock %}PK!K$$'shellhistory/templates/admin/index.html{% extends 'admin/master.html' %} {% block main_menu %} {% endblock %} {% block body %}

Go back to the home page

{% endblock %} PK!O_!shellhistory/templates/codes.html{% extends "home.html" %} {% block title %}Exit Codes{% endblock %} {% block content %}
{% endblock %} PK!C!shellhistory/templates/daily.html{% extends "home.html" %} {% block title %}Daily Commands{% endblock %} {% block content %}
{% endblock %} PK! )shellhistory/templates/daily_average.html{% extends "home.html" %} {% block title %}Average Daily Commands{% endblock %} {% block content %}
{% endblock %} PK!Z9*[$shellhistory/templates/duration.html{% extends "home.html" %} {% block title %}Commands by duration{% endblock %} {% block content %}
{% endblock %} PK!c>zz shellhistory/templates/home.html {% block head %} {% block title %}Home{% endblock %} {% endblock %}

© Copyright 2017-2019 by Timothée Mazzucotelli. Source code on GitHub.
PK!"shellhistory/templates/hourly.html{% extends "home.html" %} {% block title %}Hourly Commands{% endblock %} {% block content %}
{% endblock %} PK!=*shellhistory/templates/hourly_average.html{% extends "home.html" %} {% block title %}Average Hourly Commands{% endblock %} {% block content %}
{% endblock %} PK!ݶ"shellhistory/templates/length.html{% extends "home.html" %} {% block title %}Commands by Length{% endblock %} {% block content %}
{% endblock %} PK!^͸??"shellhistory/templates/markov.html{% extends "home.html" %} {% block title %}Markov chains{% endblock %} {% block content %}
{% endblock %} PK!ZTT'shellhistory/templates/markov_full.html{% extends "home.html" %} {% block title %}Markov chains (full commands){% endblock %} {% block content %}
{% endblock %} PK!"#shellhistory/templates/monthly.html{% extends "home.html" %} {% block title %}Monthly Commands{% endblock %} {% block content %}
{% endblock %} PK!.  +shellhistory/templates/monthly_average.html{% extends "home.html" %} {% block title %}Average Monthly Commands{% endblock %} {% block content %}
{% endblock %} PK!^%%shellhistory/templates/over_time.html{% extends "home.html" %} {% block title %}Commands over time{% endblock %} {% block content %}
{% endblock %} PK!Y(shellhistory/templates/top_commands.html{% extends "home.html" %} {% block title %}Top commands{% endblock %} {% block content %}
{% endblock %} PK!6TxK  -shellhistory/templates/top_commands_full.html{% extends "home.html" %} {% block title %}Top commands (full lines){% endblock %} {% block content %}
{% endblock %} PK!P$shellhistory/templates/trending.html{% extends "home.html" %} {% block title %}Trending Commands{% endblock %} {% block content %}
{% endblock %} PK!S ! shellhistory/templates/type.html{% extends "home.html" %} {% block title %}Commands by Type{% endblock %} {% block content %}
{% endblock %} PK!""shellhistory/templates/yearly.html{% extends "home.html" %} {% block title %}Yearly commands{% endblock %} {% block content %}
{% endblock %} PK!H~H-shellhistory-0.2.3.dist-info/entry_points.txtN+I/N.,()*H,./MɴE X&f*ON,T AU_(PK!#|$shellhistory-0.2.3.dist-info/LICENSEISC License Copyright (c) 2017-2019, Timothée Mazzucotelli Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. PK!HڽTU"shellhistory-0.2.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!HL"%shellhistory-0.2.3.dist-info/METADATAYZFxfJW29 C3nIAR+jƳg۷%c'0귪jyŽwR[9Dt"4Rԙ5ݹӀwR<_XHv"GI8'*^%UUaNzLM{Fn|&#k8:pGL'ur_S-;nv*Qee/kK Rd3쉌4`(2?Υ`uAʵR@t' E"^sQQɳL#vQMV+|OL0[^Ac[Κ>wKQ"|Z[ф 텻I3"g\ʡQ) o@+zZ9z]&! {3tNq!)dvo3QIroa `+.蚪֊17R5QҥOۗFLyPn?O޼>WM/y4#pG|n)s(u;i,鴓z@03wǸrG6JU)fm2=EΚu0wvyp f,0nldO#NdǛx^Ai߹=(T#8cB!U [b `Ɣ.R>Rպes ׃2jF$B!LDG|Ј)+xl.q\DBkءMɽax%Kl-rf=Dgj:a's Cb.i&`sT$* !fc[vsÞ{lwU/o$¼ɽTE|{y?A>{t~ 56pnG [-'#gƎWdGMm].|q.ƫ稵UJv)Din**" A*!PƍU t >(s q0CsBj=g(1 h\-Za( 8|FE'0uWs4C쿼LkoH2It4J Sɽ1b͋2͜S 0qd5I9u% _v Asbd$a wW[䂨l5GB)Q ^ aǩ}vt?cy0H2 xV *Pbըo!a՘uOq/Tt}T25U"˷F|ׯM32H |Ċ ;b?apW R-.iz,,K> ϧMQx+$&b )dr1^&:ꈉѐʗp B"@d%pҝ~~ UJpu|%5cuYB-s1TdKkن-tlG@L+D2+sSTY_51Fzf鼒"E1lkk3@#7y)/eVw]VUS O!ސ>}5a)c,LOBkRB>k= ݂F8@F m4JQXV#˂&41;fҋɆ? 9Ȩ̦;oF?R#${}fG9QDQ(H1  ,>V2vHb *˺Q9S{t-p栿`}{{?/tlm_x;/{|]xb烍l"su cg/7dq|8pF٠W*~ӅkۓO7e>] ǯɻ遝l}qX;?h~s y/0ivGI<|u}c_V*Z{%ZKW~O~. 3'w!#{!vwlj!QE{g~:N#^UUv? QI+܊G:a-q}yp!eyX0:ջ.VwCCḟh8))GljLJ @1mPc$'gd+!؍y$`k[+{fXۢpm~xU8Unk/cم9Z8{dSO#9tBu!v[6}{o\ו7T*H NGklm`7Ӵ[3ϱ-*a;Eۘ}@/XyV_ǝ `ݾ۱dC'i;K<8WO*D훷{SYv\*"FPG>4b#/Mt̳OKYCw &E4C!>$eZz})-D86k+UF~!w`qM>Grӛ"F L>=13DzFQA$fše C}*)"r*-t aC[⤄VNVwID3betY`Sl丟p:khiPHZe+}ae"\5E]T.ڤi==nN;n}^4]rqzيF|"҈-͈YP%mVQRDɝf\%elVh+nN8z.o ڔAMM>/w/!庚fo#ʷS*mw}grP-U۲!Μh*hh(G:E`|ڔOuN}b!!+eSs@=eaDqqm#M%dg[a~16&,KF*UɘJC(z)9u՜󔜛v` d84#7ؾpM"lРpa-%!@yj>g"+C|jҏJfw;٥H؋\<6 [:L/PmԖ0`ٓELܵo (o !I0mܜ^,JSԨ{Y麠H ":͗ψ nNί~Y5_%PK!H07 _#shellhistory-0.2.3.dist-info/RECORDhQ7Xa# NjZF9<ͩ[ć1?A\5LM(V+jZV@G, N<΢`xx8,1 XJn(y*Gvl"$*$퇺{{^Z5JpdX>Ry0F#2F{$FJ?>ѴKn$iԀH%INi c\Ez_N7AՀ๹abɬ!~ ӌt|B t3rJ^FF{ˆ(ru f5Dt.Kw|a "ǁU~y,0y@ɳ|ƛSAyj; G0#60>{zGC)fq t4x=m:f?Ày6* Lǂpa#w AF[ {4n]tzD;#*_tZ|G!+lsrBCY elLځ჋7ȥ d4!!1ko2݌`ԙʾy=V}[TQɶ@!W:a|É \V3^ا۪\w_Ki&0{¡F9m] C MOvF8L}cQl"k<(Jc}`r(֏yڋsXՐ|*oO%3i">h lnl@3t΢$gڝC+O?|7j?ڌ?eMdW\z}ϯP7^_[R$.lꆮyv Nʧ.'tJew'x˙ҿ)9OxFTU\T$mݶWK#8tQU!>mo<ZU l OZQz1Gs8*':]nUU}p5q`X;aJn7]`T+2@gAo *`h@`fQ~Xb}KKU0P"H9)g\[򤉩몘@MTu5_U=>uN, 0yuZ f,=_bN?` ~DŽ5Hj*x_AoٗaQ߷fE1%ʩ=(1ưW^򰢸x6iwé3B)eSӎ>\dO l؆lc@ 46:>0J~Oco1T{\ILcrܖ=9҂NIW~LapKw4Og*(XA֎zE[VB?I)^'Qa[{wD}OG:]vh \IPwTI'u•zVO#;nO2 Ͳr'u 0 'gBh$x{0I:OM2#T1}+\C|ݕ[xSeI > MQ;#2U(68ˎ{^v'cidN'Ϥ؋{2 ǃN<-@[U5o.˙I$زiҀӭT/g*e[6AfZ^>Q;)5]W![ؽ`R ~kL7qQ'"'h?ߖ!Rk>IZUx食dcw%\9PҬE*< G#/7 y ; {T 6uZ >v@6pvt[t9,z"A(}-{?5 xXrJ{> f+="<Ԩ*/5ҢkG>WX֓z#ߑ`i_J@/G]K i"}A$@hlƿ>#shellhistory/static/js/wordcloud.jsPK!  shellhistory/static/js/yearly.jsPK!Wp.shellhistory/templates/admin/history_list.htmlPK!K$$'$shellhistory/templates/admin/index.htmlPK!O_!shellhistory/templates/codes.htmlPK!C!shellhistory/templates/daily.htmlPK! )shellhistory/templates/daily_average.htmlPK!Z9*[$@shellhistory/templates/duration.htmlPK!c>zz shellhistory/templates/home.htmlPK!"9shellhistory/templates/hourly.htmlPK!=*qshellhistory/templates/hourly_average.htmlPK!ݶ"shellhistory/templates/length.htmlPK!^͸??"shellhistory/templates/markov.htmlPK!ZTT'{shellhistory/templates/markov_full.htmlPK!"#shellhistory/templates/monthly.htmlPK!.  +Oshellhistory/templates/monthly_average.htmlPK!^%%shellhistory/templates/over_time.htmlPK!Y(shellhistory/templates/top_commands.htmlPK!6TxK  -$shellhistory/templates/top_commands_full.htmlPK!P$|shellhistory/templates/trending.htmlPK!S ! shellhistory/templates/type.htmlPK!""shellhistory/templates/yearly.htmlPK!H~H-'shellhistory-0.2.3.dist-info/entry_points.txtPK!#|$shellhistory-0.2.3.dist-info/LICENSEPK!HڽTU"shellhistory-0.2.3.dist-info/WHEELPK!HL"%shellhistory-0.2.3.dist-info/METADATAPK!H07 _#shellhistory-0.2.3.dist-info/RECORDPK55x