Socowi
Socowi

Reputation: 27225

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.

Upvotes: 7

Views: 3420

Answers (4)

Grisha Levit
Grisha Levit

Reputation: 8617

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-eend-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:

  1. Type in something like

    ebook-convert *.epub .mobi
  2. Press CtrlI
  3. 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).

Upvotes: 4

chepner
chepner

Reputation: 531315

The for loop has a shortened form that you might like:

for f (*.epub) ebook-convert $f .mobi

Upvotes: 2

Fred
Fred

Reputation: 6995

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.

Upvotes: 0

hchbaw
hchbaw

Reputation: 5319

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.

Upvotes: 5

Related Questions