Reputation: 755
I have a bunch of files in a folder, in subfolders and I'm trying to make some kind of one-liner for quick copy/pasting once in a while.
The contents is (too long to paste here): http://pastebin.com/4aZCPbwT
I've tried the following commands:
List all files and their directories
find . -name '[!.]*'
Replace all instances of "Namespace" with "Test:
find . -name '[!.]*' -print0 | sed 's/Namespace/Test/gI' | xargs -i -0 echo '{}'
What I need to do is:
Replace foldes names like above, and copy the folders (including files), to another location. Create the folders if they don't exist (they most likely won't) - BUT, there are some of them that I don't need, like ./app, as this folder exists. I could use -wholename './app' for that.
When they are copied, I need to replace some text inside each file, same as above (Namespace with Test - also occours inside the files and save them of course).
Something like this I would imagine:
-print -exec sed -i 's/Namespace/Test/gI' {} \;
Can these 3 things be done in a one-liner? Replace text in files (Namespace <=> Test), copy files including their directories with cp -p (don't want to write over folders), but renaming each directory/file with as above (Namespace <=> Test).
Thanks a lot :-)
Upvotes: 2
Views: 2826
Reputation: 694
Besides describing the how with painstaking verbosity below, this method may also be unique in that it incorporates built-in debugging. It basically doesn't do anything at all as written except compile and save to a variable all commands it believes it should do in order to perform the work requested.
It also explicitly avoids loops as much as possible. Besides the sed
recursive search for more than one match of the pattern there is no other recursion as far as I know.
And last, this is entirely null
delimited - it doesn't trip on any character in any filename except the null
. I don't think you should have that.
By the way, this is REALLY fast. Look:
% _mvnfind() { mv -n "${1}" "${2}" && cd "${2}"
> read -r SED <<SED
> :;s|${3}\(.*/[^/]*${5}\)|${4}\1|;t;:;s|\(${5}.*\)${3}|\1${4}|;t;s|^[0-9]*\(.*\)${5}|\1|p
> SED
> find . -name "*${3}*" -printf "%d\tmv %P ${5} %P\000" |
> sort -zg | sed -nz ${SED} | read -r ${6}
> echo <<EOF
> Prepared commands saved in variable: ${6}
> To view do: printf ${6} | tr "\000" "\n"
> To run do: sh <<EORUN
> $(printf ${6} | tr "\000" "\n")
> EORUN
> EOF
> }
% rm -rf "${UNNECESSARY:=/any/dirs/you/dont/want/moved}"
% time ( _mvnfind ${SRC=./test_tree} ${TGT=./mv_tree} \
> ${OLD=google} ${NEW=replacement_word} ${sed_sep=SsEeDd} \
> ${sh_io:=sh_io} ; printf %b\\000 "${sh_io}" | tr "\000" "\n" \
> | wc - ; echo ${sh_io} | tr "\000" "\n" | tail -n 2 )
<actual process time used:>
0.06s user 0.03s system 106% cpu 0.090 total
<output from wc:>
Lines Words Bytes
115 362 20691 -
<output from tail:>
mv .config/replacement_word-chrome-beta/Default/.../googlestars \
.config/replacement_word-chrome-beta/Default/.../replacement_wordstars
NOTE: The above function
will likely require GNU
versions of sed
and find
to properly handle the find printf
and sed -z -e
and :;recursive regex test;t
calls. If these are not available to you the functionality can likely be duplicated with a few minor adjustments.
This should do everything you wanted from start to finish with very little fuss. I did fork
with sed
, but I was also practicing some sed
recursive branching techniques so that's why I'm here. It's kind of like getting a discount haircut at a barber school, I guess. Here's the workflow:
rm -rf ${UNNECESSARY}
./app
might be unwanted. Delete it or move it elsewhere beforehand, or, alternatively, you could build in a \( -path PATTERN -exec rm -rf \{\} \)
routine to find
to do it programmatically, but that one's all yours. _mvnfind "${@}"
${sh_io}
is especially important in that it saves the return from the function. ${sed_sep}
comes in a close second; this is an arbitrary string used to reference sed
's recursion in the function. If ${sed_sep}
is set to a value that could potentially be found in any of your path- or file-names acted upon... well, just don't let it be. mv -n $1 $2
-noclobber
option set for mv
; as written, this function will not put ${SRC_DIR}
where a ${TGT_DIR}
already exists.read -R SED <<HEREDOC
find . -name ${OLD} -printf
find
process. With find
we search only for anything that needs renaming because we already did all of the place-to-place mv
operations with the function's first command. Rather than take any direct action with find
, like an exec
call, for instance, we instead use it to build out the command-line dynamically with -printf
.%dir-depth :tab: 'mv '%path-to-${SRC}' '${sed_sep}'%path-again :null delimiter:'
find
locates the files we need it directly builds and prints out (most) of the command we'll need to process your renaming. The %dir-depth
tacked onto the beginning of each line will help to ensure we're not trying to rename a file or directory in the tree with a parent object that has yet to be renamed. find
uses all sorts of optimization techniques to walk your filesystem tree and it is not a sure thing that it will return the data we need in a safe-for-operations order. This is why we next...sort -general-numerical -zero-delimited
find
's output based on %directory-depth
so that the paths nearest in relationship to ${SRC} are worked first. This avoids possible errors involving mv
ing files into non-existent locations, and it minimizes need to for recursive looping. (in fact, you might be hard-pressed to find a loop at all)sed -ex :rcrs;srch|(save${sep}*til)${OLD}|\saved${SUBSTNEW}|;til ${OLD=0}
%Path
printed for each string in case it contains more than one ${OLD} value that might need replacing. All other solutions I imagined involved a second sed
process, and while a short loop may not be desirable, certainly it beats spawning and forking an entire process.sed
does here is search for ${sed_sep}, then, having found it, saves it and all characters it encounters until it finds ${OLD}, which it then replaces with ${NEW}. It then heads back to ${sed_sep} and looks again for ${OLD}, in case it occurs more than once in the string. If it is not found, it prints the modified string to stdout
(which it then catches again next) and ends the loop. mv
command string, which needs to include ${OLD} of course, does include it, and the second half is altered as many times as is necessary to wipe the ${OLD} name from mv
's destination path.sed -ex...-ex search|%dir_depth(save*)${sed_sep}|(only_saved)|out
-exec
calls here happen without a second fork
. In the first, as we've seen, we modify the mv
command as supplied by find
's -printf
function command as necessary to properly alter all references of ${OLD} to ${NEW}, but in order to do so we had to use some arbitrary reference points which should not be included in the final output. So once sed
finishes all it needs to do, we instruct it to wipe out its reference points from the hold-buffer before passing it along. AND NOW WE'RE BACK AROUND
read
will receive a command that looks like this:
% mv /path2/$SRC/$OLD_DIR/$OLD_FILE /same/path_w/$NEW_DIR/$NEW_FILE \000
It will read
it into ${msg}
as ${sh_io}
which can be examined at will outside of the function.
Cool.
-Mike
Upvotes: 1
Reputation: 48775
I haven't tested this, but I think it's what you're after.
find . -name '[!.]*' -print | while read line; do nfile=`echo "$line" | sed 's/Namespace/Test/gI'`; mkdir -p "`dirname $nfile`"; cp -p "$line" "$nfile"; sed -i 's/Namespace/Test/gI' "$nfile"; done
Upvotes: 0