What does !!:2 mean in Mac or Linux? - bash

Today I saw a command in Mac:
touch !!:2/{f1.txt, f2.txt}
I know the use of touch command but what does !!:2 does in this command. I don't have Mac and tried in Linux It is giving some weird output. If anyone could explain more expression like this it would be great.

touch updates file timestamp (to current time, given no arguments)
!! is 'History expansion' operation, retrieving previous command from bash history log in this form (two exclamation dots), alias for '!-1'
:2 is word specifier, retrieving 2nd command argument. E.g. if previous history command was ls -l /tmp, !!:2 will render to '/tmp'
{f1.txt,f2.txt} is called 'Brace expansion'. Brace expansion requires single word string without unescaped white spaces (it's definitely a typo in the question). For example, foo{bar,baz} will be expanded to 'foobar foobaz'
So, let's assume we run bash command
ls -l /tmp
Now, touch !!:2/{f1.txt,f2.txt} will produce
touch /tmp/f1.txt /tmp/f2.txt

https://tiswww.case.edu/php/chet/bash/bashref.html
!! refers to the previous command. This is a synonym for ‘!-1’.
:2 refers to the second argument.
So for example :
echo "content" > foo
cp foo bar
cat !!:2
Displays the content of bar.
!!:2 is the second argument of the previous command.
Which one was it in your example?

Related

What does `$*`(a dollar sign followed by a star) mean in the default setting of `grepprg` in vim?

In a vanilla vim on Mac, when I type :set grepprg?, it returns the following:
grepprg=grep -n $* /dev/null.
I understand what -n and /dev/null means,
thanks to an old question here.
I also understand what $ and * means individually.
However, I am not sure what to make of $*.
I tried to look it up in the vim doc,
but all that I could find was
The placeholder "$*" is allowed to specify where the arguments will be included.
I sense that I am missing some important connection here.
I would really appreciate if someone could explain to me
how $* works as a placeholder.
Update:
Thanks to the detailed explanation from #romainl,
I realized that I was misinterpreting $* as regex,
whereas they are part of the convention in shell script.
In fact, there already exists old post
about this particular convention.
Silly me!
I'm not sure what kind of explanation is needed beyond what you have already quoted:
The placeholder "$*" is allowed to specify where the arguments will be included.
$* is just a placeholder and it works like all placeholders: before being actually sent to the shell, the command is built out of &grepprg and $*, if present, is replaced by any pattern, filename, flags, etc. provided by the user.
Say you want to search for foo\ bar in all JavaScript files under the current directory. The command would be:
:grep 'foo\ bar' *.js
After you press <CR>, Vim grabs any argument you gave to :grep, in this case:
'foo\ bar' *.js
then, if there is a $* in &grepprg, it is replaced with the given argument:
grep -n 'foo\ bar' *.js /dev/null
or, if there is no $* in &grepprg, the given argument is appended to it, and only then sends the whole command to a shell.
$* means "in this command, I specifically want the user-provided arguments to appear here".
As for the meaning of $*… $ and * have no intrinsic meaning and $* could have been $$$PLACEHOLDER$$$ or anything. $* may have been chosen because it is used in shell script to represent all the arguments given to a function or script, which is somewhat close in meaning to what is happening in &grepprg with $*.

Why do csh(tcsh) aliases not work when defined and invoked on one line?

[Please do not recommend I not use csh. The Electrical Engineering community is inextricably bound to it. Thanks!]
When I do this:
csh -f -c "alias foo bar; foo"
I get:
foo: Command not found.
Similarly, when I do this:
#!/bin/csh -f
alias foo bar; foo
I get the same foo: Command not found.. However, this works as expected:
#!/bin/csh -f
alias foo bar
foo
Which gives:
bar: Command not found.
Is this just a bug in csh/tcsh? Or is this intentional? I have to deal with aliases since the environment I'm using depends heavily on them to configure toolsets (including modules). But this basically means I can't invoke short csh scripts with csh -f -c. I have to dump the commands to a file, chmod +x, and invoke it. Not a huge deal. But I'm just wondering if there's a way to trick this buggy/quirky shell into recognizing aliases defined on the same line.
This is documented behavior of csh: alias substitution happens after each input line is read and split into commands, but before any of the commands are executed (including your alias command).
From csh(1) manual:
Alias substitution
The shell maintains a list of aliases that can be established, displayed
and modified by the alias and unalias commands. After a command line is scanned, it is parsed into distinct commands and the first word of each
command, left-to-right, is checked to see if it has an alias. If it
does, then the text that is the alias for that command is reread with the
history mechanism available as though that command were the previous
input line. The resulting words replace the command and argument list.
If no reference is made to the history list, then the argument list is
left unchanged.

Bash shortcut to get parent of last used parameter

In my workflow I do very usually:
cat this/very/long/filename.txt
cd !$
bash: cd: this/very/long/filename.txt: Not a directory
Which is to be expected :(
And now I recover the last command, remove manually the file part, and repeat the cd, which now works. That is too much typing!
It would be sooo nice if there was a bash shortcut like:
cd !§
Which could give me the parent of the last used parameter. I know !§ does not exist, I just wished it did! Is there something which can satisfy this?
Et voilà. History modifiers come to the rescue!
$ cd !!:$:h
which can be abbreviated to
$ cd !$:h
This command takes the last argument of the previous command, and removes the trailing path name.
In more details:
!! expands to the previous command
:$ expands to the last argument of the previous command
:h takes the header; that is, removes the file name (which is the trailing part of the above last argument)
As an aside,
!!:$:t
does exactly the opposite.
For an in-depth discussion, please refer to the Bash documentation.
This shorter version would also work:
cd !$:h
Details:
!$ is synonymous to !!:$ which presents the last argument of the previous command.
: separates the modifier from the event-word designator.
h is the modifier that removes the trailing file name component.
Building on the answer presented by several other people,
the ideal workflow is to type cd !$:h after the cat this/very/long/filename.txt command. 
This answer, cd !$:h, will still work after cd !$, but,  once you’ve made that mistake,
you can use the even shorter !:h, which repeats the last command (cd this/very/long/filename.txt),
but taking only the head of the last word.
I'm using these little-known shorthands:
!word_designator is equivalent to !!:word_designator
for non-numeric word_designators (e.g., !$ is equivalent to !!:$) and
!:modifier is equivalent to !!:modifier
(e.g., !:h is equivalent to !!:h).
In this case, I usually do: (I have emacs key binding)
cd Alt-dotAlt-b(n times)Ctrl-K
better:
cdAlt-dotALT-Backspace(n times)
note alt-b or ALT-Bs could be pressed multiple times, till you are satisfied with the path.
cat this/very/long/filename.txt
cd !!^:h
cd this/very/long
!! refers to the last command
^ first argument, second word(word designators)
:h remove trailing pathname component, leaving the head(modifers).
Instead of navigating through the history, you can also consider the use of the $_ variable to refer to the last argument of the previous command (see _ under the Special Parameters section of bash(1)).
To further strip the filename component ("everything after, and including, the last slash"), use parameter expansion (Remove matching suffix pattern):
$ cat this/very/long/filename.txt
$ cd ${_%/*}
The last command will be cd this/very/long since /filename.txt got stripped. A difference with history expansion is that this command (cd ${_%/*}) gets added to the history.

How can I recall a command using ! without executing it immediately?

In Bash, typing for example !make followed by Enter will recall and also execute the last command that starts with "make" in the history.
What I'd like is to just recall that command, but not evaluate it right away until I hit Enter again, to allow me to look at it, or edit it if needed.
Is there a way to do that in Bash?
You can use CTRL+p to get the previous command
You can also use CRL+r to reverse search for a command in your history.
The p modifier after any history expansion prints the result to the command line for further editing, rather than executing it.
$ echo foo
foo
$ !!:p
$ echo foo
^
|
cursor remains here
You can expand history on the current line using the history-expand-line key (M-^).
The M modifier key is usually mapped to Alt.
$ !make
AltShift6
$ make -C mydir
if your last make command was make -C mydir. You can then edit the command line in place.
Alternatively, you can scroll through history as outlined in other answers.

Can I have a shell alias evaluate a history substitution command?

I'm trying to write an alias for cd !!:1, which takes the 2nd word of the previous command, and changes to the directory of that name. For instance, if I type
rails new_project
cd !!:1
the second line will cd into the "new_project" directory.
Since !!:1 is awkward to type (even though it's short, it requires three SHIFTed keys, on opposite sides of of the keyboard, and then an unSHIFTed version of the key that was typed twice SHIFTed), I want to just type something like
cd-
but since the !!:1 is evaluated on the command line, I (OBVIOUSLY) can't just do
alias cd-=!!:1
or I'd be saving an alias that contained "new_project" hard-coded into it. So I tried
alias cd-='!!:1'
The problem with this is that the !!:1 is NEVER evaluated, and I get a message that no directory named !!:1 exists. How can I make an alias where the history substitution is evaluated AT THE TIME I ISSUE THE ALIAS COMMAND, not when I define the alias, and not never?
(I've tried this in both bash and zsh, and get the same results in both.)
For bash:
alias cd-='cd $(history -p !!:1)'
Another way to accomplish the same thing:
For the last argument:
cd Alt-.
or
cd Esc .
For the first argument:
cd Alt-Ctrl-y
or
cd Esc Ctrl-y
For zsh:
alias cd-='cd ${${(z)$(fc -l -1)}[3]}'
How this works:
$(fc -l -1) is evaluated. fc -l {start} [{end}] means «list history commands from {start} till {end} or last if {end} is not present».
${(z)...} must split ... into an array just like the shell does (see «Parameter Expansion Flags» in man zshexpn), but in fact it splits on blanks. Maybe it is only my bug.
${...[3]} takes third value from the array. First value is a number of a command, second is command and third and later are arguments.

Resources