Bash: call script with customized keyboard shortcuts? - linux

Lets say I have a script, "myscript.sh", with contents being simply echo $PWD. I'd like to bind somehow this script to a key combo in bash (gnome-terminal) - so that when I press this key combination, the output of "myscript.sh" is inserted ("pasted") at the cursor position in the terminal.
Apparently, bash history and line manipulation is handled by readline - and the references I got for bash keyboard shortcuts, do reference readline:
bash keyboard shortcuts
Bash Reference Manual: Bindable Readline Commands
I've also seen in Bash Reference Manual: Readline Init File Syntax that the key bindings for bash can be listed by using bind -p (see help bind [not 'man bind'] for more). So maybe this question would better be titled as "_binding macros to custom keyboard shortcuts in readline" :) But in any case, is what I want possible to do?
I guess an alternative would be to have the script be something like "pwd | xsel -b", and then I call it on terminal - and I can paste afterwards; but I'd still like a single keyboard shortcut instead, say like Ctrl-Alt-H (which seems to be not used for anything), which will immediately insert/paste script output when pressed.
Thanks in advance,
Cheers!
EDIT: Just to clarify - here is my use case where I'd like this facility. I'm usually cd'd in a project folder, usually named something like myproject-folder-0012a, which is under revision control by svn. And there is a bunch of these folders. So quite often, I do commits where the first word of the message is the directory name, as in:
svn ci -m "myproject-folder-0012a: here a commit message"
But that is what I don't like - first I type 11 characters, which go rather fast:
svn ci -m "
And then, I cannot use autocompletion to get the name (i'm inside the folder) - which means I either have to fully type it (no way :)), or I copy paste it from the prompt (which requires selection - press mouse, drag, release mouse; then Ctrl+Shift+C, and then Ctrl+Shift+V, plus any left/right keys if I miss allignment - plus deletions and such if I make the copy wrong).
Meaning - so much work, just to get the bloody folder name for a bloody commit message :( I'd MUCH rather press something like (say) Ctrl-Alt-H, and have the folder name automatically inserted at cursor position, and be done with it :)
My suggestion for xsel is only because I could put it into a "global" script - say symlink it as /usr/bin/myscript (and obviously, the contents of the script are echo $(basename $PWD) rather than just pwd for my needs), and then I could do:
$ myscript # this puts directory name in clipboard
$ svn ci -m "[CTRL+SHIFT+V TO PASTE HERE]myproject-folder-0012a[NOW TYPE]: here a commit message"
... which sort of makes the workload less, but still - then I have to remember what the script name is, and call it, before I type the svn command (and I don't always remember that)... And still - I have to call a command, and then press a key combo; why shouldn't I just press a key combo once, and be done with it ??! :)
Well, hope this clarifies my problem a bit better ....
EDIT2: However, another reason why a bash keyboard shortcut would be useful, is that then I could also "paste/insert current directory name" not only in shell commands - but also in terminal programs, say like nano (where it would, arguably, be more difficult to use bash script or function expansion directly).

Simple version:
This command at a shell prompt:
bind '"\ee": "${PWD##*/}\e\C-e"'
or this line added to your ~/.inputrc:
"\ee": "${PWD##*/}\e\C-e"
will cause Alt-e to insert the basename of the current directory on the command line. It requires that the default binding of the readline function shell-expand-line which is \e\C-e be present (this could be adapted if it's different). I'm also making the assumption that you're using Bash's emacs mode.
Unfortunately, it causes things that have already been typed to be expanded as well. One of the affects of this is that after having typed:
svn ci -m "
and pressing Alt-e, the quotation mark will have disappeared. There are a couple of ways to deal with this.
One, assume that all you'll lose is the quote and either manually add it back or have the readline macro add it for you:
bind '"\ee": "${PWD##*/}\e\C-e\eb\"\C-e"'
which just isn't very satisfactory.
Advanced version:
Or, two, kill the line, do the insertion, then yank the line back:
bind '"\ee": " \C-u \C-a\C-k${PWD##*/}\e\C-e\C-y\C-a\C-y\ey\b"'
or
bind '"\ee": " \C-u \C-a\C-k${PWD##*/}\e\C-e\C-y\C-a\C-y\ey\b\ef\C-f"'
This leaves the rest of the line intact (nothing else is expanded or deleted), but it uses the kill ring, so it may leave it in a state that's different than you expect (if you're using it). It also inserts a space after the inserted directory name (the spaces in the macro are used to ensure that older kill-ring contents are not regurgitated if the macro is executed at the beginning or end of the line). The macro should work regardless of the position of the cursor in the line. The insertion will be made at the cursor's position, leaving the cursor in the same position [in the first version].
Edit: The second version leaves the cursor after the dirname and space that are inserted.
Edit 2:
The readline function shell-forward-word (unbound) does a better job than forward-word (\ef) for this. You can make use of that like this:
bind '"\ew":shell-forward-word'
bind '"\ee": " \C-u \C-a\C-k${PWD##*/}\e\C-e\C-y\C-a\C-y\ey\b\ew\C-f"'
By the way, you should know that Bash keyboard shortcuts are not active in other programs such as nano.

Ok, not really an answer, but I'd just like to summarize the comments I got so far, which are useful for my problem. However, the question as it stands - in respect to bash keyboard shortcuts running arbitrary scripts - is still not answered (I'd still prefer doing all this with a single key combo :))
First, I can use a 'global' script like:
$ sudo bash -c 'cat > /usr/bin/bpwd <<EOF
#!/bin/bash
basepwd=\$(basename \$(pwd))
echo -n \$basepwd # suppress line ending
# exec 1>/dev/null # debug: redir stdout to null
echo -n \$basepwd | xsel -i -b # suppress LF, and make xsel read from stdin
# exec 1>/dev/tty # debug: restore stdout
EOF
chmod +x /usr/bin/bpwd'
Or, I can add bash functions to my .bashrc (note: make sure you reload bash after you add these lines to .bashrc - for example, simply by typing bash in your current terminal):
$ echo '
bpwd2() { basepwd=${PWD##*/} ; echo -n $basepwd | xsel -i -b ; echo -n $basepwd ; }
svnci-test() { echo -n "$(bpwd2): $*" ; }
svnci-m() { svn ci -m "$(bpwd2): $*" ; }' >> ~/.bashrc
Basically, I misunderstood Reese Moore's suggestion originally - you can indeed use backticks - consider this command session (after the above commands have been ran):
$ bpwd
Desktop\
$ bpwd2
Desktop\
$ echo `bpwd`
Desktop
$ echo "`bpwd2` 2"
Desktop 2
This is what I needed to understand Moore's "the output from the backticked commands will be used as input on the executed command" (however, one also needs to take care to clean the line endings from the output); or, in my case, I can call
svn ci -m "`bpwd`: my message here"
# svn ci -m "${PWD##*/}: my message here" # alternatively
... or, I could follow camh's suggestion, and use svnci-m as a function (in my case, I almost never use additional arguments to svn ci, and so my version is slightly different). And to test whether arguments are passed correctly, I can use the svnci-test function:
$ svnci-test "my message"
Desktop: my message\
Thanks for the comments so far,
Cheers!

One way to do what you want with a single key press is to take advantage of programmable completion in bash. You possibly have some programmable completion set up with the bash_completion tool/package. If not, look into that to see the specifics of how it is done.
The idea is to have the programmable completion recognise when you have hit at the start of a svn commit message and then have it return a single completion which is the text you want to insert (the basename of the current directory).
I've only dabbled with programmable completion so I can't give you the details, but the above-mentioned bash_completion package or the subversion completion script may be a good start.

Related

How can I prepopulate a terminal prompt with a command or partial command?

I would like to write a command/function that will prepopulate my terminal prompt with a partial command or text. I need to prefix my git commit messages and I was thinking it would be useful for this (and other uses) if I could prefill the prompt with a partial command rather than always needing to retype the same thing.
> gencommit
> git commit -m "ABC-123:
In the code snippet above, git commit -m "ABC-123: is placed on the command line and I would simply be able to add a commit message and execute the command.
echo is the only thing that comes to mind but that does not do what I want, If gencommit used echo it would look something like this.
> gencommit
git commit -m "ABC-123:
>
As far as I know, bash doesn't provide any way to write to the Readline buffer programatically. (zsh does, as an aside.) One alternative is to make gencommit a Readline macro. For example, add this to your .inputrc file (creating the file if necessary):
"gencommit": "git commit -m \"ABC-123:"
One thing about his approach that I find disconcerting is that because a macro is text that gets replaced immediately, Readline won't actually echo the text you are typing to the screen until it has determined what macro you are typing, so while you are typing gencommit, nothing appears on your screen until you finish, then the expansion text appears. Worse, if you make a mistake while typing, you have to cancel the command and start over.
Better is to define an alias
alias gencommit='git commit -m "ABC-123:'
and, after typing gencommit on the command line, use the shell-expand-line Readline function (bound to M-! by default; M is the meta key, which is either Alt or Esc, depending on your platform and terminal emulator settings) to expand it to the replacement text. (This tries to expand anything on the command line: aliases, parameters, history expansions, etc. There is a dedicated alias-expand-line function that only expands aliases, but it is unbound by default. I leave it as an exercise to bind this command to an unused key sequence if you like.)
As I alluded to zsh, the command in that shell would simply be
print -z 'git commit -m "ABC-123:'

Key binding to go up by one directory (in Bash)

In Zsh, I have a key binding to go up by one directory (very useful):
# C-M-u: up-directory
up-directory() {
builtin cd .. && zle reset-prompt
}
zle -N up-directory
bindkey '\e\C-u' up-directory
It's very nice. So nice that I would like to get it as well in my Bash config.
How can we do that?
In case someone still need this:
This binds cd .. to F2. Keeps the currently typed command and throws an updated prompt.
bind '"\eOQ": "\C-a\C-kcd ..\C-m\C-y"'
How it works:
\eOQ is F2 on my terminal
C-a beginning-of-line
C-k kill-line
cd ..
C-m accept-line
C-y yank <- this restores the killed line
However the cd .. command appears in the screen. If it bothers you, use this instead:
bind -x '"\201":cd ..'
bind '"\eOQ": "\C-a\C-k\201\C-m\C-y"'
\201 is non existent key, an empty slot we can use.
Note1: It doesn't save the cursor position, after F2 the cursor lands on the end of the line, which is a little limitation.
Note2: The original was: bind '"\eOQ": " \C-a\C-k\201\C-m\C-y\C-b\C-d"' which adds an extra space, then removes it. I don't know why, maybe it was some workaround in the old ages when i wrote this, but it looks it does not needed now.
You can do that.
It's not as elegant or straight forward as with zsh but it's doable in bash using bind.
You can not only bind built in Readline functions (listed with bind -l) but other macros and shell functions too.
bind -m emacs -x '"\C-i":"cd .."' will bind a shell command (cd ..) to the keys (Ctrl+i) in emacs mode (the default mode). (Ctrl+i is unbound by default, u isn't)
Note that your prompt will probably not reflect the change.
If you leave out -x the string will instead be typed out for you so "cd ..\n" achieves the same result.
Edit: bind is how you bind keys and macros can accomplish what you want even though no built in thing exists.
If you end your PS1 prompt with \033[K (erase to eol) and can use bind -m emacs '"\C-i":" cd ..&&echo -e \"\\033[2A\"\n"' to do what you want.This will first print cd .. then control chars to move the cursor up and run it (with \n).
The end of your PS1 prevents it from showing. This is a hack but it shows that it's doable.

Customize tab completion in shell

This may be have a better name than "custom tab completion", but here's the scenario:
Typically when I'm at the command line and I enter a command, followed with {TAB} twice, I get a list of all files and subdirectories in the current directory. For example:
[user#host tmp]$ cat <TAB><TAB>
chromatron2.exe Fedora-16-i686-Live-Desktop.iso isolate.py
favicon.ico foo.exe James_Gosling_Interview.mp3
However, I noticed at least one program somehow filters this list: wine. Consider:
[user#host tmp]$ wine <TAB><TAB>
chromatron2.exe foo.exe
It effectively filters the results to *.exe.
Thinking it might be some sort of wrapper script responsible for the filtering, a did a which and file an it turns out wine is not a script but an executable.
Now, I don't know whether this "filter" is somehow encoded in the program itself, or otherwise specified during the default wine install, so I'm not sure whether this question is more appropriate for stackoverflow or superuser, so I'm crossing my fingers and throwing it here. I apologize if I guessed wrong. (Also, I checked a few similar questions, but most were irrelevant or involved editing the shell configuration.)
So my question is, how is this "filtering" accomplished? Thanks in advance.
You will likely find a file on your system called /etc/bash_completion which is full of functions and complete commands that set up this behavior. The file will be sourced by one of your shell startup files such as ~/.bashrc.
There may also be a directory called /etc/bash_completion.d which contains individual files with more completion functions. These files are sourced by /etc/bash_completion.
This is what the wine completion command looks like from the /etc/bash_completion on my system:
complete -f -X '!*.#(exe|EXE|com|COM|scr|SCR|exe.so)' wine
This set of files is in large part maintained by the Bash Completion Project.
You can take a look at Programmable Completion in bash manual to understand how it works.
I know this is old but I was looking to do something similar with a script of my own.
You can play around with an example I made here:
http://runnable.com/Uug-FAUPXc4hAADF/autocomplete-for-bash
Pasted code from above:
# Create function that will run when a certain phrase is typed in terminal
# and tab key is pressed twice
_math_complete()
{
# fill local variable with a list of completions
local COMPLETES="add sub mult div"
# you can fill this variable however you want. example:
# ./genMathArgs.sh > ./mathArgsList
# local COMPLETES=`cat ./mathArgsList`
# we put the completions into $COMPREPLY using compgen
COMPREPLY=( $(compgen -W "$COMPLETES" -- ${COMP_WORDS[COMP_CWORD]}) )
return 0
}
# get completions for command 'math' from function '_math_complete()'
complete -F _math_complete math
# print instructions
echo ""
echo "To test auto complete do the following:"
echo "Type math then press tab twice."
echo "You will see the list we created in COMPLETES"
echo ""

Alternative to Up Arrow + Enter to run previous command?

Sometimes I have to run a command many times in succession, for example to see if a service has started, and it becomes tedious to move my hands away from my normal typing position to press the Up Arrow and Enter keys repeatedly. Is there a way to run the previous command without the Up Arrow and Enter keys, perhaps with an elaborate shell script?
I've tried the following, but it is unsatisfactory because it cannot execute aliases, and it is a little slow.
history | tail -2 | head -1 | cut -d' ' -f4- | cat > prev_command.txt
sleep .01
chmod 777 prev_command.txt
eval prev_command.txt
rm prev_command.txt
Ideally I'd have an alias to this script so I can type in something like "prev" in the command line and hit Enter to run the previous command again.
In bash, you can press ctrlp to go to the previous command -- that's a lot better than having to move to the arrow keys.
See also: https://github.com/fliptheweb/bash-shortcuts-cheat-sheet/
Use
!!
to run your previous command.
sudo !!
also works , for the record.
Instead of running the same command many times in succession, why not watch it instead? watch will run a specified command repeatedly and display the output in stdout so you can see it change over time.
watchcommand
I often use the "history expansion" feature in bash (usually activated with cntlR) -- it interactively searches through your history for the previous closest match.
See the bash manual section Searching for Commands in the History, and also Using History Interactively.
Are you an emacs or vi user? You can use
set -o vi
set -o emacs
to set emacs or vi keybindings. You can then use the emacs or vi key bindings in bash. I don't know if this should work for other shells. I believe the vi mode starts in insert mode, so you need to hit esc to enter command mode. In emacs mode (the default), you can use ctrl+p and then ctrl+j to move to the previous line and do a carriage return.
Otherwise, you can use !! as someone else suggested.
In bash:
$ help fc
fc: fc [-e ename] [-lnr] [first] [last] or fc -s [pat=rep] [command]
Display or execute commands from the history list.
fc is used to list or edit and re-execute commands from the history list.
FIRST and LAST can be numbers specifying the range, or FIRST can be a
string, which means the most recent command beginning with that
string.
Options:
-e ENAME select which editor to use. Default is FCEDIT, then EDITOR,
then vi
-l list lines instead of editing
-n omit line numbers when listing
-r reverse the order of the lines (newest listed first)
With the `fc -s [pat=rep ...] [command]' format, COMMAND is
re-executed after the substitution OLD=NEW is performed.
A useful alias to use with this is r='fc -s', so that typing `r cc'
runs the last command beginning with `cc' and typing `r' re-executes
the last command.
Exit Status:
Returns success or status of executed command; non-zero if an error occurs.
Note the suggestion for alias r; I use this frequently.
Depending on what terminal you're using, I know a lot used to have F3 as an option for repeating, but that's still outside the normal range for typing as well unless you have a special keyboard with more accessible function keys.
My keyboard makes the function keys easily accessible, but I don't do much command line work in unix any more, so I wouldn't be able to tell you for sure whether or not this is still possible.

Edit shell script while it's running

Can you edit a shell script while it's running and have the changes affect the running script?
I'm curious about the specific case of a csh script I have that batch runs a bunch of different build flavors and runs all night. If something occurs to me mid operation, I'd like to go in and add additional commands, or comment out un-executed ones.
If not possible, is there any shell or batch-mechanism that would allow me to do this?
Of course I've tried it, but it will be hours before I see if it worked or not, and I'm curious about what's happening or not happening behind the scenes.
It does affect, at least bash in my environment, but in very unpleasant way. See these codes. First a.sh:
#!/bin/sh
echo "First echo"
read y
echo "$y"
echo "That's all."
b.sh:
#!/bin/sh
echo "First echo"
read y
echo "Inserted"
echo "$y"
# echo "That's all."
Do
$ cp a.sh run.sh
$ ./run.sh
$ # open another terminal
$ cp b.sh run.sh # while 'read' is in effect
$ # Then type "hello."
In my case, the output is always:
hello
hello
That's all.
That's all.
(Of course it's far better to automate it, but the above example is readable.)
[edit] This is unpredictable, thus dangerous. The best workaround is , as described here put all in a brace, and before the closing brace, put "exit". Read the linked answer well to avoid pitfalls.
[added] The exact behavior depends on one extra newline, and perhaps also on your Unix flavor, filesystem, etc. If you simply want to see some influences, simply add "echo foo/bar" to b.sh before and/or after the "read" line.
Try this... create a file called bash-is-odd.sh:
#!/bin/bash
echo "echo yes i do odd things" >> bash-is-odd.sh
That demonstrates that bash is, indeed, interpreting the script "as you go". Indeed, editing a long-running script has unpredictable results, inserting random characters etc. Why? Because bash reads from the last byte position, so editing shifts the location of the current character being read.
Bash is, in a word, very, very unsafe because of this "feature". svn and rsync when used with bash scripts are particularly troubling, because by default they "merge" the results... editing in place. rsync has a mode that fixes this. svn and git do not.
I present a solution. Create a file called /bin/bashx:
#!/bin/bash
source "$1"
Now use #!/bin/bashx on your scripts and always run them with bashx instead of bash. This fixes the issue - you can safely rsync your scripts.
Alternative (in-line) solution proposed/tested by #AF7:
{
# your script
exit $?
}
Curly braces protect against edits, and exit protects against appends. Of course, we'd all be much better off if bash came with an option, like -w (whole file), or something that did this.
Break your script into functions, and each time a function is called you source it from a separate file. Then you could edit the files at any time and your running script will pick up the changes next time it gets sourced.
foo() {
source foo.sh
}
foo
Good question!
Hope this simple script helps
#!/bin/sh
echo "Waiting..."
echo "echo \"Success! Edits to a .sh while it executes do affect the executing script! I added this line to myself during execution\" " >> ${0}
sleep 5
echo "When I was run, this was the last line"
It does seem under linux that changes made to an executing .sh are enacted by the executing script, if you can type fast enough!
An interesting side note - if you are running a Python script it does not change. (This is probably blatantly obvious to anyone who understands how shell runs Python scripts, but thought it might be a useful reminder for someone looking for this functionality.)
I created:
#!/usr/bin/env python3
import time
print('Starts')
time.sleep(10)
print('Finishes unchanged')
Then in another shell, while this is sleeping, edit the last line. When this completes it displays the unaltered line, presumably because it is running a .pyc? Same happens on Ubuntu and macOS.
I don't have csh installed, but
#!/bin/sh
echo Waiting...
sleep 60
echo Change didn't happen
Run that, quickly edit the last line to read
echo Change happened
Output is
Waiting...
/home/dave/tmp/change.sh: 4: Syntax error: Unterminated quoted string
Hrmph.
I guess edits to the shell scripts don't take effect until they're rerun.
If this is all in a single script, then no it will not work. However, if you set it up as a driver script calling sub-scripts, then you might be able to change a sub-script before it's called, or before it's called again if you're looping, and in that case I believe those changes would be reflected in the execution.
I'm hearing no... but what about with some indirection:
BatchRunner.sh
Command1.sh
Command2.sh
Command1.sh
runSomething
Command2.sh
runSomethingElse
Then you should be able to edit the contents of each command file before BatchRunner gets to it right?
OR
A cleaner version would have BatchRunner look to a single file where it would consecutively run one line at a time. Then you should be able to edit this second file while the first is running right?
Use Zsh instead for your scripting.
AFAICT, Zsh does not exhibit this frustrating behavior.
usually, it uncommon to edit your script while its running. All you have to do is to put in control check for your operations. Use if/else statements to check for conditions. If something fail, then do this, else do that. That's the way to go.
Scripts don't work that way; the executing copy is independent from the source file that you are editing. Next time the script is run, it will be based on the most recently saved version of the source file.
It might be wise to break out this script into multiple files, and run them individually. This will reduce the execution time to failure. (ie, split the batch into one build flavor scripts, running each one individually to see which one is causing the trouble).

Resources