The following bash ≥4 script copies ~/mail/foo:bar to /media/usb99/mail/foo_bar, and similarly for all files under ~/mail. Files that already exist in the destination tree and that are not older than the source are skipped.
#!/bin/bash
set -e
shopt -s dotglob globstar
for source in "$HOME"/mail/**/*; do
target=/media/usb99/${source#"$HOME"/}
target=${target//:/_}
if [[ -d $source ]]; then
mkdir -p -- "$target"
elif [[ $target -ot $source ]]; then
cp -p -- "$source" "$target"
fi
done
This script works under zsh with minor modifications: replace shopt -s dotglob globstar by setopt dot_glob and [[ $target -ot $source ]] by [[ ! -e $target || $target -ot $source ]].
Here's a zsh two-liner (three if you count the autoloads). It's shorter, but fairly advanced and not very readable.
autoload zargs zmv
zargs -- ~/mail/**/*(/e\''REPLY=/media/usb99/${${REPLY#$HOME/}//:/_}'\') -- mkdir -p --
zmv -C -Q -o -pu '~/mail/(**/)(*)(.)' '/media/usb99/mail/${1//:/_}${2//:/_}'
- The
zargs line is equivalent to mkdir -p ~/mail/**/*(…), except that it won't bomb out if the cumulated length of the directory names are too long. That line creates the target directories as necessary.
~/mail/**/*(/) expands to all the directories under ~/mail (directories only due to the (/) at the end).
(/e\''…'\') selects only directories and further executes the code within '…' to transform each file name, which is stored in the REPLY variable.
${${REPLY#$HOME/}//:/_} removes the prefix corresponding with the source directory and changes : into _.
zmv -C copies each file matching its first operand (a zsh pattern) to the file name obtained by expandingg its second operand.
-o -pu says to pass -pu to the cp utility, so as to preserve permissions and copy only updated files. (We could tell zsh to perform the update check; it would be a little faster but even more cryptic.)
(.) selects only regular files. -Q says that this is to be parsed as a glob qualifier and not as a . with parentheses around it indicating a subexpression.
$1 and $2 in the replacement text match the parenthesized expressions (**/) and *. (** loses its special meaning as zero or more subdirectory levels if it's in parentheses, unless the parentheses contain exactly **/.)
I initially thought to use pax
, which is an archiving tool
(here intended to be used in pass-through mode) that has a file renaming feature (its -s option). However, the -s and -u options do not work together (the POSIX definition of pax literally says that -u must check a file of the same name in the destination tree, rather than the file name transformed by -s; the pax implementation in Ubuntu follows the spec literally rather than usefully). It's still possible to make use of it to make renamed hard links, and then copy the hard links (with rsync -au or pax -rw -pp -u) to the other media, but it feels more trouble than it's worth.
cd ~/mail
mkdir -p /media/usb99/mail
pax -rw -l -pp -s '!:!_!g' . ../mail.colonless
rsync -au ../mail.colonless/ /media/usb99/mail/
Another idea would be to use FUSE (Filesystem in Userspace) to create a renaming stackable filesystem. I don't know of any ready-made FUSE filesystem that can turn : into _, but one that comes very close is convmvfs (not in Ubuntu as of 10.04, but you can compile it after installing build-essential
and libfuse-dev
), which translates between character sets using the iconv library. You can even use convmvfs if you write an iconv module for a character set that approximates : with _, but this requires a little C programming. Alternatively, there are FUSE bindings in high-level languages, for instance python-fuse
, which make it a small programming task to implement a simple renaming filesystem.