Mid-command alias/function - bash

Since I use && !! a lot, I wonder, is there a way to shorten it (right now I need to move a lot for it).
Example use case:
$ alias ¤¤='\&\& \!\!' # This is not a mid-command alias
$ echo a
a
$ echo b ¤¤
b
a

bash has no equivalent to zsh global aliases (which can expand anywhere in a command like you want). A Readline macro may be more appropriate:
$ bind '"¤¤": "&& !!"'
$ echo a
$ echo b ¤¤
b
a
Note that when you type ¤¤, Readline will immediately replace those two characters with && !! instead (which is hard to demonstrate here).
To make sure this is always available in your shell, either add
bind '"¤¤": "&& !!"'
to your .bashrc file, or add
$if Bash
"¤¤": "&& !!"
$endif
to your .inputrc file. (Note that .inputrc is read by programs other than bash, and this particular macro may not be worth defining for other programs.)

Related

Expand An Alias That Executes Another Alias (Nested Alias)

I have two aliases:
alias ls="ls -G"
alias la="ls -aFhlT"
I know that after you type your alias, but before you execute, you can type Meta-Control-e (probably Alt-Control-e, but possibly Esc-Control-e) to expand what you've typed.
So, if I expand my alias la using this method I get:
ls -aFhlT
However, what I really want is to see:
ls -G -aFhlT
Is there any way to achieve this besides typing Meta-Control-e a second time?
--OR--
Is there any way to confirm that my execution of la actually executed ls -G -aFhlT (other than knowing how nested aliases work and trusting that it did what I think it did)?
I'm trying to do this on macOS, but a general bash solution will also be accepted.
This question rides the fine line between using an alias and using a function. When aliases get even slightly complicated, it is generally better to write a function instead. That being said, I did find a solution for this question that allows for expanding aliases as desired.
I wrote a bash function for this:
xtrace() {
local eval_cmd
printf -v eval_cmd '%q ' "${#}"
{ set -x
eval "${eval_cmd}"
} 2>&1 | grep '^++'
return "${PIPESTATUS[0]}"
}
The -v flag of printf will store the output of printf in the specified variable.
The printf format string %q will print the associated argument ($# in this case) shell-quoted, reusable as input. This eliminates the dangers associated with passing arbitrary code/commands to eval.
I then use a command group { ... } so I can control the functionality of set -x, which tells bash to print a trace of all executed commands. For my purposes, I do not care about any output except for the fully expanded command, so I redirect stderr and grep for the output line that starts with "++". This will be the line that shows the fully expanded command.
Finally, I return the value of PIPESTATUS[0], which contains the return code of the last command executed in the command group (i.e. the eval command).
Thus, we will get something like the following:
$ xtrace la; echo $?
++ ls -G -aFhlT
0
Much thanks to #CharlesDuffy for the set -x recommendation as well as the input sanitation for eval.

Wildcard that executes command once for each match

Alternate title: How to loop without a loop or xargs.
Recently, I switched to zsh because of its many features. I'm curious: Is there a feature which expands wildcards such that the command is executed once for each match instead of only one time for all matches at once.
Example
The command ebook-convert input_file output_file [options] accepts just one input file. When I want to convert multiple files, I have to execute the command multiple times manually or use a loop, for instance:
for i in *.epub; do
ebook-convert "$i" .mobi
done
What I'd like is a wildcard that functions like the loop so that I can save a few keystrokes. Let said wildcard be ⁂. The command
ebook-convert ⁂.epub .mobi
should expand to
ebook-convert 1stMatch.epub .mobi
ebook-convert 2ndMatch.epub .mobi
ebook-convert 3rdMatch.epub .mobi
...
Still interested in other answers
I accepted an answer that works for me (thanks to Grisha Levit). But if you know other shells with such a feature, alternative commands which are shorter than writing a loop, or even a way to extend zsh with the wanted wildcard your answers are appreciated.
so that I can save a few keystrokes
OK, so let's say you typed out
ebook-convert *.epub .mobi
…and now you realized that this isn't going to work — you need to write a loop. What would you normally do? Probably something like:
add ; done to the end of the line
hit CtrlA to go the beginning of the line
type for i in…
etc…
This looks like a good fit for readline keyboard macro:
Let's write this out the steps in terms of readline commands and regular keypresses:
end-of-line # (start from the end for consistency)
; done # type in the loop closing statement
character-search-backward * # go back to the where the glob is
shell-backward-word # (in case the glob is in the mid-word)
shell-kill-word # "cut" the word with the glob
"$i" # type the loop variable
beginning-of-line # go back to the start of the line
for i in # type the beginning of the loop opening
yank # "paste" the word with the glob
; do # type the end of the loop opening
Creating the binding:
For any readline command used above that does not have a key-binding, we need to create one. We also need to create a binding for the new macro that we are creating.
Unless you've already done a lot of readline customization, running the commands below will set the bindings up for the current shell. This uses default bindings like \C-e ➙ end-of-line.
bind '"\eB": shell-backward-word'
bind '"\eD": shell-kill-word'
bind '"\C-i": "\C-e; done\e\C-]*\eB\eD \"$i\"\C-afor i in\C-y; do "'
The bindings can also go into the inputrc file for persistence.
Using the shortcut:
After setting things up:
Type in something like
ebook-convert *.epub .mobi
Press CtrlI
The line will transform into
for i in *.epub; do ebook-convert "$i" .mobi; done
If you want to run the command right away, you can modify the macro to append a \C-j as the last keypress, which will trigger accept-line (same as hitting Return).
You could checkout zargs in zsh.
This function has a similar purpose to GNU xargs. Instead of reading lines of arguments from the standard input, it takes them from the command line
zshcontrib(1): OTHER FUNCTIONS, zargs
So, we could write:
autoload -Uz zargs
zargs -I⁂ -- *.epub -- ebook-convert ⁂ .mobi
PS: you could find zmv is handy if you need to capture some portions of patterns for building commands.
The for loop has a shortened form that you might like:
for f (*.epub) ebook-convert $f .mobi
You could make yourself a script that does this :
#!/bin/bash
command="$1"
shift
if
[[ $# -lt 3 ]]
then
echo "Usage: command file/blog arg1, arg2..."
exit 1
fi
declare -a files=()
while [ "$1" != "--" ]
do
[ "$1" ] || continue
files+=("$1")
shift
done
if
[ "$1" != "--" ]
then
echo "Separator not found : end file list with --"
exit 1
fi
shift
for file in "${files[#]}"
do
"$command" "$file" "$#"
done
You cal call this like this (assumes the script is called apply_to).
apply_to command /dir/* arg1, arg2...
EDIT
I modified the code to insert filenames at the beginning of the command.

Shell - Reading backslash in command line parameters

I'm thinking of writing a script for cygwin to cd into a windows directory which is copied from Windows explorer.
e.g.
cdw D:\working\test
equals to
cd /cygdrive/d/working/test
But it seems for shell script, all backslashs in parameters are ignored unless using single quote 'D:\working\test' or double backslashs D:\\working\\test.
But in my case it would be very inconvenience because I can't simply paste the directory name in the command line to execute the script.
Is there any way to make cdw D:\working\test working?
Well, you can do it, but you want something strange :)
cdw()
{
set $(history | tail -1 )
shift 2
path="$*"
cd $(cygpath "$path")
}
Example of usage:
$ cdw D:\working\test
$ pwd
/cygdrive/d/working/test
The main point here is the usage of history.
You don't use an argument directly, but get it from the history in the form it was typed.
$ rawarg() { set $(history | tail -1 ); shift 2; echo "$#"; }
$ rawarg C:\a\b\c\d
C:\a\b\c\d
Of course, you can use this trick in a interactive shell only (for obvious reasons).
The problem you deal with is related to the shell. Any argument you add to cdw on the command line, will be processed by the shell before cdw gets executed.
In order to prevent that processing to happen, you need at least one level of quoting,
either by enclosing the whole string in single quotes:
cd 'D:\working\test'
or with double backslashses:
cd D:\\working\test
A separate program will not help, because the damage is already done before it runs. ;-)
However, I have a possible function for cdw, which works in my AST UWIN ksh:
function cdw { typeset dir
read -r dir?"Paste Directory Path: "
cd ${dir:?}
}
And this one works in Bash (which does not support read var?prompt):
function cdw {
typeset dir
printf "Paste Directory Path: "
read -r dir || return
cd ${dir:?}
}
For me, I just type the two single quotes around the Pasted value.
The solution to add single quotes allows to copy paste

Shell script : changing working dir and spaces in folder name

I want to make a script that takes a file path for argument, and cds into its folder.
Here is what I made :
#!/bin/bash
#remove the file name, and change every space into \space
shorter=`echo "$1" | sed 's/\/[^\/]*$//' | sed 's/\ /\\\ /g'`
echo $shorter
cd $shorter
I actually have 2 questions (I am a relative newbie to shell scripts) :
How could I make the cd "persistent" ? I want to put this script into /usr/bin, and then call it from wherever in the filesystem. Upon return of the script, I want to stay in the $shorter folder. Basically, if pwd was /usr/bin, I could make it by typing . script /my/path instead of ./script /my/path, but what if I am in an other folder ?
The second question is trickier. My script fails whenever there is a space in the given argument. Although $shorter is exactly what I want (for instance /home/jack/my\ folder/subfolder), cd fails whith the error /usr/bin/script : line 4 : cd: /home/jack/my\: no file or folder of this type. I think I have tried everything, using things like cd '$shorter' or cd "'"$shorter"'" doesn't help. What am I missing ??
Thanks a lot for your answers
in your .bashrc add the following line:
function shorter() { cd "${1%/*}"; }
% means remove the smaller pattern from the end
/* is the patern
Then in your terminal:
$ . ~/.bashrc # to refresh your bash configuration
$ type shorter # to check if your new function is available
shorter is a function
shorter ()
{
cd "${1%/*}"
}
$ shorter ./your/directory/filename # this will move to ./your/directory
The first part:
The change of directory won't be “persistent” beyond the lifetime of your script, because your script runs in a new shell process. You could, however, use a shell alias or a shell function. For example, you could embed the code in a shell function and define it in your .bash_profile or other source location.
mycdfunction () {
cd /blah/foo/"$1"
}
As for the “spaces in names” bit:
The general syntax for referring to a variable in Bourne shells is: "$var" — the "double quotes" tell the shell to expand any variables inside of them, but to group the outcome as a single parameter.
Omitting the double quotes around $var tells the shell to expand the variable, but then split the results into parameters (“words”) on whitespace. This is how the shell splits up parameters, normally.
Using 'single quotes' causes the shell to not expand any contents, but group the parameters togethers.
You can use \ (backslash-blank) to escape a space when you're typing (or in a script), but that's usually harder to read than using 'single quotes' or "double quotes"…
Note that the expansion phase includes: $variables wild?cards* {grouping,names}with-braces $(echo command substitution) and other effects.
| expansion | no expansion
-------------------------------------------------------
grouping | " " | ' '
splitting | (no punc.) | (not easily done)
For the first part, there is no need for the shorter variable at all. You can just do:
#!/bin/bash
cd "${1%/*}"
Explanation
Most shells, including bash, have what is called Parameter Expansion and they are very powerful and efficient as they allow you to manipulate variables nativly within the shell that would normally require a call to an external binary.
Two common examples of where you can use Parameter Expansion over an external call would be:
${var%/*} # replaces dirname
${var##*/} # replaces basename
See this FAQ on Parameter Expansion to learn more. In fact, while you're there might as well go over the whole FAQ
When you put your script inside /usr/bin you can call it anywhere. And to deal with whitespace in the shell just put the target between "" (but this doesn't matter !!).
Well here is a demo:
#!/bin/bash
#you can use dirname but that's not apropriate
#shorter=$(dirname $1)
#Use parameter expansion (too much better)
shorter=${1%/*}
echo $shorter
An alternate way to do it, since you have dirname on your Mac:
#!/bin/sh
cd "$(dirname "$1")"
Since you mentioned in the comments that you wanted to be able to drag files into a window and cd to them, you might want to make your script allow file or directory paths as arguments:
#!/bin/sh
[ -f "$1" ] && set "$(dirname "$1")" # convert a file to a directory
cd "$1"

Can zsh or bash expand history expressions referring to directories?

For example, let's suppose I just copied something:
mv foo_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
and I'd like to use history substitution like so:
mv bar_file.txt !!:2
I'm surprised that zsh is not expanding the !!:2 for me when I hit [tab]. In a more complex reference to a historical argument I might really want the expansion before I hit return, just so I know with certainty that I referred to the correct argument. Is there any way to make it do that? (I would expect that to be the default behavior. Is it the default behavior, that I have somehow inadvertently disabled or broken?)
If zsh can't do it, can bash?
UPDATE: zsh will expand the history expression if it refers to a file, but not a directory:
mv foo_file.txt foo_bar_file.txt
mv bar_file.txt !!:2[TAB]
It will expand it if it is just an arbitrary string:
echo one two three four
echo !!:1[TAB]
But not if you're trying to move something to a directory. It looks more and more like this must be a bug.
I am using zsh in cygwin:
$ zsh --version
zsh 4.3.12 (i686-pc-cygwin)
$ setopt
interactive
monitor
shinstdin
zle
I just tried the following:
$ touch foo_file.txt bar_file.txt
$ mkdir -p ~/to/some/long/path/that/i/do/not/want/to/retype
$ mv foo_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
I then tried the tab completion mentioned above:
$ mv bar_file.txt !!:2[TAB]
and it worked fine, the last argument being expanded as follows:
$ mv bar_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
You can pseudo-hack it in bash:
$ shopt -s histreedit
$ shopt -s histverify
Then, to actually try an expansion:
$ echo !!:2 [now hit enter]
$ echo histverify
Now you can't do tab expansion in bash. Unequivocally no. That's because of the order in which bash expansion is processed.
Works perfectly for me with zsh 4.3.17. Sounds like you probably have a bug which might be worth reporting on the zsh-user mailing list. However there are at least five other keybindings which should accomplish what you want: C-x * which is by default bound to expand-word, and Esc Space or Meta-Space or Esc ! or Meta-! which are all bound to expand-history by default. (Meta means the Alt key for many people, although it depends on your terminal setup.)
Having said that, Esc . (or Meta-. or Alt-.) is a nicer way of retrieving the last word from the previous line in the history, since it provides instant visual feedback. You can also choose the last word from older lines by repeatedly pressing the keyboard shortcut, or even the n th last word on a previous line by prefixing the shortcut with Alt-n (or Meta-n or Esc n). So for example to retrieve the penultimate word from the 3rd newest line of history, the sequence would be:
Meta-. (goes back one line of history, selecting the last word from that line)
Meta-. (goes back another, again selecting the last word)
Meta-2 Meta-. (goes back another, but this time selects the penultimate word from that line)
I've tried what you've described, and I don't think bash supports this either.
In bash, Alt-. is typically bound to yank-last-arg, which will give you what you want.
Here is a link to the whole list of history related commands that can be bound to keystrokes
http://www.gnu.org/software/bash/manual/bashref.html#Commands-For-History
For example,
ls /etc/passwd /etc/group
cat
# press Alt+. (Alt dot) here, bash adds /etc/group
cat /etc/group
# press space, then press Alt+1 Alt+.
# bash adds the first argument of the previous command /etc/passwd
cat /etc/group /etc/passwd

Resources