See https://gist.github.com/stilist/dd88f71557c5231d374ce36645a065de for previous context.
Last active
April 24, 2020 04:47
-
-
Save stilist/4d56d0ddbe1786ba8cf6375b3f8f5360 to your computer and use it in GitHub Desktop.
Accurately count the number of lines from PS1, PS2, and the command the user entered.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# Return the rows printed in the terminal since the previous command was | |
# executed -- PS1, PS2, and the command that was executed. PS1 and the command | |
# can each can span one or more lines, and PS2 is interpolated into the command | |
# if relevant. | |
# | |
# @note This strips out ANSI escape sequences. | |
lines_from_prompt_and_command() { | |
# `history 1` prints a command with some additional information: a whitespace- | |
# padded sequence number for the command's index in the `history`, and (if | |
# set), the result of passing `$HISTTIMEFORMAT` to `strftime`. | |
# | |
# The default `$HISTTIMEFORMAT`, which isn't set, will result in something | |
# like ` 53 man read`. For a custom format like | |
# `HISTTIMEFORMAT="[%FT%T%z]%_*"`, `history 1` might return something | |
# like ` 53 [2020-04-23T13:59:42-0700]*man read`. | |
local command | |
# This `sed` attempts to find static characters in `$HISTTIMEFORMAT` by | |
# stripping out everything prefixed with a `%` (indicating a date-time | |
# formatting token passed to `strftime`). It will produce an empty string if | |
# `HISTTIMEFORMAT` isn't set, or ends with a `%` formatting token. | |
static_histtimeformat="$(echo "${HISTTIMEFORMAT}" | sed -E 's/^.*%[[:alnum:]_]//')" | |
# If `static_histtimeformat` is a non-empty string, `cut` can easily strip | |
# out everything prior to the executed command, implicitly also stripping | |
# out the sequence number. | |
if [ -n "${static_histtimeformat}" ] ; then | |
command="$(history 1 | cut -d "${static_histtimeformat}" -f 2-)" | |
# Otherwise, simply strip out the sequence number. | |
else | |
# Using `\s+\d+\s+` as the pattern didn't seem to work, but it does work | |
# using character classes. | |
command="$(history 1 | sed -E 's/[[:space:]]+[[:digit:]]+[[:space:]]+//')" | |
fi | |
# Evaluate PS1 and PS2 to get an accurate view of how many lines they span. | |
# The `@P` operator was added in Bash 4.4. | |
# | |
# @see https://stackoverflow.com/a/37137981/672403 | |
# @see https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html | |
expanded_PS1="$(printf '%s' "${PS1@P}")" | |
expanded_PS2="$(printf '%s' "${PS2@P}")" | |
# If the command spanned multiple lines (due to newlines, not soft-wrapping) | |
# put `$PS2_PROMPT` at the start of every line beginning with the second | |
# line, to match how things appear in the shell. | |
# | |
# @see https://gist.github.com/JPvRiel/b337dfee8f273aac1332447ed1342304 | |
command_with_ps2="${command/$'\n'/$'\n'${expanded_PS2}}" | |
all_lines="${expanded_PS1}${command_with_ps2}" | |
# Strip ANSI escape sequences. | |
# | |
# `\x1B` is decimal 27, the escape key (`\e`), so this matches any sequence | |
# that begins with `\e[` followed by a digit, `;`, or letter. | |
# | |
# @see https://stackoverflow.com/a/43627833/672403 | |
sanitized="$(echo "${all_lines}" | sed $'s,\x1B\[[0-9;]*[a-zA-Z],,g')" | |
echo "${sanitized}" | |
} | |
count_lines_after_timestamp_placeholder() { | |
# It's important to know how many lines `$TIMESTAMP_PLACEHOLDER` was printed | |
# before the line where the user enters commands, because that's how many | |
# lines backwards `move_cursor_to_start_of_ps1` will need to move to | |
# overwrite `$TIMESTAMP_PLACEHOLDER` with `$TIMESTAMP`. | |
# | |
# `$TIMESTAMP_PLACEHOLDER` may not be on the first line of PS1. This `perl` | |
# snippets removes any newlines before `$TIMESTAMP_PLACEHOLDER` to compensate | |
# for this. | |
relevant_lines="$(lines_from_prompt_and_command | perl -pe "s/^\s+//")" | |
# Count the rows consumed by PS1 + command, including lines that are so long | |
# the terminal soft-wraps them to multiple rows (e.g. if PS1 includes `\w` | |
# and you're in a deeply-nested directory). | |
# | |
# @see https://unix.stackexchange.com/a/275797 | |
total_rows=0 | |
while IFS=$'\n' read -r line ; do | |
line_length="${#line}" | |
# Bash arithmetic rounds down; this is a trick to fake rounding up. | |
# | |
# @see https://stackoverflow.com/a/2395294/672403 | |
(( line_rows = (line_length + COLUMNS - 1) / COLUMNS )) | |
(( total_rows += line_rows )) | |
done <<< "${relevant_lines}" | |
echo "${total_rows}" | |
} | |
# @see https://redandblack.io/blog/2020/bash-prompt-with-updating-time/ | |
move_cursor_to_start_of_ps1() { | |
tput cuu "$(count_lines_after_timestamp_placeholder)" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The 'minified' version that puts all the code back into
move_cursor_to_start_of_ps1
, removes the comments, and removes the$HISTTIMEFORMAT
handling: