11

Question

Let's say I just entered this command to get a count of how many lines contain a particular string:

me@machine $ command-producing-multi-line-output | grep -i "needle" | wc -l

Now how could I quickly replace "needle" with some other word?


Current Inefficient Solution

Right now I:

  1. Press Up on the keyboard to load the last command.
  2. Press Left or Ctrl+Left until I reach "needle".
  3. Press Backspace or Ctrl+w to delete the word.
  4. Type or paste in the new word.
  5. Hit Enter.

This seems pretty inefficient.


Attempted Non-working Solutions

I've tried to research history shortcuts like !!:sg/needle/new-needle; but there are a few problems with this:

  1. You can't press Up to put !!:sg/needle/new-needle back on the command line. Doing this will just show what it expands to (running right back into the original problem).
  2. Repeating this with yet another new needle requires you to replace both needle and new-needle (i.e. !!:sg/new-needle-from-before/yet-another-new-needle).
  3. You need to type the entire needle instead of using something like !:4 or $4 (i.e. !!:sg/!:4/new-needle or !!:sg/$4/new-needle) to save time/keystrokes on however long the needle was.

I've also found things like !:^-3 "new-needle" !:5-$ but that also has issues:

  1. It expands in history so it can't be re-used quickly.
  2. Even if it didn't expand, you run into the original problem of needing to replace a word in the middle of a command chain.

I believe there has to be some super fast way to do what I want to do, and that some linux gurus out there know it. I would be very grateful for any input or suggestions on this.


EDIT:

Background

Just a bit of background, I work a lot with OpenStack on the command line and I find myself often needing to replace a parameter in several places within a long command chain of piped commands.

The way our OpenStack environments are configured, several engineers share a single stack user on any number of servers, and there are several clusters within multiple environments. So a function declared within .bashrc or .profile file isn't really possible.

I'm hoping for something portable that can be used quickly with no or very minimal setup required. Otherwise I may just need to resort to using a solution outside of the shell entirely (such as clipboard replacement in AutoHotKey/Keyboard Maestro/AutoKey, etc.).

  • 1
    You could use grep -c instead of the UUOWC and then you would have less movement on the command line to replace the word – jesse_b Dec 19 '19 at 19:50
  • Similar: https://unix.stackexchange.com/q/243317/117549 – Jeff Schaller Dec 19 '19 at 20:03
  • @Jesse_b - Learn something new everyday. I'm familiar with cat abuse, but now I'm learning about useless wc lol. Thank you! – Jeff Reeves Dec 19 '19 at 20:18
  • @JeffSchaller - I've come across that one already. It runs into one of my attempted non-working solutions. I appreciate the link though. – Jeff Reeves Dec 19 '19 at 20:18
  • I sort of understand you want to find a better way to edit long command, but maybe scripting it is better, especially if you know multiple things you need to search for in advance. Something along the lines of echo needle haystack | xargs -L1 bash -c 'command-producing-multi-line-output | grep -c -i "$1"' sh – Sergiy Kolodyazhnyy Dec 22 '19 at 08:36
  • I use vi mode in the shell (set +o vi). This does help a lot assuming you are familiar with vi. There is also a mode for emacs key bindings. In your particular scenario, it would have been <Up>Fncw<newneedle><Enter> (press Up, search for the first occurrence of "n" backwards, change the word under cursor and type the new needle, hit Enter). Using vi/emacs key bindings in the shell makes interactive sessions way faster. – Rolf Jan 01 '20 at 20:42

8 Answers8

11

That's exactly what history expansion is for:

$ command-producing-multi-line-output | grep -i "needle" | wc -l
...
$ ^needle^nipple
command-producing-multi-line-output | grep -i "nipple" | wc -l
...
  • Please see the "Attempted Non-working Solutions" section of my original post. What happens when you need to replace needle with nipple, then replace nipple with reqi-7834a-1259252-af251e13-9915-xwrz, then replace that with another long/complex string, and so on... I'm trying to avoid typing and juggling clipboard content as much as possible. – Jeff Reeves Dec 19 '19 at 21:11
  • 3
    This is a working solution for the undestandable part of your Q, for the benefit of those who come upon it via search results. If you want a reusable command, then use a function! If you're wondering "how to save the unexpanded command line in the history?", then ask a separate Q about that. For the "current unefficient" part of your Q, I suggest you read some emacs or vi tutorial, depending on which line editing mode you're using. –  Dec 19 '19 at 21:21
  • I'm sorry if I've said something to make you upset. Could you please explain what you mean by the "understandable part of your question"? – Jeff Reeves Dec 22 '19 at 02:23
  • It means exactly that: the part of your Q that I was able to understand with my poor lights. There are many things that you could do with readline keybindings to make your life easier or more miserable, but I wasn't able to make out what you're after. For instance, you can bind Ctrl-F to turn it the command line into a function named _: bind -x '"\C-f":READLINE_LINE="_(){ $READLINE_LINE; }"'; then you could just replace "needle" with "$1" and you're ready to go with _ nipple, etc. –  Dec 22 '19 at 04:26
  • But I'm absolutely not convinced that that's worth the trouble: all the tedious editing from your "current inefficient" section is just <esc>kf"cW"nipple" for me (using vi key-bindings). –  Dec 22 '19 at 04:28
10

I've run into a similar situation and present my solution here just in case it's a useful pattern.

Once I realize that I'm repeatedly changing one piece of data that's annoying to replace interactively, I'll stop and write a little while loop:

$ command-producing-multi-line-output | grep -i "needle" | wc -l
0
$ command-producing-multi-line-output | grep -i "haystack" | wc -l
0
$ while read needle; do

Up-Arrow to the previous command-producing-multi-line-output line and replace "haystack" with "$needle"

command-producing-multi-line-output | grep -i "$needle" | wc -l; done
something
0
something else
0
gold
1

(ending with Control+D)

Or the slightly fancier variation:

$ while read needle; do
printf 'For: %s\n' "$needle"
command-producing-multi-line-output | grep -i "$needle" | wc -l; done

If I see a future for this command-line beyond "today", I'll write it into a function or script.

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • Ah, that's really smart! So you open the while loop, then use history hotkeys to Up to the command you want to loop over, replace the desired parameter once, and then close the loop with done. Then the shell will keep accepting your STDIN until you Ctrl+D to terminate. That's genius and pretty much exactly what I was looking for!

    Like you, if I find that I'm repeating myself enough I just write a script or a function. But portable solutions for ad-hoc tasks like in my example are extremely handy.

    Thank you for this workflow!

    – Jeff Reeves Dec 19 '19 at 20:40
8

A useful alias in bash is

alias r='fc -s'

The r command is often found in other shells by default1, and repeats the most recent command in history. The bash manual even refers to this as a useful alias to define:

[...]  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.

It will also allow you to do replacements in the text of the last command.

Here I'm running your command, then I use the above alias to replace the word needle with the word haystack:

$ command-producing-multi-line-output | grep -i "needle" | wc -l
bash: command-producing-multi-line-output: command not found
       0
$ r needle=haystack
command-producing-multi-line-output | grep -i "haystack" | wc -l
bash: command-producing-multi-line-output: command not found
       0

When I use r needle=haystack on the command line, the shell print out the command it's going to run and then immediately runs it. As you can see, it also replaces the word.

The errors are obviously due to me not having a command-producing-multi-line-output command, but that's not important in this exercise.


The fc command won't get saved to your history, but you can make it get saved by creating it as a shell function like this:

fc() {
    command fc "$@"
    history -s fc "$@"   # append the given fc command to history
}

You may then do

$ command-producing-multi-line-output | grep -i "needle" | wc -l
bash: command-producing-multi-line-output: command not found
       0

Rerun the most recent command starting with comm and replace needle using r needle=haystack comm:

$ r needle=haystack comm
command-producing-multi-line-output | grep -i "haystack" | wc -l
bash: command-producing-multi-line-output: command not found
       0

Rerun the most recent command, but replace haystack, using r haystack=beeswax:

$ r haystack=beeswax
fc -s needle=beeswax comm
command-producing-multi-line-output | grep -i "beeswax" | wc -l
bash: command-producing-multi-line-output: command not found
       0

Note the double call that gets made to fc in that last line, first through our alias r and then through our function fc.

Obviously, saving the fc command to history makes you run the risk of accidentally calling fc -s recursively.


1In zsh it's a built-in command, in OpenBSD ksh it's a default alias for fc -s, in ksh93 it a default alias for hist -s, etc.

Kusalananda
  • 333,661
  • This seems like a useful approach, but I still run into an issue.

    Let's say I do r needle=haystack but then I need to check for something else, then I'm doing r haystack=new-needle, etc. It's not a big deal when it's a single word, but when you start having to search for 16 digit GUIDs or long strings with a lot of mixed characters, it becomes tedious to replace the needle portion every subsequent run.

    – Jeff Reeves Dec 19 '19 at 20:16
  • 1
    @JeffReeves: It seems like you might want to just wrap this command into a function – jesse_b Dec 19 '19 at 20:18
  • my_command () { command-producing-multi-line-output | grep -i "$1" | wc -l; }. Then you could just run: my_command needle, my_command haystack, my_command foo – jesse_b Dec 19 '19 at 20:20
  • @Jesse_b I could do that, but I was hoping for something more portable for a variety of situations. A lot of my work is with OpenStack, and the position of a particular parameter may be anywhere in a long chain of piped commands. I guess I could make a function for replacement of parameters, but then I run into an issue where I would need to have that function declared on any number of thousands of servers I'm managing from a shared stack or heat-admin user account (I know, not best practices, but I didn't set it up). – Jeff Reeves Dec 19 '19 at 20:23
  • @JeffReeves What Jesse_b just said. The history expansions are for quick rewrites and for correcting stuff you got wrong. If you have actual workflows that require certain input, I'd too suggest writing a shell function to do stuff like this, or a full-blown shell script. Relying on history expansion to work portably is not something I would bet my salary on. If you additionally want to do this reliably across, as you say, thousands of systems, I would look into more robust software like Ansible for doing management tasks. – Kusalananda Dec 19 '19 at 20:24
  • 1
    @JeffReeves See update. I think I solved some of your issues. – Kusalananda Dec 19 '19 at 20:42
  • @Kusalananda You certainly gave me some food for thought on how to approach a more robust solution in situations where I can have aliases and functions defined. Thank you!

    Also, I typically write a full script in either bash or python, and do use Ansible for heavy lifting. I'm just needing something portable for those one off situations when someone PMs me to look something up for them, and it's not worth the effort to write out a full script, put it into version control, clone/pull it down to the servers I need it on, etc.

    – Jeff Reeves Dec 19 '19 at 21:13
5

The OP says that a function declared in .bashrc is not practical, but there is nothing stopping declaring one interactively. As this is for just the current session a single letter name is suitable, e.g. q.

Starting with

$ command-producing-multi-line-output | grep -i "haystack" | wc -l

press Up ;} to end a function, edit the haystack to $1, Control+aq(){Space to get

q(){ command-producing-multi-line-output | grep -i "$1" | wc -l;}

then you can use q needle, q haystack, q something. I prefer this over the while solution in the currently accepted answer as it allows other commands in-between the searching/counting.

For this particular case I would actually define

q(){ command-producing-multi-line-output | grep -i "$@";}

as I would probably want to see the actually matching lines at some stage, so I could use q needle | wc -l or the better q -c needle to count, and q needle to see the lines.

As the function definition is going to be stored in the bash history, it is usually possible to reload the definition if it is needed in a future session.

icarus
  • 17,920
  • 1
    I do like the portability of this solution. It's pretty easy to load the last command, append ;}, then Ctrl+a to go to the start of the line, give it a one letter function name, and then change whatever parameter is needed into a positional variable (or all variables). Thanks for the input! – Jeff Reeves Dec 22 '19 at 02:26
4

Summary: Ctrl+R nee Alt+D haystack Enter

Press Ctrl+R to start a reverse incremental search and type a few characters of the text you want to replace (or some text very close to it) until the desired part of the desired command line appears (it doesn't have to be the previous command line). Press Ctrl+R to search for the previous occurrence. Press Ctrl+S to go forward if you've gone back too far. Press Backspace if you've mistyped a character.

Once you've reached the place you want, you can use keys like Delete, Alt+D or Alt+Backspace to start deleting text around the cursor, or just Escape to exit the incremental search at the point you've reached.

Press Enter only when you've finished editing the command line. Note that Enter during an incremental search has its normal meaning to execute the current command line as it is: it doesn't just exit search mode (press Escape to just exit search mode).

Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255
  • Oh wow! I've know about reverse and forward search in history with Ctrl+R/S, but using Alt+D to replace the needle with haystack is awesome! I'll also mention to new users that sometimes history search gets stuck, when that happens you can press Ctrl+C to send SIGINT and stop the search process. – Jeff Reeves Dec 21 '19 at 18:17
0

Open a proper editor which is built for this stuff. By default, you can do this with Ctrl-x Ctrl-e, though if you have enabled vi editing mode (one way is set -o vi), you can also use Esc v.

Once there, use your editor’s substitution command. In vi(m), e.g.,

:%substitute/needle/haystack/g
0

In these situations I often use an environment variable for the parameter as follows:

ARG1=needle; command-producing-multi-line-output | grep -i "${ARG1}" | wc -l

The first assignment sets an environment variable which can then be used in subsequent commands. This serves as the parameter which can be changed as needed. Note this will set an environment variable for the life of this shell, but I have found for quick, specific commands this is ok. The usual rules apply for variable expansion, such as single quotes '${ARG1}' will not expand the variable. General information can be found on this (here is one) https://www.gnu.org/software/bash/manual/html_node/Quoting.html

Paul
  • 319
0

I find myself using fzf for this.

If you're willing to install something like fzf, and your output is normally less than a screen, try this:

echo | fzf -q 'shuf < /usr/share/dict/words | grep needle | head' --preview-window=up:99% --preview="eval {q}"

Once the output pops up, cursor to the end of the word "needle", hit Ctrl-W and type haystack. Then hit Ctrl-W again and type some other word. And so on.

However, I would not advise this for any commands that change the system :-) You're using fzf's preview feature to run the command, and fzf attempts to run it as soon as anything changes. For example, when you hit Ctrl-W you'll see grep showing it's usage message because momentarily, until you type the first character of the replacement word, there is no argument to it.

I use this so often I have a script called try, which is basically this:

#!/bin/bash

export FZF_DEFAULT_COMMAND=echo
fzf -q "$*" --preview-window=up:99% --preview="eval {q}"