How to change extension of multiple files using bash script - bash

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.

Related

How to rename all files in a directory and subdirectories?

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.

glob operator with for loop is stuck

I am trying to traverse all files in /home directory recursively. I want to do some linux command with each file . So, I am making use of for loop as below:
for i in /home/**/*
I have put below statements as start of script as well:
shopt -s globstar
shopt -s nullglob
But its getting stuck in for loop. It might be the problem with handling so many files. If I give some another directory(with less no of files) to for loop loop, then it traverse properly.
What else I can try.
Complete code:
#!/bin/bash
shopt -s globstar
shopt -s nullglob
echo "ggg"
for i in /home/**/*
do
NAME=${i}
echo "It's there." $NAME
if [ -f "$i" ]; then
echo "It's there." $NAME
printf "\n\n"
fi
done
Your code isn't getting stuck. It will just be very, very slow since it needs to build up the list of all files before entering the for loop. The standard alternative is to use find, but you need to be careful about what exactly you want to do. If you want it to behave exactly like your for loop, which means i) ignore hidden files (those whose name starts with .) and ii) follow symlinks, you can do this (assuming GNU find since you are on Linux):
find -L . -type f -not -name '.*' -printf '.\n' | wc -l
That will print a . for each file found, so wc -l will give you the number of files. The -L makes find dereference symlinks and the -not -name '.*' will exclude hidden files.
If you want to iterate over the output and do something to each file, you would need to use this:
find -L . -type f -not -name '.*' -print0 |
while IFS= read -r -d '' file; do
printf -- "FILE: %s\n" "$file"
done
Perhaps this approach may help:
#!/bin/bash
shopt -s globstar
shopt -s nullglob
echo "ggg"
for homedir in /home/*/
do
for i in "$homedir"**
do
NAME=${i}
echo "It's there." "$NAME"
if [ -f "$i" ]; then
echo "It's there." "$NAME"
printf "\n\n"
fi
done
done
Update: Another approach in pure bash might be
#!/bin/bash
shopt -s nullglob
walktree() {
local file
for file in *; do
[[ -L $file ]] && continue
if [[ -f $file ]]; then
# Do something with the file "$PWD/$file"
echo "$PWD/$file"
elif [[ -d $file ]]; then
cd "./$file" || exit
walktree
cd ..
fi
done
}
cd /home || exit
walktree

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'`

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/

why does the mv command in bash delete files?

running the following script to rename all .jpg files in the current folder works well sometimes, but it often deletes one or more files it is renaming. How would I write a script to rename files without deleting them in the process? This is running on Mac OSX 10.8 using GNU bash, version 3.2.48
this is an example file listing I would change for illustration:
original files
red.jpg
blue.jpg
green.jpg
renamed files if counter is set to 5
file_5.jpg
file_6.jpg
file_7.jpg
instead I get usually lose one or more files
#!/bin/bash
counter=5
for file in *.jpg; do
echo renaming "$file" to "file_${counter}.jpg";
mv "$file" "file_${counter}.jpg";
let "counter+=1";
done
** UPDATE **
it no longer seems to be deleting files, but the output is still not as expected. for example:
file_3.jpg
file_4.jpg
turns into
file_3.jpg
file_5.jpg
when counter is set to 4, when the expected output is
file_4.jpg
file_5.jpg
-
#!/bin/bash
counter=3
for file in *.jpg; do
if [[ -e file_${counter}.jpg ]] ; then
echo Skipping "$file", file exists.
else
echo renaming "$file" to "file_${counter}.jpg"
mv "$file" "file_${counter}.jpg"
fi
let "counter+=1"
done
The problem is that some of the files already have names corresponding to the target names. For example, if there are files
file_1.jpg
file_7.jpg
and you start with counter=7, you overwrite file_7.jpg with file_1.jpg in the first step, and then rename it to file_8.jpg.
You can use mv -n to prevent clobbering (if supported), or test for existence before running the command
if [[ -e file_${counter}.jpg ]] ; then
echo Skipping "$file", file exists.
else
mv "$file" "file_${counter}.jpg"
fi
I think you are glazing over an obvious problem with the glob. If the glob matches file_2.jpg, it will try and create file_file_2.jpg (I don't mean that in the literal sense, just that you will be reprocessing files you already processed). To solve this, you need to make sure your initial glob expression doesn't match the files you have already moved:
shopt -s extglob
i=0
for f in !(file_*).jpg ; do
while [[ -e "file_${i}.jpg" ]] ; do
(( i++ ))
done
mv -v "$f" "file_$i.jpg"
(( i++ ))
done
What choroba said is correct. You can also use:
mv "$file" "file_${counter}.jpg" -n
to simply neglect the move when the destination filename already exists, or
mv "$file" "file_${counter}.jpg" -i
to ask whether it should overwrite or not.
Instead of iterating over *.jpg you should skip your already renamed files i.e. file_[0-9]*.jpg and run your loop like this:
counter=5
while read file; do
echo renaming "$file" to "file_${counter}.jpg";
mv -n "$file" "file_${counter}.jpg";
let "counter+=1";
done < <(find . -maxdepth 1 -name "*.jpg" -not -name "file_[0-9]*.jpg")
Another way is to continue your counting until a file does not exist:
#!/bin/bash
counter=1
shopt -s extglob
for file in *.jpg; do
[[ $file == ?(*/)file_+([[:digit:]]).jpg ]] && continue
until
newname=file_$(( counter++ )).jpg
[[ ! -e $newname ]]
do
continue
done
echo "renaming $file to $newname.";
mv -i "$file" "$newname" ## Remove the -i option if you think it's safe already.
done
When doing things recursively:
#!/bin/bash
shopt -s extglob
counter=1
while read file; do
dirprefix=${file%%+([^/])
until
newfile=$dirprefix/file_$(( counter++ )).jpg
[[ ! -e $newfile ]]
do
continue
done
echo "renaming $file to $newfile."
mv -i "$file" "$newfile" ## Remove the -i option if you think it's safe already.
done < <(find -type f -name '*.jpg' -and -not -regex '^.*/file_[0-9]\+$')

Resources