summary refs log tree commit diff
path: root/Functions/Misc/zmv
diff options
context:
space:
mode:
Diffstat (limited to 'Functions/Misc/zmv')
-rw-r--r--Functions/Misc/zmv118
1 files changed, 102 insertions, 16 deletions
diff --git a/Functions/Misc/zmv b/Functions/Misc/zmv
index 2067c73c4..da7a81f78 100644
--- a/Functions/Misc/zmv
+++ b/Functions/Misc/zmv
@@ -1,6 +1,30 @@
 # function zmv {
 # zmv, zcp, zln:
 #
+# This is a multiple move based on zsh pattern matching.  To get the full
+# power of it, you need a postgraduate degree in zsh.  However, simple
+# tasks work OK, so if that's all you need, here are some basic examples:
+#   zmv '(*).txt' '$1.lis'
+# Rename foo.txt to foo.lis, etc.  The parenthesis is the thing that
+# gets replaced by the $1 (not the `*', as happens in mmv, and note the
+# `$', not `=', so that you need to quote both words).
+#   zmv '(**/)(*).txt '$1$2.lis'
+# The same, but scanning through subdirectories.  The $1 becomes the full
+# path.  Note that you need to write it like this; you can't get away with
+# '(**/*).txt'.
+#   zmv -w '**/*.txt' '$1$2.lis'
+# This is the lazy version of the one above; zsh picks out the patterns
+# for you.  The catch here is that you don't need the / in the replacement
+# pattern.  (It's not really a catch, since $1 can be empty.)
+#   zmv -C '**/(*).txt' ~/save/'$1'.lis
+# Copy, instead of move, all .txt files in subdirectories to .lis files
+# in the single directory `~/save'.  Note that the ~ was not quoted.
+# You can test things safely by using the `-n' (no, not now) option.
+# Clashes, where multiple files are renamed or copied to the same one, are
+# picked up.
+#
+# Here's a more detailed description.
+#
 # Use zsh pattern matching to move, copy or link files, depending on
 # the last two characters of the function name.  The general syntax is
 #   zmv '<inpat>' '<outstring>'
@@ -8,15 +32,35 @@
 # immediate expansion, while <outstring> is a string that will be
 # re-evaluated and hence may contain parameter substitutions, which should
 # also be quoted.  Each set of parentheses in <inpat> (apart from those
-# around glob qualifiers and globbing flags) may be referred to by a
-# positional parameter in <outstring>, i.e. the first (...) matched is
-# given by $1, and so on.  For example,
+# around glob qualifiers, if you use the -Q option, and globbing flags) may
+# be referred to by a positional parameter in <outstring>, i.e. the first
+# (...) matched is given by $1, and so on.  For example,
 #   zmv '([a-z])(*).txt' '${(U)1}$2.txt'
 # renames algernon.txt to Algernon.txt, boris.txt to Boris.txt and so on.
 # The original file matched can be referred to as $f in the second
 # argument; accidental or deliberate use of other parameters is at owner's
 # risk and is not covered by the (non-existent) guarantee.
 #
+# As usual in zsh, /'s don't work inside parentheses.  There is a special
+# case for (**/) and (***/):  these have the expected effect that the
+# entire relevant path will be substituted by the appropriate positional
+# parameter.
+#
+# There is a shortcut avoiding the use of parenthesis with the option -w
+# (with wildcards), which picks out any expressions `*', `?', `<range>'
+# (<->, <1-10>, etc.), `[...]', possibly followed by `#'s, `**/', `***/', and
+# automatically parenthesises them. (You should quote any ['s or ]'s which
+# appear inside [...] and which do not come from ranges of the form
+# `[:alpha:]'.)  So for example, in
+#    zmv -w '[[:upper:]]*' '${(L)1}$2'
+# the $1 refers to the expression `[[:upper:]]' and the $2 refers to
+# `*'. Thus this finds any file with an upper case first character and
+# renames it to one with a lowercase first character.  Note that any
+# existing parentheses are active, too, so you must count accordingly.
+# Furthermore, an expression like '(?)' will be rewritten as '((?))' --- in
+# other words, parenthesising of wildcards is independent of any existing
+# parentheses.
+#
 # Any error --- a substitution resulted in an empty string, a
 # substitution did not change the file name, two substitutions gave the
 # same result, the destination was an existing regular file and -f was not
@@ -30,7 +74,9 @@
 #      to execute it.  Y or y will execute it, anything else will skip it.
 #      Note that you just need to type one character.
 #  -n  no execution: print what would happen, but don't do it.
-#  -q  don't allow bare glob qualifiers in the filename pattern, see below.
+#  -q  Turn bare glob qualifiers off:  now assumed by default, so this
+#      has no effect.
+#  -Q  Force bare glob qualifiers on.
 #  -s  symbolic, passed down to ln; only works with zln or z?? -L.
 #  -v  verbose: print line as it's being executed.
 #  -o <optstring>
@@ -41,6 +87,8 @@
 #      Call <program> instead of cp, ln or mv.  Whatever it does, it should
 #      at least understand the form '<program> -- <oldname> <newname>',
 #      where <oldname> and <newname> are filenames generated.
+#  -w  Pick out wildcard parts of the pattern, as described above, and
+#      implicitly add parentheses for referring to them.
 #  -C
 #  -L
 #  -M  Force cp, ln or mv, respectively, regardless of the name of the
@@ -48,15 +96,17 @@
 #
 # Bugs:
 #   Parenthesised expressions can be confused with glob qualifiers, for
-#   example a trailing '(*)' is treated as a glob qualifier.  Use -q to
-#   turn off glob qualifiers, or (yuk) add a suitable dummy qualifier
-#   (e.g. `(.)') or dummy pattern (e.g. `(|)') at the end.
+#   example a trailing '(*)' would be treated as a glob qualifier in
+#   ordinary globbing.  This has proved so annoying that glob qualifiers
+#   are now turned off by default.  To force the use of glob qualifiers,
+#   give the flag -Q.
 #
 #   The second argument is re-evaluated in order to expand the parameters,
 #   so quoting may be a bit haphazard.  In particular, a double quote
 #   will need an extra level of quoting.
 #
-#   The pattern is always treated as an extendedglob pattern.
+#   The pattern is always treated as an extendedglob pattern.  This
+#   can also be interpreted as a feature.
 #
 # Unbugs:
 #   You don't need braces around the 1 in expressions like '$1t' as
@@ -67,12 +117,13 @@ emulate -L zsh
 setopt extendedglob
 
 local f g args match mbegin mend files action myname tmpf opt exec
-local opt_f opt_i opt_n opt_q opt_s opt_M opt_C opt_L opt_o opt_p
-local pat repl errstr
+local opt_f opt_i opt_n opt_q opt_Q opt_s opt_M opt_C opt_L 
+local opt_o opt_p opt_v opt_w MATCH MBEGIN MEND
+local pat repl errstr fpat
 typeset -A from to
 integer stat
 
-while getopts ":o:p:MCLfinqsv" opt; do
+while getopts ":o:p:MCLfinqQsvw" opt; do
   if [[ $opt = "?" ]]; then
     print -P "%N: unrecognized option: -$OPTARG" >&2
     return 1
@@ -81,15 +132,20 @@ while getopts ":o:p:MCLfinqsv" opt; do
 done
 (( OPTIND > 1 )) && shift $(( OPTIND - 1 ))
 
-[[ -n $opt_q ]] && setopt nobareglobqual
+[[ -z $opt_Q ]] && setopt nobareglobqual
 [[ -n $opt_M ]] && action=mv
 [[ -n $opt_C ]] && action=cp
 [[ -n $opt_L ]] && action=ln
 [[ -n $opt_p ]] && action=$opt_p
 
 if (( $# != 2 )); then
-  print -P "Usage: %N oldpattern newpattern
-  e.g. %N '(*).lis' '\$1.txt'" >&2
+  print -P "Usage:
+  %N oldpattern newpattern
+where oldpattern contains parenthesis surrounding patterns which will
+be replaced in turn by $1, $2, ... in newpattern.  For example,
+  %N '(*).lis' '\$1.txt'
+renames 'foo.lis' to 'foo.txt', 'my.old.stuff.lis' to 'my.old.stuff.txt',
+and so on." >&2
   return 1
 fi
 
@@ -118,7 +174,28 @@ if [[ -n $opt_s && $action != ln ]]; then
   return 1
 fi
 
-files=(${~pat})
+if [[ -n $opt_w ]]; then
+  # Parenthesise all wildcards.
+  local newpat
+  # Well, this seems to work.
+  # The tricky bit is getting all forms of [...] correct, but as long
+  # as we require inactive bits to be backslashed its not so bad.
+  newpat="${pat//\
+(#m)(\*\*#\/|[*?]|\<[0-9]#-[0-9]#\>|\[(\[:[a-z]##:\]|\\\[|\\\]|[^\[\]]##)##\])\##\
+/($MATCH)}"
+  if [[ $newpat = $pat ]]; then
+    print -P "%N: warning: no wildcards were found" >&2
+  else
+    pat=$newpat
+  fi
+fi
+
+if [[ $pat = (#b)(*)\((\*\*##/)\)(*) ]]; then
+  fpat="$match[1]$match[2]$match[3]"
+else
+  fpat=$pat
+fi
+files=(${~fpat})
 
 if [[ -o bareglobqual && $pat = (#b)(*)\([^\)\|\~]##\) ]]; then
   # strip off qualifiers for use as ordinary pattern
@@ -128,8 +205,16 @@ fi
 errs=()
 
 for f in $files; do
+  if [[ $pat = (#b)(*)\(\*\*##/\)(*) ]]; then
+    # This looks like a recursive glob.  This isn't good enough,
+    # because we should really enforce that $match[1] and $match[2]
+    # don't match slashes unless they were explicitly given.  But
+    # it's a start.  It's fine for the classic case where (**/) is
+    # at the start of the pattern.
+    pat="$match[1](*/|)$match[2]"
+  fi
   [[ -e $f && $f = (#b)${~pat} ]] || continue
-  set -- $match
+  set -- "$match[@]"
   eval g=\"$repl\"
   if [[ -z $g ]]; then
     errs=($errs "$f expanded to empty string")
@@ -151,6 +236,7 @@ if (( $#errs )); then
 fi
 
 for f in $files; do
+  [[ -z $to[$f] ]] && continue
   exec=($action ${=opt_o} $opt_s -- $f $to[$f])
   [[ -n $opt_i$opt_n$opt_v ]] && print -- $exec
   if [[ -n $opt_i ]]; then