Replacing both spaces and " - " in filenames and directorys - linux

I have been reading this this discussion and this find . -depth -name '* *' \ | while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done
helps me deleting spaces in files and directories.
Beat Boy becomes Beat_Boy. This is ok.
What I don't get right is how to deal with this:
Beat Boy - Best of becomes Beat_Boy_-_Best_of while I want it to be Beat_Boy-Best_of.
I would appreciate any hint which way to go...
Regards

You can add sed to substitute "_-_" with "-"
f="Beat Boy - Best of"
echo $f | tr ' ' _ | sed 's/_-_/-/g'
#Beat_Boy-Best_of
In your case, you would want:
find . -depth -name '* *' |
while IFS= read -r f ; do
mv -i "$f" "$(dirname "$f")/$(basename "$f" |
tr ' ' _ |
sed 's/_-_/-/g')" ;
done
Edit
You can also replace tr ' ' _ | sed 's/_-_/-/g' with sed 's/ /_/g ; s/_-_/-/g'.

f="Beat Boy - Best of"
f1=${f// - /-}
f1=${f1// /_}
echo $f1
Beat_Boy-Best_of

This solution just replaces any number of [ ' ' or '-' ] with a single '_'. I assume that's probably what you want.
The find command still only searches for files with spaces in them, but you can change that to suit your needs.
while IFS= read -r f ;
mv -i "$f" "$(dirname "$f")/$(basename "$f" | sed -re 's/[ -]+/_/g')";
done < <(find . -depth -name '* *')
Full credit: this takes LC-datascientist's solution and replacing the somewhat awkward combination of tr and sed. Even Doyousketch2's comment about sed didn't use s///g option to make it simpler.

Related

How to adapt the next command to be able to change any character

Im trying to rename a whole bulks of files with underscores, hashtags and a bunch of characters that the ftp servers have trouble dealing it.
I have always have resorted to do it with
find . -depth -name '* *' \
| while IFS= read -r f ; do
mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)"
done
but I have failed to adapt the tr part to make the change, this way too
....tr '(' '_')"
here I want to change the ( character.
UPDATE
As noted by wjandrea, I did not update the definition in the -name parameter of the find comand, it should be
find . -depth -name '*(*'
To replace the space character, !, (, ) and # with an underscore, you could use:
find . -depth -name '*[ !()#]*' -exec sh -c '
for f; do
mv -i "$f" "$(dirname "$f")/$(basename "$f" | tr " !()#" _)"
done
' sh {} +
If your find supports the -execdir action used in combination with the Perl rename tool:
find . -name '*[ !()#]*' -execdir rename -n 's/[ !()#]/_/g' {} +
Remove option -n if the output looks as expected.
Your command tr '(' '_' translates an open parenthesis into an underscore. Nothing more.
From the tr man-page:
tr [OPTION]... SET1 [SET2]
.... SET2 is extended to length of SET1 by repeating its last character as necessary. ....
Hence, you need to specify all the characters to be translated, for instance:
tr '()#!' _

How to find files that have long chains of zero bytes?

How can I find files that have long chains of consecutive 0s (zero bytes - 0x00) as a result of disk failure? For example, how can I find files that have more than 10000 zero bytes in sequence?
Sure, I can write a program using Java or other programming language, but is there a way to do it using more or less standard Linux command line tools?
Update
You can generate test file with dd if=/dev/zero of=zeros bs=1 count=100000.
This may be a start:
find /some/starting/point -type f -size +10000 -exec \
perl -nE 'if (/\x0{10000}/) {say $ARGV; close ARGV}' '{}' +
To test for a single file, named filename:
if tr -sc '\0' '\n' < filename | tr '\0' Z | grep -qE 'Z{1000}'; then
# ...
fi
You can now use a suitable find command to filter relevant files for test.
For example, all *.txt files in PWD:
while read -rd '' filename;do
if tr -sc '\0' '\n' < "$filename" | tr '\0' Z | grep -qE 'Z{1000}'; then
# For example, simply print "$filename"
printf '%s\n' "$filename"
fi
done < <(find . -type f -name '*.txt' -print0)
Find and grep should work just fine:
grep -E "(\0)\1{1000}" <file name>
if it's a single file or a group of files in the same dir
If you want to search throughout the system there's:
find /dir/ -exec grep -E "(\0)\1{1000}" {} \; 2> /dev/null
this is very slow though, if you're looking for something faster and can do without the thousand(or large number) of zeros
I'll suggest replacing the grep with 'grep 000000000*' instead

rename files with one single command [duplicate]

Can anyone recommend a safe solution to recursively replace spaces with underscores in file and directory names starting from a given root directory? For example:
$ tree
.
|-- a dir
| `-- file with spaces.txt
`-- b dir
|-- another file with spaces.txt
`-- yet another file with spaces.pdf
becomes:
$ tree
.
|-- a_dir
| `-- file_with_spaces.txt
`-- b_dir
|-- another_file_with_spaces.txt
`-- yet_another_file_with_spaces.pdf
I use:
for f in *\ *; do mv "$f" "${f// /_}"; done
Though it's not recursive, it's quite fast and simple. I'm sure someone here could update it to be recursive.
The ${f// /_} part utilizes bash's parameter expansion mechanism to replace a pattern within a parameter with supplied string.
The relevant syntax is ${parameter/pattern/string}. See: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html or http://wiki.bash-hackers.org/syntax/pe .
Use rename (aka prename) which is a Perl script which may be on your system already. Do it in two steps:
find . -name "* *" -type d | rename 's/ /_/g' # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'
Based on Jürgen's answer and able to handle multiple layers of files and directories in a single bound using the "Revision 1.5 1998/12/18 16:16:31 rmb1" version of /usr/bin/rename (a Perl script):
find /tmp/ -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done
failed to get it right at first, because I didn't think of directories.
you can use detox by Doug Harple
detox -r <folder>
A find/rename solution. rename is part of util-linux.
You need to descend depth first, because a whitespace filename can be part of a whitespace directory:
find /tmp/ -depth -name "* *" -execdir rename " " "_" "{}" ";"
you can use this:
find . -depth -name '* *' | while read fname
do
new_fname=`echo $fname | tr " " "_"`
if [ -e $new_fname ]
then
echo "File $new_fname already exists. Not replacing $fname"
else
echo "Creating new file $new_fname to replace $fname"
mv "$fname" $new_fname
fi
done
bash 4.0
#!/bin/bash
shopt -s globstar
for file in **/*\ *
do
mv "$file" "${file// /_}"
done
Recursive version of Naidim's Answers.
find . -name "* *" | awk '{ print length, $0 }' | sort -nr -s | cut -d" " -f2- | while read f; do base=$(basename "$f"); newbase="${base// /_}"; mv "$(dirname "$f")/$(basename "$f")" "$(dirname "$f")/$newbase"; done
In macOS
Just like the chosen answer.
brew install rename
#
cd <your dir>
find . -name "* *" -type d | rename 's/ /_/g' # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'
For those struggling through this using macOS, first install all the tools:
brew install tree findutils rename
Then when needed to rename, make an alias for GNU find (gfind) as find. Then run the code of #Michel Krelin:
alias find=gfind
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done
Here's a (quite verbose) find -exec solution which writes "file already exists" warnings to stderr:
function trspace() {
declare dir name bname dname newname replace_char
[ $# -lt 1 -o $# -gt 2 ] && { echo "usage: trspace dir char"; return 1; }
dir="${1}"
replace_char="${2:-_}"
find "${dir}" -xdev -depth -name $'*[ \t\r\n\v\f]*' -exec bash -c '
for ((i=1; i<=$#; i++)); do
name="${#:i:1}"
dname="${name%/*}"
bname="${name##*/}"
newname="${dname}/${bname//[[:space:]]/${0}}"
if [[ -e "${newname}" ]]; then
echo "Warning: file already exists: ${newname}" 1>&2
else
mv "${name}" "${newname}"
fi
done
' "${replace_char}" '{}' +
}
trspace rootdir _
This one does a little bit more. I use it to rename my downloaded torrents (no special characters (non-ASCII), spaces, multiple dots, etc.).
#!/usr/bin/perl
&rena(`find . -type d`);
&rena(`find . -type f`);
sub rena
{
($elems)=#_;
#t=split /\n/,$elems;
for $e (#t)
{
$_=$e;
# remove ./ of find
s/^\.\///;
# non ascii transliterate
tr [\200-\377][_];
tr [\000-\40][_];
# special characters we do not want in paths
s/[ \-\,\;\?\+\'\"\!\[\]\(\)\#\#]/_/g;
# multiple dots except for extension
while (/\..*\./)
{
s/\./_/;
}
# only one _ consecutive
s/_+/_/g;
next if ($_ eq $e ) or ("./$_" eq $e);
print "$e -> $_\n";
rename ($e,$_);
}
}
An easy alternative to recursive version is to increase the range of for loop step by step(n times for n sub-levels irrespective of number of sub-directories at each level). i.e from the outermost directory run these.
for f in *; do mv "$f" "${f// /_}"; done
for f in */*; do mv "$f" "${f// /_}"; done
for f in */*/*; do mv "$f" "${f// /_}"; done
To check/understand what's being done, run the following before and after the above steps.
for f in *;do echo $f;done
for f in */*;do echo $f;done
for f in */*/*;do echo $f;done
I found around this script, it may be interesting :)
IFS=$'\n';for f in `find .`; do file=$(echo $f | tr [:blank:] '_'); [ -e $f ] && [ ! -e $file ] && mv "$f" $file;done;unset IFS
Here's a reasonably sized bash script solution
#!/bin/bash
(
IFS=$'\n'
for y in $(ls $1)
do
mv $1/`echo $y | sed 's/ /\\ /g'` $1/`echo "$y" | sed 's/ /_/g'`
done
)
This only finds files inside the current directory and renames them. I have this aliased.
find ./ -name "* *" -type f -d 1 | perl -ple '$file = $_; $file =~ s/\s+/_/g; rename($_, $file);
I just make one for my own purpose.
You may can use it as reference.
#!/bin/bash
cd /vzwhome/c0cheh1/dev_source/UB_14_8
for file in *
do
echo $file
cd "/vzwhome/c0cheh1/dev_source/UB_14_8/$file/Configuration/$file"
echo "==> `pwd`"
for subfile in *\ *; do [ -d "$subfile" ] && ( mv "$subfile" "$(echo $subfile | sed -e 's/ /_/g')" ); done
ls
cd /vzwhome/c0cheh1/dev_source/UB_14_8
done
For files in folder named /files
for i in `IFS="";find /files -name *\ *`
do
echo $i
done > /tmp/list
while read line
do
mv "$line" `echo $line | sed 's/ /_/g'`
done < /tmp/list
rm /tmp/list
My solution to the problem is a bash script:
#!/bin/bash
directory=$1
cd "$directory"
while [ "$(find ./ -regex '.* .*' | wc -l)" -gt 0 ];
do filename="$(find ./ -regex '.* .*' | head -n 1)"
mv "$filename" "$(echo "$filename" | sed 's|'" "'|_|g')"
done
just put the directory name, on which you want to apply the script, as an argument after executing the script.
Use below command to replace space with underscore in filename as well as directory name.
find -name "* *" -print0 | sort -rz | \
while read -d $'\0' f; do mv -v "$f" "$(dirname "$f")/$(basename "${f// /_}")"; done
If you need to rename only files in one directory by replacing all spaces. Then you can use this command with rename.ul:
for i in *' '*; do rename.ul ' ' '_' *; done
Actually, there's no need to use rename script in perl:
find . -depth -name "* *" -execdir bash -c 'mv "$1" `echo $1 | sed s/ /_/g`' -- {} \;

Remove ./ from output

I have a code to output data to a txt file but i would like to remove the ./ from the output on each file name
code is as follows
#!/bin/bash
# fill with more extensions or have it as a cmd line arg
TYPES=( mov mp4 avi mp3 wma)
DIR=$1
# Create a regex of the extensions for the find command
echo "Available Media Files in Directory"
TYPES_RE="\\("${TYPES[1]}
for t in "${TYPES[#]:1:${#TYPES[*]}}"; do
TYPES_RE="${TYPES_RE}\\|${t}"
done
TYPES_RE="${TYPES_RE}\\)"
# Set the field seperator to newline instead of space
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
# Generate output from path and size using: `stat -c "%s" filepath`
OUTPUT=""
for f in `find ${DIR} -type f -regex ".*\.${TYPES_RE}"`; do
OUTPUT=`echo ${f}`";"$OUTPUT
done
# Reset IFS
IFS=$SAVEIFS
# Reverse numeric sort the output and replace ; with \n for printing
echo $OUTPUT | tr ';' '\n' | sed 's/.*/"&"/' | sort -nr >playlist.txt
Result is:
"./You Da One.mp3"
"./Wiz Khalifa Roll Up.mp4"
"./Vybz Kartel neva get a gal.mp3"
"./Tyga Rack City.mp4"
"./Tyga Lap Dance.mp4"
"./Travis Porter Make It Rain.mp4"
"./Travis Porter ft. Tyga Ayy Ladies.mp4"
"./Snoop Dogg feat. Wiz Khalifa Bruno Mars Young Wild & Free.mp4"
"./Shot Caller.mp3"
"./Chris Brown - Your Body.mp4"
"./Chris Brown Turn Up The Music.mp4"
Need to remove the ./ from each file
Thanks
You can use basename command to strip the directory filename, in your script probably a good way to use it is in the find command, i.e.
find ${DIR} -type f -regex ".*\.${TYPES_RE}" -exec basename '{}' \;
change
echo $OUTPUT | tr ';' '\n' | sed 's/.*/"&"/' | sort -nr >playlist.txt
to
echo $OUTPUT | tr ';' '\n' | sed 's/^\.\///gi' | sed 's/.*/"&"/' | sort -nr >playlist.txt

How to replace spaces in file names using a bash script

Can anyone recommend a safe solution to recursively replace spaces with underscores in file and directory names starting from a given root directory? For example:
$ tree
.
|-- a dir
| `-- file with spaces.txt
`-- b dir
|-- another file with spaces.txt
`-- yet another file with spaces.pdf
becomes:
$ tree
.
|-- a_dir
| `-- file_with_spaces.txt
`-- b_dir
|-- another_file_with_spaces.txt
`-- yet_another_file_with_spaces.pdf
I use:
for f in *\ *; do mv "$f" "${f// /_}"; done
Though it's not recursive, it's quite fast and simple. I'm sure someone here could update it to be recursive.
The ${f// /_} part utilizes bash's parameter expansion mechanism to replace a pattern within a parameter with supplied string.
The relevant syntax is ${parameter/pattern/string}. See: https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html or http://wiki.bash-hackers.org/syntax/pe .
Use rename (aka prename) which is a Perl script which may be on your system already. Do it in two steps:
find . -name "* *" -type d | rename 's/ /_/g' # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'
Based on Jürgen's answer and able to handle multiple layers of files and directories in a single bound using the "Revision 1.5 1998/12/18 16:16:31 rmb1" version of /usr/bin/rename (a Perl script):
find /tmp/ -depth -name "* *" -execdir rename 's/ /_/g' "{}" \;
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done
failed to get it right at first, because I didn't think of directories.
you can use detox by Doug Harple
detox -r <folder>
A find/rename solution. rename is part of util-linux.
You need to descend depth first, because a whitespace filename can be part of a whitespace directory:
find /tmp/ -depth -name "* *" -execdir rename " " "_" "{}" ";"
you can use this:
find . -depth -name '* *' | while read fname
do
new_fname=`echo $fname | tr " " "_"`
if [ -e $new_fname ]
then
echo "File $new_fname already exists. Not replacing $fname"
else
echo "Creating new file $new_fname to replace $fname"
mv "$fname" $new_fname
fi
done
bash 4.0
#!/bin/bash
shopt -s globstar
for file in **/*\ *
do
mv "$file" "${file// /_}"
done
Recursive version of Naidim's Answers.
find . -name "* *" | awk '{ print length, $0 }' | sort -nr -s | cut -d" " -f2- | while read f; do base=$(basename "$f"); newbase="${base// /_}"; mv "$(dirname "$f")/$(basename "$f")" "$(dirname "$f")/$newbase"; done
In macOS
Just like the chosen answer.
brew install rename
#
cd <your dir>
find . -name "* *" -type d | rename 's/ /_/g' # do the directories first
find . -name "* *" -type f | rename 's/ /_/g'
For those struggling through this using macOS, first install all the tools:
brew install tree findutils rename
Then when needed to rename, make an alias for GNU find (gfind) as find. Then run the code of #Michel Krelin:
alias find=gfind
find . -depth -name '* *' \
| while IFS= read -r f ; do mv -i "$f" "$(dirname "$f")/$(basename "$f"|tr ' ' _)" ; done
Here's a (quite verbose) find -exec solution which writes "file already exists" warnings to stderr:
function trspace() {
declare dir name bname dname newname replace_char
[ $# -lt 1 -o $# -gt 2 ] && { echo "usage: trspace dir char"; return 1; }
dir="${1}"
replace_char="${2:-_}"
find "${dir}" -xdev -depth -name $'*[ \t\r\n\v\f]*' -exec bash -c '
for ((i=1; i<=$#; i++)); do
name="${#:i:1}"
dname="${name%/*}"
bname="${name##*/}"
newname="${dname}/${bname//[[:space:]]/${0}}"
if [[ -e "${newname}" ]]; then
echo "Warning: file already exists: ${newname}" 1>&2
else
mv "${name}" "${newname}"
fi
done
' "${replace_char}" '{}' +
}
trspace rootdir _
This one does a little bit more. I use it to rename my downloaded torrents (no special characters (non-ASCII), spaces, multiple dots, etc.).
#!/usr/bin/perl
&rena(`find . -type d`);
&rena(`find . -type f`);
sub rena
{
($elems)=#_;
#t=split /\n/,$elems;
for $e (#t)
{
$_=$e;
# remove ./ of find
s/^\.\///;
# non ascii transliterate
tr [\200-\377][_];
tr [\000-\40][_];
# special characters we do not want in paths
s/[ \-\,\;\?\+\'\"\!\[\]\(\)\#\#]/_/g;
# multiple dots except for extension
while (/\..*\./)
{
s/\./_/;
}
# only one _ consecutive
s/_+/_/g;
next if ($_ eq $e ) or ("./$_" eq $e);
print "$e -> $_\n";
rename ($e,$_);
}
}
An easy alternative to recursive version is to increase the range of for loop step by step(n times for n sub-levels irrespective of number of sub-directories at each level). i.e from the outermost directory run these.
for f in *; do mv "$f" "${f// /_}"; done
for f in */*; do mv "$f" "${f// /_}"; done
for f in */*/*; do mv "$f" "${f// /_}"; done
To check/understand what's being done, run the following before and after the above steps.
for f in *;do echo $f;done
for f in */*;do echo $f;done
for f in */*/*;do echo $f;done
I found around this script, it may be interesting :)
IFS=$'\n';for f in `find .`; do file=$(echo $f | tr [:blank:] '_'); [ -e $f ] && [ ! -e $file ] && mv "$f" $file;done;unset IFS
Here's a reasonably sized bash script solution
#!/bin/bash
(
IFS=$'\n'
for y in $(ls $1)
do
mv $1/`echo $y | sed 's/ /\\ /g'` $1/`echo "$y" | sed 's/ /_/g'`
done
)
This only finds files inside the current directory and renames them. I have this aliased.
find ./ -name "* *" -type f -d 1 | perl -ple '$file = $_; $file =~ s/\s+/_/g; rename($_, $file);
I just make one for my own purpose.
You may can use it as reference.
#!/bin/bash
cd /vzwhome/c0cheh1/dev_source/UB_14_8
for file in *
do
echo $file
cd "/vzwhome/c0cheh1/dev_source/UB_14_8/$file/Configuration/$file"
echo "==> `pwd`"
for subfile in *\ *; do [ -d "$subfile" ] && ( mv "$subfile" "$(echo $subfile | sed -e 's/ /_/g')" ); done
ls
cd /vzwhome/c0cheh1/dev_source/UB_14_8
done
For files in folder named /files
for i in `IFS="";find /files -name *\ *`
do
echo $i
done > /tmp/list
while read line
do
mv "$line" `echo $line | sed 's/ /_/g'`
done < /tmp/list
rm /tmp/list
My solution to the problem is a bash script:
#!/bin/bash
directory=$1
cd "$directory"
while [ "$(find ./ -regex '.* .*' | wc -l)" -gt 0 ];
do filename="$(find ./ -regex '.* .*' | head -n 1)"
mv "$filename" "$(echo "$filename" | sed 's|'" "'|_|g')"
done
just put the directory name, on which you want to apply the script, as an argument after executing the script.
Use below command to replace space with underscore in filename as well as directory name.
find -name "* *" -print0 | sort -rz | \
while read -d $'\0' f; do mv -v "$f" "$(dirname "$f")/$(basename "${f// /_}")"; done
If you need to rename only files in one directory by replacing all spaces. Then you can use this command with rename.ul:
for i in *' '*; do rename.ul ' ' '_' *; done
Actually, there's no need to use rename script in perl:
find . -depth -name "* *" -execdir bash -c 'mv "$1" `echo $1 | sed s/ /_/g`' -- {} \;

Resources