Whenever you need to be able to reproduce something from shell sessions, it can be very beneficial to have a full log of the input and output of your terminal session. The built-in tool ‘script’ can easily create a fully replayable transcript of your session that can easily be replayed with ‘scriptreplay’; this might be sufficient for some people in some cases. Personally, I don’t use the replay functionality and am merely interested in the input and output most of the time and find it tedious and easy to forget to manually start script for each terminal that I open (which counts up rapidly during a session). So, let’s find a way to optimize and automate this!
Fixing the prompt
Your prompt is controlled by the PS1 environment variable which is set in your ~/.bashrc. Before you do anything to this file, it is probably a good idea to open a second terminal or graphical editor so you can still fix it if you break something. Otherwise, all your subsequent shells will be troubled by the same mistake and there is no way to fix it. Also, please back it up, just to be sure.
Here is the default for a Ubuntu 19.10 installation:
if [ "$color_prompt" = yes ]; then
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi
As you see here, there is a variable called $color_prompt that determines if there will be color coding characters in your prompt. Having colours in your prompt looks nice and is actually useful in normal day-to-day use, but it is problematic for raw transcripts of a terminal session because there will be color coding characters all over the place. Of course, you could also opt to sed them out afterwards, but avoiding them in the first place makes your transcripts smaller and easier to quickly reference from the command line.
While we could turn off these colours by using this variable, we want to customize our prompt even further, so instead of doing that, we leave the original prompt alone and create one from scratch.
The ‘${debian_chroot:+($debian_chroot)}’ part is a shorthand notation for: if there is a $debian_chroot variable, add it between parenthesis, otherwise, don’t do anything. So, if you are in a chroot environment (a (very insecure) way to isolate a process and simulate a different root folder), you can tell from the prompt.
The other interesting parts we see here are \u (the current user), \h (the hostname) and \w (the working directory).
Let’s start by storing this default prompt in a separate variable, so we can always go back to it by executing ‘PS1=$DEFAULT_PROMPT’ from a shell, should we so desire:
# Add this at the bottom of your .bashrc
DEFAULT_PROMPT=$PS1
PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
We can now modify PS1 to make it match our needs.
First, I think it’s always handy to have a timestamp when scanning logfiles. This can be easily achieved by adding the \t (24 hour format) or \T (12 hour format) to your prompt:
PS1='${debian_chroot:+($debian_chroot)}[\t] \u@\h:\w\$ '
Another thing I like to add is the shell depth. While this is not so much useful for the logfiles themselves, it comes in handy while you’re actually using the shell, because you can tell whether or not you’re in a subshell (script spawns a Bash subshell, more about that later). The shell depth is stored in the $SHLVL environment variable, and as such can be added to your prompt easily:
PS1='${debian_chroot:+($debian_chroot)}[\t] [$SHLVL] \u@\h:\w\$ '
Now, whenever you see a number higher than 1 at this position, you know you’re probably in a script session.
I think this drastically improves the readability of the logfiles generated by script. Let’s move on to that part!
Automating script
It would be really useful if each and every shell that you spawn, be it interactive or remotely, can be set up to be logged automatically. To make things happen on shell start, you have several options in Bash.
In your home directory, you may find a .profile, a .bash_profile or none of these at all. If a .bash_profile exists, the .profile (which is a non-Bash-specific version) is ignored. These files are executed only for login shells and not when interactively spawning shells, which makes them useless for what I’m trying to achieve, but may suffice for your own purposes. Instead, I will be focussing on the .bashrc file again, which is executed for each login and interactive shell, making it the ideal candidate for my script automation.
Unfortunately, simply adding a line like this…
# End of .bashrc
script
… at the end of your .bashrc will cause an infinite loop when you login, because script spawns its own Bash subshell, which in turn executes .bashrc, which in turn calls script, etc.
Fortunately, we already learned that $SHLVL contains the shell depth, so we can use that to avoid this recursion:
# End of .bashrc
if [ $SHLVL -eq 1 ]; then
script
fi
Now, your session will only be logged automatically whenever you spawn a shell or login from a remote machine. Any subshells spawned will not trigger the script utility. Nice!
Making it optional
Of course, sometimes you just want a shell without it getting logged, so let’s not force this behaviour upon every shell. Instead, let’s make this optional by asking the user to type a short name for the script or allow the user to discard this functionality altogether by just hitting enter once upon login to be minimally intrusive. Let’s create a folder ~/.scriptlogs with a separate Bash script called ask-session-log.sh:
#!/bin/bash
echo "(enter to ignore)"
read -p "To log this session, type a descriptive short name: " session_name
# We prefer underscores rather than whitespace
session_name=${session_name// /_}
if [ ${#session_name} -gt 0 ]; then
script $session_name.log
fi
And use this in your .bashrc instead:
# End of .bashrc
if [ $SHLVL -eq 1 ]; then
~/.scriptlogs/ask-session-log.sh
fi
So, each time you spawn a shell, you will be asked whether you’d like to start logging it. To quickly ignore the functionality, you just hit enter; otherwise you supply a descriptive name that you can easily reference later.
Organizing your logs
Now, this is all fine and dandy, but this will quickly fill up your home folder with transcripts from terminal sessions, which both clutters your home folder and makes it hard to find logs afterwards. So, let’s not do that.
Instead, let’s create another script that organizes our logs into a folder per day, placed in ~/.scriptlogs as well, called create-session-log.sh:
#!/bin/bash
DAYFOLDER=$(date +%Y-%m-%d)
FULLPATH=~/.scriptlogs/$DAYFOLDER
if [ ! -d $FULLPATH ]; then
mkdir -p $FULLPATH
fi
script $FULLPATH/$1.log
This script takes a single argument $1 which represents the name of the file you want to log to. It creates a folder per day as a subfolder of .scriptlogs and places your new log file there.
All that’s left to do, is actually call this script from our ask-session-log.sh:
# Rest of ask-session-log.sh omitted for brevity
if [ ${#session_name} -gt 0 ]; then
~/.scriptlogs/create-session-log.sh $session_name
fi
Conclusion
Now, you can easily reference log files in the future as they are organized by date and named according to a descriptive name you provided when starting the shell. You can also omit logging altogether if you so desire, all in a minimally intrusive automation.
The logfiles themselves are relatively legible as they are not cluttered with any colour coding characters (in your prompt at least, other colourful aliases might exist you’d like to optimize), but there are control characters in there which make it a little bit harder to read. Fortunately, ‘less -R’ will handle these nicely so you could use that to read your logfiles in the future. It is also an option to extend these scripts with some ‘sed’ invocations to strip them out, but I felt that was out of scope now.
A word of advice: don’t use things like man, top and anything else that rewrites your screen, as this seriously reduces the legibility of your logfile. Spawn another shell to do these things.
Another thing I personally do is securely copy my logfiles to a remote location once my session is over, but whether that’s useful to you entirely depends on your use case.
Enjoy!