Last active
February 25, 2024 01:10
-
-
Save cheater/3304da6741186f623635a575a51c1cc8 to your computer and use it in GitHub Desktop.
turn any command into a prompt
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
#!/bin/bash | |
# USAGE | |
# | |
# If you have to run the same program over and over with the last few arguments | |
# always changing, you can use this command like this: | |
# | |
# prompt myprog --arg1 -arg2 arg3 "this is arg 4" | |
# | |
# A prompt will show up which you can exit using Ctrl-D. | |
# | |
# You can add multiple arguments per each prompt line. Spaces always separate | |
# arguments unless you surround them by single or double quotes. | |
# | |
# Lines entered into the prompt will be stored into a history name derived from | |
# the supplied command complete with its arguments, and ending with the | |
# .history file extension. Lines that result in errors (non-zero exit codes) | |
# from the command will also be added to a file named the same, but which ends | |
# in .errors instead of ending in .history. The .errors file logs all errors, | |
# even for prompts that start with a space. | |
# | |
# You can call the program multiple times with one prompt line by using the | |
# semicolon (;). For example, this prompt: | |
# | |
# myprog -a> foo; bar | |
# | |
# will invoke myprog -a foo, then myprog -a bar, and then log "foo; bar" into | |
# the .history file. If either invokation of myprog returns an error, the whole | |
# prompt "foo; bar" will be logged to the .errors file. It does not matter that | |
# the part that contained "bar" starts with a space (located right after the | |
# semicolon). Whether or not a line gets logged to .history only depends on | |
# whether the whole prompt starts with a space. | |
# | |
# If you wish to use a literal semicolon, you have to escape it using a | |
# backslash, like so: foo\; bar. | |
# | |
# Semicolons are not observed within quotes (both single and double quotes). | |
# | |
# You can start comments with # and they will continue either until the end of | |
# line or until the next semicolon which is not escaped with a backslash and | |
# not within quotes. | |
# | |
# Please set the disallowed variable depending on whether you're on Linux, | |
# Windows, or some other operating system. | |
# Set characters not allowed within a file name on your platform. If you want | |
# to use a backslash (\), make sure to escape it (\\), because it'll be used by | |
# the tr command, which needs the backslash escaped. | |
disallowed='<>:"/\\|?' # for Windows. | |
if [[ "$(uname -s)" == "Linux" ]]; then | |
disallowed='/' # for Linux | |
fi | |
# If you're on another platform, set the variable here: | |
# disallowed='/' | |
# The file name will have disallowed characters replaced with underscores and | |
# will end in .history. Prompt lines that result in an error will also be added | |
# to a file that is named the same, but ends in .errors instead of .history. | |
this_histfile_base="$(printf '%s' "$*" | tr "$disallowed" _)" | |
this_histfile="$this_histfile_base"".history" | |
this_errorfile="$this_histfile_base"".errors" | |
# Is debugging turned on? This will print some additional testing information. | |
# Set to 1 to turn on, set to 0 to turn off. | |
dbg=0 | |
# Save the args passed to this program in a new array that is globally | |
# accessible, also from within functions, as $@ is used not only for holding | |
# the arguments to the program but it also holds the arguments to a function: | |
declare -a prompt_args=("$@") | |
# This variable holds the information about whether the last command line has | |
# spawned a command that returned with an error (return value > 0) | |
error=0 | |
# Helper function. I think this one isn't being used any more. | |
# Usage: join joinchar [arg ...] | |
join() { | |
local IFS="$1" | |
shift | |
printf '%s\n' "$*" | |
} | |
# The command line parser: split on separator but not within quotes or when | |
# escaped; allow comments that end on separator. | |
# | |
# The escape character is \, the separator is ;. | |
# | |
# | |
# | |
# States: | |
# | |
# normal | |
# comment (starts with # and goes on until separator (;) or EOL) | |
# after-separator (for skipping whitespace after ;) | |
# single-quoted (with ') | |
# double-quoted (with ") | |
# escape-on-first-character (for commands) | |
# escape-normal | |
# escape-single-quoted | |
# escape-double-quoted | |
# | |
# | |
# | |
# Allowed actions within states - transitions are not mentioned, they're | |
# documented further below - here we only mention actions that don't change the | |
# current state: | |
# | |
# normal: | |
# - encounter a normal non-space character and add it to current entry in the | |
# args array being built (the args array is the accumulator for the parser) | |
# - encounter a space and finish current entry in args array and start a new, | |
# empty one | |
# | |
# comment: | |
# - encounter any non-special character and skip it | |
# | |
# after-separator: | |
# - encounter a whitespace character and skip it | |
# | |
# single-quoted: | |
# - encounter any character, space or non-space, and add it to the current | |
# argument being built | |
# | |
# double-quoted: | |
# - encounter any character, space or non-space, and add it to the current | |
# argument being built | |
# | |
# | |
# | |
# Allowed state transitions: | |
# | |
# Trigger Transition | |
# | |
# # normal -> comment | |
# ; normal -> after-separator | |
# \ normal -> escape-on-first-character | |
# \ normal -> escape-normal | |
# ' normal -> single-quoted (set continuation to "normal") | |
# " normal -> double-quoted (set continuation to "normal") | |
# | |
# ; comment -> after-separator | |
# \ comment -> escape-comment | |
# ' comment -> single-quoted (set continuation to "comment") | |
# " comment -> double-quoted (set continuation to "comment") | |
# | |
# # after-separator -> comment | |
# ; after-separator -> after-separator | |
# \ after-separator -> escape-on-first-character | |
# ' after-separator -> singe-quoted (set continuation to "normal") | |
# " after-separator -> double-quoted (set continuation to "normal") | |
# any other non-whitespace after-separator -> normal | |
# | |
# \ single-quoted -> escape-single-quoted | |
# ' single-quoted -> normal/comment (depending on continuation) | |
# | |
# \ double-quoted -> escape-double-quoted | |
# " double-quoted -> normal/comment (depending on continuation) | |
# | |
# any escape-normal -> normal | |
# | |
# any escape-comment -> comment | |
# | |
# any escape-single-quoted -> single-quoted | |
# | |
# any escape-double-quoted -> double-quoted | |
# | |
# (at end of input) EOL - note this is not represented within the | |
# state loop, it's performed after the loop. | |
separator=';' | |
parser() { | |
local line="$1" | |
line_len=${#line} | |
state="normal" | |
declare -a args=() | |
current_arg="" | |
quoted_str="" | |
continuation="" | |
# Iterate over every character of the input line | |
for ((i = 0; i < line_len; i++)); do | |
char="${line:i:1}" | |
if [ "$dbg" -gt 0 ]; then | |
echo "------" | |
echo "i: $i" | |
echo "char: $char" | |
echo "state: $state" | |
echo "continuation: $continuation" | |
echo "current_arg: $current_arg" | |
echo "quoted_str: $quoted_str" | |
echo "args: ${args[@]}" | |
fi | |
case "$state" in | |
normal) | |
case "$char" in | |
'#') # -> comment | |
# We've encountered the start of a comment. Finish the current | |
# argument in the argument list (accumulator) and switch to comment | |
# state. | |
if [ -n "$current_arg" ]; then # if $current_arg is not an empty string | |
args+=("$current_arg") | |
current_arg="" | |
fi | |
state="comment" | |
;; | |
';') # -> after-separator | |
# We've found a separator in normal mode; finish off the current | |
# argument, run the command with the current args, empty the | |
# accumulator, and continue parsing the next subline. | |
if [ -n "$current_arg" ]; then # if $current_arg is not an empty string | |
args+=("$current_arg") | |
current_arg="" | |
fi | |
[ "$dbg" -gt 0 ] && echo "about to execute 1" | |
# Execute the command | |
if [ ${#args[@]} -gt 0 ]; then # if the args array is non-empty | |
"${prompt_args[@]}" "${args[@]}" | |
if [ "$?" -gt 0 ]; then | |
error=1 | |
fi | |
printf '\n' | |
fi | |
args=() | |
state="after-separator" | |
;; | |
'\') # -> escape-normal | |
# We've found an escape character. The next character will not | |
# change modes. | |
state="escape-normal" | |
;; | |
"'") # -> single-quoted | |
# We've found a single quote | |
continuation="normal" | |
state="single-quoted" | |
;; | |
'"') # -> double-quoted | |
# We've found a double quote | |
continuation="normal" | |
state="double-quoted" | |
;; | |
' ') | |
# We've encountered a space. Finish the current arg and start a new | |
# one. | |
if [ -n "$current_arg" ]; then # if $current_arg is not an empty string | |
args+=("$current_arg") | |
current_arg="" | |
fi | |
;; | |
*) | |
# Any other normal char - just add it to the end of the | |
# accumulator. | |
current_arg="$current_arg""$char" | |
esac | |
;; | |
comment) | |
case "$char" in | |
';') # -> after-separator | |
# We've found a separator in comment mode; run the command with the | |
# current args, empty the accumulator, and continue parsing the | |
# next subline. | |
[ "$dbg" -gt 0 ] && echo "about to execute 2" | |
# Execute the command | |
if [ ${#args[@]} -gt 0 ]; then # if the args array is non-empty | |
"${prompt_args[@]}" "${args[@]}" | |
if [ "$?" -gt 0 ]; then | |
error=1 | |
fi | |
printf '\n' | |
fi | |
args=() | |
state="after-separator" | |
;; | |
'\') # -> escape-comment | |
# We've found an escape character. The next character will not | |
# change modes. | |
state="escape-comment" | |
;; | |
"'") # -> single-quoted | |
# We've found a single quote | |
continuation="comment" | |
state="single-quoted" | |
;; | |
'"') # -> double-quoted | |
# We've found a double quote | |
continuation="comment" | |
state="double-quoted" | |
;; | |
*) | |
# Do nothing, just continue parsing. | |
esac | |
;; | |
after-separator) | |
case "$char" in | |
' ') | |
# We're skipping leading whitespace of a sub-line after the | |
# separator (;) has been parsed. | |
# Do nothing, just continue parsing. | |
;; | |
'#') # -> comment | |
current_arg="" | |
state="comment" | |
;; | |
';') # -> after-separator | |
# We've encountered another separator. | |
# Do nothing, just continue parsing. | |
;; | |
"'") # -> single-quoted | |
# We've found a single quote | |
continuation="normal" | |
state="single-quoted" | |
;; | |
'"') # -> double-quoted | |
# We've found a double quote | |
continuation="normal" | |
state="double-quoted" | |
;; | |
*) # -> normal | |
# We've found a non-space, non-special character. Change to normal | |
# state and start building an argument for the args array. | |
state="normal" | |
current_arg="$char" # Note there may not be any other input into | |
# the argument before the first normal | |
# character that's after a separator. | |
esac | |
;; | |
single-quoted) | |
# We're in single-quoted mode. We don't recognize the separator, only | |
# quotes and escaped quotes | |
case "$char" in | |
'\') # -> escape-single-quoted | |
state="escape-single-quoted" | |
;; | |
"'") # -> normal / comment (depending on continuation) | |
# We've found a single quote | |
case "$continuation" in | |
normal) | |
continuation="" | |
current_arg="$current_arg""$quoted_str" | |
quoted_str="" | |
state="normal" | |
;; | |
comment) | |
continuation="" | |
quoted_str="" | |
state="comment" | |
;; | |
*) | |
echo "Erroneous continuation at end of single-quoted string: $continuation; exiting." >&2 | |
exit 128 | |
esac | |
;; | |
*) | |
# Any other normal char - just add it to the end of the | |
# accumulator. | |
quoted_str="$quoted_str""$char" | |
esac | |
;; | |
double-quoted) | |
# We're in double-quoted mode. We don't recognize the separator, only | |
# quotes and escaped quotes | |
case "$char" in | |
'\') # -> escape-double-quoted | |
state="escape-double-quoted" | |
;; | |
'"') # -> normal / comment (depending on continuation) | |
# We've found a double quote | |
case "$continuation" in | |
normal) | |
continuation="" | |
current_arg="$current_arg""$quoted_str" | |
quoted_str="" | |
state="normal" | |
;; | |
comment) | |
continuation="" | |
quoted_str="" | |
state="comment" | |
;; | |
*) | |
echo "Erroneous continuation at end of double-quoted string: $continuation; exiting." >&2 | |
exit 128 | |
esac | |
;; | |
*) | |
# Any other normal char - just add it to the end of the | |
# accumulator. | |
quoted_str="$quoted_str""$char" | |
esac | |
;; | |
escape-normal) | |
case "$char" in | |
'#'|';'|'\'|"'"|'"') # -> normal | |
current_arg="$current_arg""$char" | |
state="normal" | |
;; | |
*) # -> normal | |
# Failed escape - the escape character wasn't followed by an | |
# escapable character, so treat them both literally. | |
current_arg="$current_arg"'\'"$char" | |
state="normal" | |
esac | |
;; | |
escape-comment) | |
# We're in comment mode, and the last char was an escape character. | |
case "$char" in | |
# We don't need to special case on escapable characters and | |
# non-escapable characters since it doens't matter - in either case, | |
# the characters aren't making it into any sort of string we're | |
# building up: | |
# | |
# '#'|';'|'\'|"'"|'"') # -> comment | |
# # Do nothing, just go back to the comment state. | |
# state="comment" | |
# ;; | |
*) # -> comment | |
# Do nothing, just go back to the comment state. | |
# In fact, we don't even need this case block, but we just included | |
# it so it loks the same as all the other code around it. | |
state="comment" | |
esac | |
;; | |
escape-single-quoted) | |
# We're in single-quoted mode, and the last char was an escape | |
# character (the backslash \). | |
case "$char" in | |
"'") # -> single-quoted | |
# We've found an escaped single quote - add it to the end of the | |
# accumulator. | |
quoted_str="$quoted_str""$char" | |
state="single-quoted" | |
;; | |
*) # -> single-quoted | |
# Failed escape - the escape character wasn't followed by an | |
# escapable character, so treat them both literally. | |
quoted_str="$quoted_str"'\'"$char" | |
state="single-quoted" | |
esac | |
;; | |
escape-double-quoted) | |
# We're in double-quoted mode, and the last char was an escape | |
# character (the backslash \). | |
case "$char" in | |
'"') # -> double-quoted | |
# We've found an escaped double quote - add it to the end of the | |
# accumulator. | |
quoted_str="$quoted_str""$char" | |
state="double-quoted" | |
;; | |
*) # -> double-quoted | |
# Failed escape - the escape character wasn't followed by an | |
# escapable character, so treat them both literally. | |
quoted_str="$quoted_str"'\'"$char" | |
state="double-quoted" | |
esac | |
;; | |
*) | |
echo "Erroneous Stage 1 Parser state: $state; exiting." >&2 | |
exit 128 | |
esac | |
done # We've finished parsing the whole line | |
args+=("$current_arg") | |
current_arg="" | |
[ "$dbg" -gt 0 ] && echo "about to execute 3" | |
# Execute the command | |
if [ ${#args[@]} -gt 0 ]; then # if the args array is non-empty | |
"${prompt_args[@]}" "${args[@]}" | |
if [ "$?" -gt 0 ]; then | |
error=1 | |
fi | |
printf '\n' | |
fi | |
args=() | |
} | |
# Note: Starting bash 5.3, you will be able to do read -E, which is like | |
# read -e, but has history. However, bash 5.3 has not been released yet. | |
while IFS=';' read -r -e -p "$*> " line; do | |
# line="$(join ";" "${linearr[@]}")" | |
# echo "line: $line" | |
if [ -z "$line" ]; then | |
continue | |
fi | |
if ! [[ "${line:0:1}" == " " ]]; then # do not log lines that start with a space | |
printf '%s\n' "$line" >> "$this_histfile" | |
fi | |
error=0 | |
parser "$line" | |
# Log prompts that cause errors | |
if [ "$error" -gt 0 ]; then | |
printf '%s\n' "$line" >> "$this_errorfile" | |
fi | |
done |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment