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

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.

Related

How to create an bash alias for the command "cd ~1"

In BASH, I use "pushd . " command to save the current directory on the stack.
After issuing this command in couple of different directories, I have multiple directories saved on the stack which I am able to see by issuing command "dirs".
For example, the output of "dirs" command in my current bash session is given below -
0 ~/eclipse/src
1 ~/eclipse
2 ~/parboil/src
Now, to switch to 0th directory, I issue a command "cd ~0".
I want to create a bash alias command or a function for this command.
Something like "xya 0", which will switch to 0th directory on stack.
I wrote following function to achieve this -
xya(){
cd ~$1
}
Where "$1" in above function, is the first argument passed to the function "xya".
But, I am getting the following error -
-bash: cd: ~1: No such file or directory
Can you please tell what is going wrong here ?
Generally, bash parsing happens in the following order:
brace expansion
tilde expansion
parameter, variable, arithmetic expansion; command substitution (same phase, left-to-right)
word splitting
pathname expansion
Thus, by the time your parameter is expanded, tilde expansion is already finished and will not take place again, without doing something explicit like use of eval.
If you know the risks and are willing to accept them, use eval to force parsing to restart at the beginning after the expansion of $1 is complete. The below tries to mitigate the damage should something that isn't eval-safe is passed as an argument:
xya() {
local cmd
printf -v cmd 'cd ~%q' "$1"
eval "$cmd"
}
...or, less cautiously (which is to say that the below trusts your arguments to be eval-safe):
xya() {
eval "cd ~$1"
}
You can let dirs print the absolute path for you:
xya(){
cd "$(dirs -${1-0} -l)"
}

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

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?

Bash "command not found" error

I am trying to edit my .bashrc file with a custom function to launch xwin. I want it to be able to open in multiple windows, so I decided to make a function that accepts 1 parameter: the display number. Here is my code:
function test(){
a=$(($1-0))
"xinit -- :$a -multiwindow -clipboard &"
}
The reason why I created a variable "a" to hold the input is because I suspected that the input was being read in as a string and not a number. I was hoping that taking the step where I subtract the input by 0 would convert the string into an integer, but I'm not actually sure if it will or not. Now, when I call
test 0
I am given the error
-bash: xinit -- :0 -multiwindow -clipboard &: command not found
How can I fix this? Thanks!
Because the entire quoted command is acting as the command itself:
$ "ls"
a b c
$ "ls -1"
-bash: ls -1: command not found
Get rid of the double quotation marks surrounding your xinit:
xinit -- ":$a" -multiwindow -clipboard &
In addition to the double-quotes bishop pointed out, there are several other problems with this function:
test is a standard, and very important, command. Do not redefine it! If you do, you risk having some script (or sourced file, or whatever) run:
if test $num -eq 5; then ...
Which will fire off xinit on some random window number, then continue the script as if $num was equal to 5 (whether or not it actually is). This way lies madness.
As chepner pointed out in a comment, bash doesn't really have an integer type. To it, an integer is just a string that happens to contain only digits (and maybe a "-" at the front), so converting to integer is a non-opertation. But what you might want to do is check whether the parameter got left off. You can either check whether $1 is empty (e.g. if [[ -z "$1" ]]; then echo "Usage: ..." >&2 etc), or supply a default value with e.g. ${1:-0} (in this case, "0" is used as the default).
Finally, don't use the function keyword. bash tolerates it, but it's nonstandard and doesn't do anything useful.
So, here's what I get as the cleaned-up version of the function:
launchxwin() {
xinit -- ":${1:-0}" -multiwindow -clipboard &
}
That happens because bash interprets everything inside quotes as a String. A command is an array of strings which the first element is a binary file or a internal shell command. Subsequent strings in the array are taken as argument.
When you type:
"xinit -- :$a -multiwindow -clipboard &"
the shell thinks that everything you wrote is a command. Depending on the command/program you ran all the rest of the arguments can be a single string. But mostly you use quotes only if you are passing a argument that has spaces inside like:
mkdir "My Documents"
That creates a single directory named My Documents. Also, you could escape spaces like this.
mkdir My\ Documents
But remember, "$" is a special character like "\". It gets interpreted by the shell as a variable. "$a" will be substituted by its value before executing. If you use a simple quote ('$a') it will not be interpreted by the shell.
Also, "&" is a special character that executes the command in background. You should probably pass it outside the quotes also.

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.

Prevent bash alias from evaluating statement at shell start

Say I have the following alias.
alias pwd_alias='echo `pwd`'
This alias is not "dynamic". It evaluates pwd as soon as the shell starts. Is there anyway to delay the evaluation of the expression in the ticks until the alias's runtime?
What you really want is a function, instead of an alias.
pwd_alias() {
echo "$PWD"
}
Aliases do nothing more than replace text. Anything with complexity calls for a function.
As jordanm said, aliases do nothing more than replace text.
If you want the argument of echo to be the output of pwd expanded by bash, then I don't understand your question.
If you want the argument of echo to be `pwd` with the backquotes kept, it's indeed possible, for example:
alias a="echo '\`pwd\`'"
So, if instead of echo you have something which does backquote expansion in its own runtime, maybe that's what you want.
I do not believe you can change the evaluation from occurring at shell start. Since the processes of creating the alias is run at shell start the pwd is evaluated then. You could simple change the alias to just run pwd without the back ticks as pwd outputs without the need to echo. A simple way to resolve this is to change from using an alias to a shell script in your path if you do not wish to change from using an alias.
#!/bin/bash
pwd

Resources