How to rename all files in a directory and subdirectories? - bash

I'm trying to access all files in a directory and it's subdirectories; and I'm trying to implement this code for it to work:
#!/bin/bash
NEWNAME="newFile"
echo "- - - - - - - - - - - "
for f in *\ *; do mv "$f" "${f// /_}"; done
for f in *.* *; do mv "$f" "${f// /_}"; done
FILES=$(find ./ -type f)
for f in $FILES; do
path=$(dirname "${f}")
extension="${f##*.}"
echo $extension;
mv "$f" "${f/$f/${$path/$NEWNAME.$extension}}"
done
But my lack of knowledge in bash results in this error:
mv: rename * * to *_*: No such file or directory
mv: rename very_important_folder to very_important_folder/very_important_folder: Invalid argument
//very_important_folder/bob
./app.sh: line 13: ${$path/$NEWNAME.$extension}: bad substitution
This my folder setup:
-very important folder:
-|filewithoutspaces.py
-|anothersubfolder:
--|file with spaces.txt
-app.sh
And so, I'm trying to change all the files in the directories 1 name: newname, but leaving the extension as it was. I encountered problems changing files with spaces in their names, and I'm not really familiar with bash.
It does change the files not in folders, but it doesn't work for the ones in subdirectories.
I'm using MacBook Air (M1, 2020) macOS 12.3 Monterey.

I'm not sure about what you really wanna do but the following construct can surely help you:
#!/bin/bash
shopt -s globstar
for path in ./**
do
[[ -d "$path" ]] && continue
[[ $path =~ ^(.*/)([^/]+)(\.[^/]*)$|^(.*/)(.+)$ ]]
dirpath="${BASH_REMATCH[1]}${BASH_REMATCH[4]}"
filename="${BASH_REMATCH[2]}${BASH_REMATCH[5]}"
extension="${BASH_REMATCH[3]}"
echo mv "$path" "$dirpath${filename// /_}$extension"
done
notes:
shopt -s globstar allows the glob ** to match any path length.
./** is for making sure that there is at least one / in the resulting paths. It simplifies greatly the splitting of a path into its different components.
[[ -d "$path" ]] && continue means to skip paths that are directories.
[[ $path =~ ... ]] is a bash way of using a regex for splitting the path into dirname filename and extension.
echo ... Now that you have the different components of the filepath at hand, you can do whatever you want.
Update
As a workaround for older bash you can define a function and call it in find:
#!/bin/bash
doit() {
local path dirpath filename extension
for path
do
[[ $path =~ ^(.*/)([^/]+)(\.[^/]*)$|^(.*/)(.+)$ ]]
dirpath="${BASH_REMATCH[1]}${BASH_REMATCH[4]}"
filename="${BASH_REMATCH[2]}${BASH_REMATCH[5]}"
extension="${BASH_REMATCH[3]}"
echo mv "$path" "$dirpath${filename// /_}$extension"
done
}
export -f doit
find . -type f -exec bash -c 'doit "$0" "$#"' {} +
Just be aware that if you use an external variable inside the function (like NEWNAME in your code), you'll also have to export it: export NEWNAME="newFile".
BTW, it's not safe to capitalise your shell variables.

Related

sed replace spaces in directory names

I'm trying to replace space with - in directory names, not file names on macOS. I have the following:
cd ~/foo
for directory in **; do
if [[ -d $directory ]] && [[ -w $directory ]]; then
sed -i '' 's/ /-/g' "$directory"
fi
done
However I'm getting an error in-place editing only works for regular files.
How can I replace spaces in directory names?
sed is meant for search and replacement on files and not on directories in Linux/Unix. The -i flag in sed is used to make the text replacement on-the-fly on a file, the action simply does not make sense for a directory. You probably meant to change the name of the directory using sed on the filename and eventually use mv to rename the actual directory with the replaced string.
But you could just use mv in the first place with shell native features to replace white space with a - character.
for directory in **; do
if [[ -d $directory ]] && [[ -w $directory ]]; then
mv -- "$directory" "${directory// /-}"
fi
done
To rename directory, you must use command mv
#!/bin/bash
cd foo
for directory in **; do
if [[ -d $directory ]] && [[ -w $directory ]]; then
newdir=`echo "$directory" |sed 's/ /-/g' `
if [ "$directory" -ne "$newdir" ]
mv -- "$directory" "$newdir"
fi
fi
done
sed is versatile but I suggest you use a more specialised tool such as rename:
rename 's/ /-/g' "$directory"
Also you're very likely to try and rename already renamed directories (and thus no longer existing). I suggest you use find options -depth (depth first) and -execdir (execute command inside directory, to avoid affecting parent directories):
find foo -depth -type d -execdir rename 's/ /-/g' {} +
directory=`echo $directory | sed 's/ /-/g'`

How to change extension of multiple files using bash script

I need a bash script to recursively rename files with blank extensions to append .txt at the end. I found the following script, but I can't figure out how to make it recursive:
#!/bin/sh
for file in *; do
test "${file%.*}" = "$file" && mv "$file" "$file".txt;
done
Thanks.
Thanks.
You can delegate the heavy lifting to find
$ find . -type f ! -name "*.*" -print0 | xargs -0 -I file mv file file.txt
assumption is without extension means without a period in name.
If you don't mind using a recursive function, then you can do it in older Bash versions with:
shopt -s nullglob
function add_extension
{
local -r dir=$1
local path base
for path in "$dir"/* ; do
base=${path##*/}
if [[ -f $path && $base != *.* ]] ; then
mv -- "$path" "$path.txt"
elif [[ -d $path && ! -L $path ]] ; then
add_extension "$path"
fi
done
return 0
}
add_extension .
The mv -- is to protect against paths that begin with a hyphen.

Can't move executables

I'm working on this script but the option -x isn't working, it's supposed to move only the executable files.
This is the error I'm receiving:
$ sh wizlast.sh u555 -x
mv: target ‘./u555/ud’ is not a directory
it targets the right file (ud) but doesn't move it. I tried different types of combinations.
#!/bin/bash
dir=$1
if [ $# -lt 1 ] ; then
echo "ERROR: no argument"
exit 1 # pas 0
else
case $2 in
-d)
mv $dir/* /tmp/*
echo 'moving with -d'
;;
-x)
find -executable -type f | xargs mv -t "$dir"/* /tmp
echo 'moving executables'
;;
*)
mv $dir/* /tmp/
echo 'no flag passed so moving all'
echo "mv $dir/* /tmp/"
;;
esac
fi
man mv shows:
-t, --target-directory=DIRECTORY
You can't use $dir/* as a target directory, as the shell expands it and treats the first file in the list as the target (hence the error).
Use this format
For example to move files into $dir
find -executable -type f | xargs -I{} mv {} "$dir"/
The I{} tell xargs to replace and occurence of {} with the strings from pipe, so after mv each string is substituted before the directory "$dir"/ and the command works like normal.
The reason yours wasn't working was the strings from the find were read last and so treated as the directory to move into.
As you're working with Bash you should leverage it's tools and syntax improvement.
Solution for loop and globbing
So instead of using find you can use globbing and [[ -x ]] to test if the current file is executable:
for f in "$dir"/*; do
if [[ -x $f ]]; then
mv "$f" /tmp
fi
done
It use the conditionnal expression -x in [[ … ]]:
-x file
True if file exists and is executable
As a one-liner
You can rewrite it like: for f in "$dir"/*; do [[ -x $f ]] && mv "$f" /tmp; done
Deeper search (d>1)
Current loop is limited to what is directly in your "$dir/", if you want to explore deeper like "$dir///*" you will need:
to toggle the globstar shell option using shopt built-in ;
update your glob in the for loop to use it: "$dir"/**
shopt -s globstar # enable/set
for f in "$dir"/**/*; do [[ -x $f ]] && mv "$f" /tmp; done
shopt -u globstar # disable/unset
Arithmethic context
Bash has syntactic sugar, that let you replace:
if [ $# -lt 1 ] ; then … fi
with
if (( $# < 1 )); then … fi
More about Arithmetic Expression read articles at:
1. wooledge's wiki ;
2. bash-hackers' wiki.
Don't use wildcards in the destination part of a mv command, so instead of
mv $dir/* /tmp/*
do
mv $dir/* /tmp/

Recursive Shell Script and file extensions issue

I have a problem with this script. The script is supposed to go trough all the files and all sub-directories and sub-files (recursively). If the file ends with the extension .txt i need to replace a char/word in the text with a new char/word and then copy it into a existing directory. The first argument is the directory i need to start the search, the second is the old char/word, third the new char/word and fourth the directory to copy the files to. The script goes trough the files but only does the replacement and copies the files from the original directory. Here is the script
#!/bin/bash
funk(){
for file in `ls $1`
do
if [ -f $file ]
then
ext=${file##*.}
if [ "$ext" = "txt" ]
then
sed -i "s/$2/$3/g" $file
cp $file $4
fi
elif [ -d $file ]
then
funk $file $2 $3 $4
fi
done
}
if [ $# -lt 4 ]
then
echo "Need more arg"
exit 2;
fi
cw=$1
a=$2
b=$3
od=$4
funk $cw $a $b $od
You're using a lot of bad practices here: lack of quotings, you're parsing the output of ls... all this will break as soon as a filename contains a space of other funny symbol.
You don't need recursion if you either use bash's globstar optional behavior, or find.
Here's a possibility with the former, that will hopefully show you better practices:
#!/bin/bash
shopt -s globstar
shopt -s nullglob
funk() {
local search=${2//\//\\/}
local replace=${3//\//\\/}
for f in "$1"/**.txt; do
sed -i "s/$search/$replace/g" -- "$f"
cp -nvt "$4" -- "$f"
done
}
if (($#!=4)); then
echo >&2 "Need 4 arguments"
exit 1
fi
funk "$#"
The same function funk using find:
#!/bin/bash
funk() {
local search=${2//\//\\/}
local replace=${3//\//\\/}
find "$1" -name '*.txt' -type f -exec sed -i "s/$search/$replace/g" -- {} \; -exec cp -nvt "$4" -- {} \;
}
if (($#!=4)); then
echo >&2 "Need 4 arguments"
exit 1
fi
funk "$#"
In cp I'm using
the -n switch: no clobber, so as to not overwrite an existing file. Use it if your version of mv supports it, unless you actually want to overwrite files.
the -v switch: verbose, will show you the moved files (optional).
the -t switch: -t followed by a directory tells to copy into this directory. It's a very good thing to use cp this way: imagine instead of giving an existing directory, you give an existing file: without this feature, this file will get overwritten several times (well, this will be the case if you omit the -n option)! with this feature the existing file will remain safe.
Also notice the use of --. If your cp and sed supports it (it's the case for GNU sed and cp), use it always! it means end of options now. If you don't use it and if a filename start with a hyphen, it would confuse the command trying to interpret an option. With this --, we're safe to put a filename that may start with a hyphen.
Notice that in the search and replace patterns I replaced all slashes / by their escaped form \/ so as not to clash with the separator in sed if a slash happens to appear in search or replace.
Enjoy!
As pointed out, looping over find output is not a good idea. It also doesn't support slashes in search&replace.
Check gniourf_gniourf's answer.
How about using find for that?
#!/bin/bash
funk () {
local dir=$1; shift
local search=$1; shift
local replace=$1; shift
local dest=$1; shift
mkdir -p "$dest"
for file in `find $dir -name *.txt`; do
sed -i "s/$search/$replace/g" "$file"
cp "$file" "$dest"
done
}
if [[ $# -lt 4 ]] ; then
echo "Need 4 arguments"
exit 2;
fi
funk "$#"
Though you might have files with the same names in the subdirectories, then those will be overwritten. Is that an issue in your case?

Bash rename extension recursive

I know there are a lot of things like this around, but either they don't work recursively or they are huge.
This is what I got:
find . -name "*.so" -exec mv {} `echo {} | sed s/.so/.dylib/` \;
When I just run the find part it gives me a list of files. When I run the sed part it replaces any .so with .dylib. When I run them together they don't work.
I replaced mv with echo to see what happened:
./AI/Interfaces/C/0.1/libAIInterface.so ./AI/Interfaces/C/0.1/libAIInterface.so
Nothing is replaced at all!
What is wrong?
This will do everything correctly:
find -L . -type f -name "*.so" -print0 | while IFS= read -r -d '' FNAME; do
mv -- "$FNAME" "${FNAME%.so}.dylib"
done
By correctly, we mean:
1) It will rename just the file extension (due to use of ${FNAME%.so}.dylib). All the other solutions using ${X/.so/.dylib} are incorrect as they wrongly rename the first occurrence of .so in the filename (e.g. x.so.so is renamed to x.dylib.so, or worse, ./libraries/libTemp.so-1.9.3/libTemp.so is renamed to ./libraries/libTemp.dylib-1.9.3/libTemp.so - an error).
2) It will handle spaces and any other special characters in filenames (except double quotes).
3) It will not change directories or other special files.
4) It will follow symbolic links into subdirectories and links to target files and rename the target file, not the link itself (the default behaviour of find is to process the symbolic link itself, not the file pointed to by the link).
for X in `find . -name "*.so"`
do
mv $X ${X/.so/.dylib}
done
A bash script to rename file extensions generally
#/bin/bash
find -L . -type f -name '*.'$1 -print0 | while IFS= read -r -d '' file; do
echo "renaming $file to $(basename ${file%.$1}.$2)";
mv -- "$file" "${file%.$1}.$2";
done
Credits to aps2012.
Usage
Create a file e.g. called ext-rename (no extension, so you can run it like a command) in e.g. /usr/bin (make sure /usr/bin is added to your $PATH)
run ext-rename [ext1] [ext2] anywhere in terminal, where [ext1] is renaming from and [ext2] is renaming to. An example use would be: ext-rename so dylib, which will rename any file with extension .so to same name but with extension .dylib.
What is wrong is that
echo {} | sed s/.so/.dylib/
is only executed once, before the find is launched, sed is given {} on its input, which doesn't match /.so/ and is left unchanged, so your resulting command line is
find . -name "*.so" -exec mv {} {}
if you have Bash 4
#!/bin/bash
shopt -s globstar
shopt -s nullglob
for file in /path/**/*.so
do
echo mv "$file" "${file/%.so}.dylib"
done
He needs recursion:
#!/bin/bash
function walk_tree {
local directory="$1"
local i
for i in "$directory"/*;
do
if [ "$i" = . -o "$i" = .. ]; then
continue
elif [ -d "$i" ]; then
walk_tree "$i"
elif [ "${i##*.}" = "so" ]; then
echo mv $i ${i%.*}.dylib
else
continue
fi
done
}
walk_tree "."

Resources