bash script: How to implement your own history mechanism? - bash

I'm implementing an interactive bash script similar to the MySQL client, /usr/bin/mysql. In this script I need to issue various types of 'commands'. I also need to provide a history mechanism whereby the user can use the up/down arrow keys to scroll through the commands entered so far.
The snippet listed here (Example 15-6, Detecting the arrow keys) does not exactly do what I want it to. I really want the following:
The up/down arrow keys should operate in silent mode. Meaning, they should not echo their character codes on the terminal.
The other keys however (which will be used to read the command names and their arguments) must not operate in silent mode.
The problem with read -s -n3 is that it does not satisfy my simultaneously conflicting requirements of silent mode and echo mode, based solely on the character code. Also, the value -n3 will work for arrow keys but, for other/regular keys, it won't 'return control' to the calling program until 3 characters have been consumed.
Now, I could try -n1 and manually assemble the input, one character at a time (yuck!). But the character-code based silent-/echo-mode switching problem would still persist!
Has anyone attempted this thing in bash? (Note: I cannot use C, nor other scripting languages like Perl, Python, etc.)
EDIT
Continuing with Dennis' answer... You will also need to manually add your desired entries to your history via history -s, like so...
while read -e x; do
history -s "$x"
# ...
done

You can use read -e to have read use readline. It will process your cursor keys and maintain the history for you. You will also need to manually add your desired entries to your history via history -s, like so:
while read -e x; do
history -s "$x"
# ...
done

MySQL and Bash use the Readline library to implement this. Maybe you could use something like rlwrap or rlfe?

rlwrap has a special "one-shot" mode to act as a replacement for the 'read' shell command. If you wish, every occurrence of this command in your script can be given its own history and completion word list.
Use it like this:
REPLY=$(rlwrap -o cat)
or, specifying a history file and a completion wordlist:
REPLY=$(rlwrap -H my_history -f my_completions -o cat)

Related

How to answer a bash arrow selection menu through script

little stuck atm with figuring out how to script an answer to a bash arrow selection menu
Basically my question is how do I script selection of an item based on name?
I do know that I can use the following command to select an option by index and this is as close as I've gotten but I think there's a way to select it by name instead
echo 5 | zapier link
Important notes:
When a version is already selected it gets the [currently linked app] at the end of the version, this needs to be taken into account in text search
The numbers in () i.e (142900) is important
Pretend the name that's blocked out in red is testOver
I would prefer not to have to install any tools to accomplish this, only use whats built into bash unless there's no other option.
Thanks in advance for your help!
To select a menu entry by search, you have to read zapier's output. Usally this would be a job for expect, but since you don't want to install anything, try the following:
mkfifo fifo
zapier link <fifo |
awk '/Which integration/ {m=NR}
m && /\(142900\)/ {print NR-m; fflush(); m=0}' >fifo
rm fifo
This selects the option containing the string (142900). Of course you can adapt the regex to select any other option. Also consider replacing /Which integration/ by the full title of the menu to make detection safer.
If you want to see the output of zapier, use | tee >(awk ... >fifo).
In case zapier behaves differently when running inside a pipe, run it inside a pseudo-terminal. If zapier buffers its output for pipes, using script or stdbuf to unbuffer the output is an absolute requirement. Otherwise the entire command may run into a deadlock.
script -qec './zapier link' /dev/null <fifo # on Linux
script -qe /dev/null ./zapier link <fifo # on BSD/macOS.
Unfortunately, awk does not know when the menu is fully printed, so it cannot look at all the options first and then decide afterwards, but has to select one option as soon as it sees it. If there is no such option, the script gets stuck. To solve this you could replace awk with a small bash script:
zapier link <fifo | while IFS= read -r ln; do
[[ "$ln" = *"Which integration"* ]] || continue
IFS=$'\n' read -rd '' -t1 -a opts
# insert code to pick an option from the array `opts`
# either `echo "$((i+1))"` to select option `${opts[i]}`
# or `break` to select no option
done >fifo

How do I edit current shell command without executing it?

There seems to be quite a lot of information on how to edit and execute a command using your editor using "edit-and-execute-command (C-x C-e)", but what I would like to achieve is take the current shell command, apply certain filtering (using a script) and then return it to prompt for further approval/manual changes before execution. Is this possible with bash?
Latest update based on my experience
The part 0"+y$dd in the following mapping is really something that you should carefully think about and tailor it to your taste/workflow/experience.
For instance, very frequently I've found myself ending up with multiple lines in the buffer, where I only want to execute the one the cursor is on; in this case I can use 0"+y$dd:%d<CR> instead of 0"+y$dd.
And this is just one of the possible scenarios.
Final answer for those who like vim
Set vim as your EDITOR/VISUAL, so that when editing a command line, you will use vim to edit it.
Put au BufEnter /tmp/bash-fc.* nn <Leader>d 0"+y$dd:wq<CR> in your ~/.vimrc file to map Leaderd (which you will rarely use when editing a command) to the action "delete the current line into the + register without the trailing EOL".
you can use either the + or the * register in the mapping above; the ways to paste into the terminal will likely differ; you need the +clipboard option for these registers to be available.
When finished editing a command in the vim editor, hit EscapeLeaderd.
Paste the clipboard into the terminal (this is terminal-dependent).
Original answer
I often need to do the same, and I do it as follows. (I normally use the set -o vi in bash, so points 1 and 2 in the following are different if you use set -o emacs, the default; based on your question it looks like points 1 and 2 are unified in Ctrl+x followed by Ctrl+e, which is harder to type, imho.)
hit Escape to be in normal mode,
hit v to enter the editor to edit the command,
edit the command as I like,
(This is where you ask the question.)
hit Escape0"+y$dd:wq,
Note: 0"+y$, not simply "+yy, as the latter would copy the newline too, and this would result in executing the command upon pasting it in the command line,
paste the clipboard on the command line
how to do this depends on the terminal you are using, I guess; I hit Ctrl+Alt+v in URxvt.
proceed to approval/manual edit.
Clearly this is just a workaround, consisting in copying the edited command into the clipboard before deleting the whole command, so that nothing gets executed upon exiting the editor; however it's the best I can get for myself.
Update
As my EDITOR (and VISUAL) is equal to vim, when I edit the command, I edit it in vim.
In this respect, I have noticed that the buffer is named /tmp/bash-fc.random, where random is a 6-characters alphanumeric random string.
This gives space to a lot of possiblities, if you use vim as your editor, as you can define some mapping in your .vimrc to execute the whole sequence Escape0"+y$dd:wq. For instance, one command that you'd rarely use when editing a command line is Leaderd; therefore you can put the following mapping in your .vimrc file
au BufEnter /tmp/bash-fc.* nn <Leader>d 0"+y$dd:wq<CR>
so that step 4 in the above recipe becomes
hit EscapeLeaderd
It's not possible to do that in Bash/readline but it's possible in zsh
using edit-command-line command:
darkstar% autoload edit-command-line; zle -N edit-command-line
darkstar% bindkey "^X^E" edit-command-line
Now press Control-x Control-e to open your editor, edit line, leave the editor - you will see the updated command line but it will not be executed automatically.
Now that I think about it, maybe a variation of what #kenorb suggested in a comment is the best workaround (as it seems no solution exists), if we want to stick to bash.
What you can do is prepend a # (the comment character in bash) to the command, rather than echo. Then when you exit the editor, the command will be ineffective, and you will only have to press arrow up (or k, if you use set -o vi), remove the # and confirming.
Note that this strategy adds just a few keystrokes, so it can be fairly efficient, depending on your typing level.
These pieces might get you closer:
a) replace the the normal binding for newline newline (ctrl-M)
bind -x '"\C-M":date"
b) grab the current line from the history using !#
replace date with whatever script you want.
c) edit manually
d) if necessary, somehow tie in !!:p which prints the new command to the command line but does not execute it, thus letting you manually edit it.
e) using ctrl-J submit edited command rather than a newline
or they might not ....
There is an option in bash to modify command from history without executing it. I'm not sure it it's possible to use script for this, doesn't seem to be likely. Although, you can make modifications using history modifiers.
Enable option histverify to prevent execution of modified command
Use chain of modifiers to change last command
Use "!!" to put your result to command line for final edit
Here is how it looks:
$ shopt -s histverify
$ ls *.sh
script1.sh script2.sh script3.sh script-return.sh
$ !!:s/*/script1/:p
ls script1.sh
$ !!:s/1/2/:p
ls script2.sh
$ !!
$ ls script2.sh
script2.sh
I'd like to point you to the Composure framework for Bash (I'm not affiliated with it): https://github.com/erichs/composure
It provides draft and revise functions that sound like they could help with what you're trying to do. Here's a (long) quote from the project's readme file:
Composure helps by letting you quickly draft simple shell functions,
breaking down your long pipe filters and complex commands into
readable and reusable chunks.
Draft first, ask questions later
Once you've crafted your gem of a command, don't throw it away! Use
draft () and give it a good name. This stores your last command as a
function you can reuse later. Think of it like a rough draft.
$ cat servers.txt
bashful: up
doc: down
up-arrow
$ cat servers.txt | grep down
doc: down
$ draft finddown
$ finddown | mail -s "down server(s)" admin#here.com
Revise, revise, revise!
Now that you've got a minimal shell function, you may want to make it
better through refactoring and revision. Use the revise () command
to revise your shell function in your favorite editor.
generalize functions with input parameters
add or remove functionality
add supporting metadata for documentation
$ revise finddown
finddown ()
{
about finds servers marked 'down' in text file
group admin
cat $1 | grep down
}
$ finddown servers.txt
doc: down
It does not seem possible with a keyboard shortcut, at least:
$ bind -P | grep -e command -e edit
complete-command can be found on "\e!".
edit-and-execute-command can be found on "\C-x\C-e".
emacs-editing-mode is not bound to any keys
possible-command-completions can be found on "\C-x!".
vi-editing-mode is not bound to any keys
This can be done in native bash using readline specifically READLINE_LINE and READLINE_POINT variables. I use this functionality all the time though not through vim, you would need to get the value of $selected from your vim command and if not empty it takes your original line + your input and replaces your original line with the combination without executing. output as a variable
_main() {
selected="$(__coms_select__ "$#")"
origonal_text=$READLINE_LINE READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
}
bind -m emacs-standard -x '"\C-e": _main '
bind -m vi-command -x '"\C-e": _main '
bind -m vi-insert -x '"\C-e": _main '
Edit
Just remembered these two utilities that will let you do this as well.
Vipe allows you to run your editor in the middle of a unix pipeline and edit the data that is being piped between programs.
vp, up, vipe, Neomux (upgrade of nvim terminal) you can do some pretty neat throwing buffers between the terminal and split window.
and Athame (full vim on the command line)
https://github.com/ardagnir/athame
careful with that one though plugins work on the cli and it can get funky if you got tons of plugins

Why does TIOCSTI injection disable stty echo in bash when used with bind?

I'm trying to bind a key in bash to inject commands into the terminal using TIOCSTI (example here). This works just fine until I inject certain commands. The end goal is a bash reverse search (ctrl-r) replacement, but this serves as an example to show my issue.
# Test using a clean environment
env -i bash --noprofile --norc
# Define TIOCSTI helper
function inject() { perl -e 'ioctl(STDIN, 0x5412, $_) for split "", join " ", #ARGV' "$#"; }
# Bind ctrl-b to inject 'yes | less' as a test
bind -x '"\C-b":"inject yes \| less"'
<press ctrl-b>yes | less<press enter>
WARNING: terminal is not fully functional
y (press RETURN)
y
...
<press q to exit less>
# Terminal is now foobared. In particular I can't see what I type. Why?
# Enable echo
stty echo
If I just enter inject yes \| less and press enter the terminal is fine afterwards. I think it has something to do with TIOCSTI being run within bash's bind. Injecting yes | less, just yes and a long git log trigger this but many others such as echo and vi do not. Interestingly I can ctrl-b to inject the command, delete everything on the line, retype it myself and I still lose echo. It's as though readline gets poisoned with some invisible character that I can't delete.
Why am I losing echo?
How can I fix it? E.g. perhaps some more codes need to be sent with TIOCSTI to make it safe.
GNU bash, version 4.4.19(1)-release (x86_64-pc-linux-gnu)
If you just need some sort of keyboard macro, you could place something like this (as example) in your ~/.inputrc :
# F12 has a keyboard macro:
"\e[24~": "cls; (date; make 2>&1 || flash 1 >/dev/console 2>&1; date) | tee make.res^M"
Note 1: The ^M is an actual control-M character (i.e. a \r, CR, Carriage Return).
Note 2: I think '.inputrc' is picky about spacing; I seem to recal there has to be exactly one space between the key and the macrodef.
Note 3: To see what your F12 (for example) outputs, run cat and press the F12 key and take note of the string it prints.
I used to have this for years when bells and whistles were en vogue, and I compiled every last damn package myself and just kept all make logs (cls is a script that basically does tput clear, flash was a homegrown script to flash the terminal and ring a bell :)
End note: macros like the above that contain a CR at the end make me nervous and I (now) consider dangerous. Just the macro placing the command after the prompt, waiting for your interactive CR (or ^C) looks to me the safer way.
I believe this is a bug in bash, and it is not triggered by specifically the TIOCSTI injection, but by running a program in the bind -x handler (in this case: perl).
Your example still results in the described bug behavior in GNU bash, version 5.1.8(1)-release (x86_64-pc-linux-gnu), but in the version built from the devel branch (GNU bash, version 5.2.0(1)-alpha (x86_64-pc-linux-gnu)), which contains a fix, everything works as expected.

use "!" to execute commands with same parameter in a script

In a shell, I run following commands without problem,
ls -al
!ls
the second invocation to ls also list files with -al flag. However, when I put the above script to a bash script, complaints are thrown,
!ls, command not found.
how to realise the same effects in script?
You would need to turn on both command history and !-style history expansion in your script (both are off by default in non-interactive shells):
set -o history
set -o histexpand
The expanded command is also echoed to standard error, just like in an interactive shell. You can prevent that by turning on the histverify shell option (shopt -s histverify), but in a non-interactive shell, that seems to make the history expansion a null-op.
Well, I wanted to have this working as well, and I have to tell everybody that the set -o history ; set -o histexpand method will not work in bash 4.x. It's not meant to be used there, anyway, since there are better ways to accomplish this.
First of all, a rather trivial example, just wanting to execute history in a script:
(bash 4.x or higher ONLY)
#!/bin/bash -i
history
Short answer: it works!!
The spanking new -i option stands for interactive, and history will work. But for what purpose?
Quoting Michael H.'s comment from the OP:
"Although you can enable this, this is bad programming practice. It will make your scripts (...) hard to understand. There is a reason it is disabled by default. Why do you want to do this?"
Yes, why? What is the deeper sense of this?
Well, THERE IS, which I'm going to demonstrate in the follow-up section.
My history buffer has grown HUGE, while some of those lines are script one-liners, which I really would not want to retype every time. But sometimes, I also want to alter these lines a little, because I probably want to give a third parameter, whereas I had only needed two in total before.
So here's an ideal way of using the bash 4.0+ feature to invoke history:
$ history
(...)
<lots of lines>
(...)
1234 while IFS='whatever' read [[ $whatever -lt max ]]; do ... ; done < <(workfile.fil)
<25 more lines>
So 1234 from history is exactly the line we want. Surely, we could take the mouse and move there, chucking the whole line in the primary buffer? But we're on *NIX, so why can't we make our life a bit easier?
This is why I wrote the little script below. Again, this is for bash 4.0+ ONLY (but might be adapted for bash 3.x and older with the aforementioned set -o ... stuff...)
#!/bin/bash -i
[[ $1 == "" ]] || history | grep "^\s*$1" |
awk '{for (i=2; i<=NF; i++) printf $i" "}' | tr '\n' '\0'
If you save this as xselauto.sh for example, you may invoke
$ ./xselauto.sh 1234
and the contents of history line #1234 will be in your primary buffer, ready for re-use!
Now if anyone still says "this has no purpose AFAICS" or "who'd ever be needing this feature?" - OK, I won't care. But I would no longer want to live without this feature, as I'm just too lazy to retype complex lines every time. And I wouldn't want to touch the mouse for each marked line from history either, TBH. This is what xsel was written for.
BTW, the tr part of the pipe is a dirty hack which will prevent the command from being executed. For "dangerous" commands, it is extremely important to always leave the user a way to look before he/she hits the Enter key to execute it. You may omit it, but ... you have been warned.
P.S. This scriptlet is in fact a workaround, simulating !1234 typed on a bash shell. As I could never make the ! work directly in a script (echo would never let me reveal the contents of history line 1234), I worked around the problem by simply greping for the line I wanted to copy.
History expansion is part of the interactive command-line editing features of a shell, not part of the scripting language. It's not generally available in the context of a script, only when interacting with a (pseudo-)human operator. (pseudo meaning that it can be made to work with things like expect or other keystroke repeating automation tools that generally try to play act a human, not implying that any particular operator might be sub-human or anything).

Pre-filling a prompt in Bash

Writing a bash script, and I want to get user input. Awesome,
read -p "What directory should we save in? " -e FOLDER
Except that what I'd like to do, ideally, is have the user see something like:
What directory should we save in? /home/user/default/
with the cursor at the end of the line, and the ability to delete backwards or append or whatever. Essentially, pre-filling the user's input, but giving them the ability to edit it.
Readline obviously has the capability, but it appears to be not exposed in the read command. Any alternatives? I'd prefer to not have to use perl or such.
The constraint I'm working under is that I'm writing a single shell script that would be nice to disseminate widely, so should rely on as little pre-existing infrastructure as possible. rlwrap and read -i both work if their dependencies (rlwrap and bash version >> whatever I have, respectively) are available. Both good answers, choose whichever works for you.
$ read -p "What directory should we save in? " -i "/home/user/default/" -e FOLDER
What directory should we save in? /home/user/default/
that should work, right?
You can wrap the command in rlwrap, which provides instant readline capabilities: https://github.com/hanslub42/rlwrap
(rlwrap -P does what you want)
As far as a pure bash solution is concerned for the 3.2 line (which i am presuming you are using), I dont think its possible

Resources