Bash history re-runs: possible command to avoid using !bang!? - bash

Scenario:
You are doing your daily Bash shell stuff. You want to run a previous command so you type:
history | grep foocommand
Then you get a list of all the foocommand stuff you did for however long your history has kept track, in a list like so:
585 foocommand --baz --bleet
750 foocommand | grep quux
987 history grep | foocommand
You decide you want to run command 585, so you type
!585
Question: Just for curiosity, is there a way to take that final step out of the equation and still get the expected outcome? It would be nice if there were a way to say:
"grep through history and automatically run the first item on the list"
or
"grep through history and let me choose which item to run using the arrow keys"

Press ^r (that's CTLR-r), type foocommand press Enter ;)
That will search history and show the latest matching entry

Did you try '!f'? 'f' as first letter of the foocommand.

Personally, I use an alias derived from ksh:
alias r="fc -e -"
('r' for 'rerun', I suppose...)
I can rerun the last command starting with 'foo':
r foo
I can rerun commands 46 to 50:
r 46 50
I can rerun just command 585:
r 585
The only gotcha is that if you typed:
$ cd somewhere
then running 'r cd' won't work because there was an extra space in front of the 'cd' command. The 'fc' command underlies history, and you can edit commands 46 to 50 before rerunning them by typing:
fc 46 50
and so on for the other variants.
(The '-e -' in the alias means 'edit with the null editor'; you could write 'fc -e vim' to edit with vim, but most people set VISUAL or EDITOR or FCEDIT or all three to make that unnecessary.)
For the rest, being a 'vim' person (fanatic?), I use 'set -o vim' and then the search facility: ESC and
/grep\ -e\ 'whatever
to search history for a command containing "grep -e 'whatever". I can repeat the search, moving ever further backwards in history, or reverse direction after overshooting, or ... I assume that the emacs mode has an equivalent search mechanism.

The syntax
!foo
will run the last command that began with foo.
Failing that, bash uses the readline library for input, which supports the history-search-forward and history-search-backward commands that can be bound to keys of your choice. I've edited my ./inputrc file to bind them to F8 and Shift-F8, so that they work like Windows' equivalent feature in the console when I connect with PuTTY:
"\e[19~":history-search-backward
"\e[32~":history-search-forward

"grep through history and automatically run the first item on the list"
Okay.
`history | grep foocommand | head -1 | tr -s ' ' | cut -d ' ' -f 3-`
Give it a try, it works like a champ!

Related

Shell script not working while writing the same on the Terminal works [duplicate]

I have a simple one-liner that works perfectly in the terminal:
history | sort -k2 | uniq -c --skip-fields=1 | sort -r -g | head
What it does: Gives out the 10 most frequently used commands by the user recently. (Don't ask me why I would want to achieve such a thing)
I fire up an editor and type the same with a #!/bin/bash in the beginning:
#!/bin/bash
history | sort -k2 | uniq -c --skip-fields=1 | sort -r -g | head
And say I save it as script.sh. Then when I go to the same terminal, type bash script.sh and hit Enter, nothing happens.
What I have tried so far: Googling. Many people have similar pains but they got resolved by a sudo su or adding/removing spaces. None of this worked for me. Any idea where I might be going wrong?
Edit:
I would want to do this from the terminal itself. The system on which this script would run may or may not provide permissions to change files in the home folder.
Another question as suggested by BryceAtNetwork23, what is so special about the history command that prevents us from executing it?
Looking at your history only makes sense in an interactive shell. Make that command a function instead of a standalone script. In your ~/.bashrc, put
popular_history() {
history | sort -k2 | uniq -c --skip-fields=1 | sort -r -g | head
}
To use history from a non-interactive shell, you need to enable it; it is only on by default for interactive shells. You can add the following line to the shell script:
set -o history
It still appears that only interactive shells will read the default history file by, well, default, so you'll need to populate the history list explicitly with the next line:
history -r ~/.bash_history
(Read the bash man page for more information on using a file other than the default .bash_history.)
History command is disabled by default on bash script, that's why even
history command won't work in .sh file. for its redirection. Kindly
redirect bash_history file inside the .sh file.
History mechanism can be enabled also by mentioning history file and change run-time parameters as mentioned below
#!/bin/bash
HISTFILE=~/.bash_history
set -o history
Note: mentioned above two lines on the top of the script file. Now history command will work in history.

How do I open a file in VS Code terminal by partially matching the file name?

If I have a file named w5_align_example.cpp, how do I open that file in VS Code integrated terminal by only supplying the word align?
code w5_align_sample.cpp would open it but I sometimes only remember the keyword align unless I search in a separate command to see what the file begins with. I want to open in a single command instead.
I've tried:
$ ls | grep "align" | code which gives me Run with 'code -' to read output from another program (e.g. 'echo Hello World | code -'). error.
$ ls | grep "align" | code - opens up a new file called code-stdin-sfd.txt with the text w5_align_example.cpp inside.
What would be the simplest (i.e. shortest) command to do this?
ls | grep "align" | xargs -I{} code {}
or
code $(ls | grep "align")
You can just use *. It matches any string and can be used multiple times.
code *align*
In some shells, you can combine this with tab completion. Just type:
code *align*
And then press Tab. This will fill in the rest of the file name, but it will beep if there is more than one option.

Paste the last output and edit it in bash

I like to use bash (on linux) without touching mouse.
I often encounter the following situation.
$ locate libfreetype.a
/usr/lib/x86_64-linux-gnu/libfreetype.a
$ cd /usr/lib/x86_64-linux-gnu
In this case, I copy /usr/lib/x86_64-linux-gnu/ and paste it using mouse or type it. I do not want to do that.
Ideally, the output of locate libfreetype.a is stored in somewhere (maybe in killring??) and paste it with C-y command and edit it on terminal.
Are there good way to do this?
(Just for this example case, there are smart one-line commands. But those are not the desired answers. I want a general solution.)
Another example
Suppose that I remember that there is a memo... in the same directory as libfreetype.a but I forgot the directory name.
$ locate libfreetype.a
/usr/lib/x86_64-linux-gnu/libfreetype.a
$ nano /usr/lib/x86_64-linux-gnu/memo # Tab completion here
$ nano /usr/lib/x86_64-linux-gnu/memo_xxx.txt
if I could cache the output /usr/lib/x86_64-linux-gnu/libfreetype.a and paste it, things are very easy.
(nano $(dirname $(locate libfreetype.a))/memo_xxx.txt works for this case, but if I want to change the path itself, I need to think another technique.)
As noted in the comments, there probably no common way to do this in terminal. But it's possible to redirect the output of the command to program that copy stdin to clipboard, e. g. xclip. If you want to insert and edit copied text in terminal, you need to remove newline characters before copying. Consider following script:
copy.bash
#!/bin/bash
tr '\n' ' ' | xclip
Usage:
$ locate libfreetype.a | copy
$ cd # now press <shift> + <insert>
$ cd /usr/lib/x86_64-linux-gnu/libfreetype.a # continue editing
The xclip command copies its input for pasting into X applications.
The tr '\n' ' ' command translates all newlines into spaces. You need this if you want to paste the text into command line. It strips the trailing newline and joins lines if output contains more than one. If use plain xclip all newline characters are pasted literally, which causes bash to run command immediately after pasting and doesn't allow to edit it.
If output of the command (e. g. locate) is multi-line and you want to choose only one of them to copy (instead of copying all), you can use iselect. iselect reads input and shows the interactive menu for selecting a line/lines and prints it to the standart output.
Use it like this:
$ locate pattern | iselect -a | tr '\n' ' ' | xlip
# locate prints several lines
# iselect allows user to select one line interactively
# the result is copied to clipboard
$ # <shift> + <insert>
This also can be a script:
icopy.bash
#!/bin/bash
iselect -am | tr '\n' ' ' | xclip
(the -m option allows to choose several lines instead of one)
Usage:
$ locate pattern | icopy
Disadvantages of these approaches:
it works only with X sessions since xlcip need the X session to be running
you need to install new software (xclip and, optionally, iselect)
you need to redirect output explicitly, before running the command; so, technically, it cannot be considered as answer. But it is the best solution I have found for myself.
BTW, here is the script on my local machine that I really use quite often:
$ cat ~/bin/copy
#!/bin/bash
paste -sd\ | tr -d '\n' | xsel --clipboard
echo "Copied: $(xsel --clipboard --output)" >&2
$ echo hello | copy
Copied: hello
Links: man iselect, man xclip, man tr, yank.
You can run script (man script) from your .bashrc which generates a live log of your session's output. And bind a shortcut for opening the log file in an editor, so you can insert yanked text back into $READLINE_LINE.
But script captures raw output from interactive programs (such as editors), so if script could be modified to skip interactive output, it would work. Next step would be parsing output, to make navigation faster.
Here is a .bashrc snippet that does this for non-interactive tools only: https://asciinema.org/a/395092
I noticed that a solution to this problem is given by a terminal emulator kitty. We can use a feature called "hints" and keyboard shortcuts configured by default.
As in the original question, let's think of the situation.
$ locate libfreetype.a
/usr/lib/x86_64-linux-gnu/libfreetype.a
$ # you want to input /usr/lib/x86_64-linux-gnu here
If you are using kitty, you can type ctrl+shift+p and then l.
You will enter a mode to select a line from the screen. When you can select the previous line, it is pasted into the current terminal input.
The detail if found in the official documentation.
The configuration associated with the action is written like this.
map ctrl+shift+p>l kitten hints --type line --program -
This means that kitten hints --type line --program - is the command mapped from ctrl+shift+p followed by l.
you could use
!$
like
shell$ echo myDir/
myDir/
shell$ cd !$
cd myDir/
shell$ pwd
/home/myDir

Fast way to edit nth line of previous command output

I often find find myself doing a workflow like this:
$ find . |grep somefile
./tmp/somefile.xml
./test/another-somefile.txt
(review output)
$ vim ./tmp/somefile.xml
Now, it would be neat if there was some convenient way of using the output of the find command and feed it to vim.
The best I've come up with is:
$ nth () { sed -n $1p; }
$ find . |grep somefile
./tmp/somefile.xml
./test/another-somefile.txt
(review output)
$ vim `!!|nth 2`
I was wondering if there are other, maybe prettier, ways of accomplishing the same thing?
To clarify, I want a convenient way of grabbing the nth line from a previously run command to quickly open that file for editing in vim, without having to cut & paste the filename with the mouse or tab-complete my way through the file path.
way 1: don't pass exact file to vim, but the whole output. choose the file in vim
currently you are working in two steps:
1 - launch the find/grep... cmd
2 - vim !!....
if you are sure that you want to use vim to open one (or more) file(s) from the find result. you may try:
find. (with grep if you like) |vim -
then you have the whole output in vim, now you can use vim magic to move cursor to the file you want to edit, then press gf. (I do this sometimes)
way 2: refine your regex in your find (or grep), to get the single file, that you want to edit.
this is not a hard thing at all. then you can just vim !!.
your nth() is nice. however imagine there are 30 lines in output, and your file sits in the line# 16. how do you count it? sure you can add |nl at the end, then you cannot directly use !! any longer..
just my 2 cents
Modified after your comment. Not sure if it's "convenient" though..
command | tail -n3 | head -n1 | xargs vim
Maybe this is what you're looking for?
find . -name "*somefile*" -exec vim -p {} \;
If you want an interactive review maybe you can use something like this:
TMP_LIST=""; for i in `find . | grep somefile`; do echo $i; read -p "(y/n)?"; [ $REPLY == "y" ] && TMP_LIST="$TMP_LIST $i"; done; vim $TMP_LIST
You almost did it!!
pearl.251> cat file1
a b c d e f pearl.252> find . -name "file*"
./file1
./file2
./file3
./file4
./file5
./file6
./file7
pearl.253> vi `!!|awk 'NR==1'`
the last line overe here will open the file1 in vi.

bash: shortest way to get n-th column of output

Let's say that during your workday you repeatedly encounter the following form of columnized output from some command in bash (in my case from executing svn st in my Rails working directory):
? changes.patch
M app/models/superman.rb
A app/models/superwoman.rb
in order to work with the output of your command - in this case the filenames - some sort of parsing is required so that the second column can be used as input for the next command.
What I've been doing is to use awk to get at the second column, e.g. when I want to remove all files (not that that's a typical usecase :), I would do:
svn st | awk '{print $2}' | xargs rm
Since I type this a lot, a natural question is: is there a shorter (thus cooler) way of accomplishing this in bash?
NOTE:
What I am asking is essentially a shell command question even though my concrete example is on my svn workflow. If you feel that workflow is silly and suggest an alternative approach, I probably won't vote you down, but others might, since the question here is really how to get the n-th column command output in bash, in the shortest manner possible. Thanks :)
You can use cut to access the second field:
cut -f2
Edit:
Sorry, didn't realise that SVN doesn't use tabs in its output, so that's a bit useless. You can tailor cut to the output but it's a bit fragile - something like cut -c 10- would work, but the exact value will depend on your setup.
Another option is something like: sed 's/.\s\+//'
To accomplish the same thing as:
svn st | awk '{print $2}' | xargs rm
using only bash you can use:
svn st | while read a b; do rm "$b"; done
Granted, it's not shorter, but it's a bit more efficient and it handles whitespace in your filenames correctly.
I found myself in the same situation and ended up adding these aliases to my .profile file:
alias c1="awk '{print \$1}'"
alias c2="awk '{print \$2}'"
alias c3="awk '{print \$3}'"
alias c4="awk '{print \$4}'"
alias c5="awk '{print \$5}'"
alias c6="awk '{print \$6}'"
alias c7="awk '{print \$7}'"
alias c8="awk '{print \$8}'"
alias c9="awk '{print \$9}'"
Which allows me to write things like this:
svn st | c2 | xargs rm
Try the zsh. It supports suffix alias, so you can define X in your .zshrc to be
alias -g X="| cut -d' ' -f2"
then you can do:
cat file X
You can take it one step further and define it for the nth column:
alias -g X2="| cut -d' ' -f2"
alias -g X1="| cut -d' ' -f1"
alias -g X3="| cut -d' ' -f3"
which will output the nth column of file "file". You can do this for grep output or less output, too. This is very handy and a killer feature of the zsh.
You can go one step further and define D to be:
alias -g D="|xargs rm"
Now you can type:
cat file X1 D
to delete all files mentioned in the first column of file "file".
If you know the bash, the zsh is not much of a change except for some new features.
HTH Chris
Because you seem to be unfamiliar with scripts, here is an example.
#!/bin/sh
# usage: svn st | x 2 | xargs rm
col=$1
shift
awk -v col="$col" '{print $col}' "${#--}"
If you save this in ~/bin/x and make sure ~/bin is in your PATH (now that is something you can and should put in your .bashrc) you have the shortest possible command for generally extracting column n; x n.
The script should do proper error checking and bail if invoked with a non-numeric argument or the incorrect number of arguments, etc; but expanding on this bare-bones essential version will be in unit 102.
Maybe you will want to extend the script to allow a different column delimiter. Awk by default parses input into fields on whitespace; to use a different delimiter, use -F ':' where : is the new delimiter. Implementing this as an option to the script makes it slightly longer, so I'm leaving that as an exercise for the reader.
Usage
Given a file file:
1 2 3
4 5 6
You can either pass it via stdin (using a useless cat merely as a placeholder for something more useful);
$ cat file | sh script.sh 2
2
5
Or provide it as an argument to the script:
$ sh script.sh 2 file
2
5
Here, sh script.sh is assuming that the script is saved as script.sh in the current directory; if you save it with a more useful name somewhere in your PATH and mark it executable, as in the instructions above, obviously use the useful name instead (and no sh).
It looks like you already have a solution. To make things easier, why not just put your command in a bash script (with a short name) and just run that instead of typing out that 'long' command every time?
If you are ok with manually selecting the column, you could be very fast using pick:
svn st | pick | xargs rm
Just go to any cell of the 2nd column, press c and then hit enter
Note, that file path does not have to be in second column of svn st output. For example if you modify file, and modify it's property, it will be 3rd column.
See possible output examples in:
svn help st
Example output:
M wc/bar.c
A + wc/qax.c
I suggest to cut first 8 characters by:
svn st | cut -c8- | while read FILE; do echo whatever with "$FILE"; done
If you want to be 100% sure, and deal with fancy filenames with white space at the end for example, you need to parse xml output:
svn st --xml | grep -o 'path=".*"' | sed 's/^path="//; s/"$//'
Of course you may want to use some real XML parser instead of grep/sed.

Resources