Rename .txt files - bash

Looking for help with the following code. I have a folder titled data, with 6 subfolders (folder1, folder2, etc). Each folder has a text file I want to rename to "homeworknotes" keeping the .txt extension.
Used notes before for short:
So far I have the following code:
for file in data/*/*.txt; do
mv $file "notes"
done

find
You can use find command with -execdir that will execute command of your choice in a directory where file matching pattern is:
find data -type f -name '*.txt' -execdir mv \{\} notes.txt \;
data is path to directory where find should look for matching files recursively
-type f look only for files, not directories
-name '*.txt' match anything that ends with .txt
-execdir mv \{\} notes.txt run command mv {} notes.txt in directory where file was found; where {} is original filename found by find.
bash
EDIT1: To do this without find you need to handle recursive directory traversal (unless you have fixed directory layout). In bash you can set following shell options with shopt -s command:
extglob - extended globbing support (allows to write extended globs like **; see "Pathname Expansion" in man bash)
globstar - allows ** in pathname expansion; **/ will match any directories and their subdirectories (see "Pathname Expansion" in man bash).
nullglob - allows patterns that match no files (in case there's a directory without any .txt file)
Following script will traverse directories under data/ and rename .txt files to notes.txt:
#!/bin/bash
shopt -s extglob globstar nullglob
for f in data/**/*.txt ; do
mv $f $(dirname $f)/notes.txt
done
mv $f $(dirname $f)/notes.txt moves (renames) file; $f contains matched path so e.g. data/folder1/day4notes.txt and $(dirname $f) gets directory where that file is - in this case data/folder1 so we just append /notes.txt to that.
EDIT2: If you are absolutely positive that you want to do this only in first level of subdirectories under data/ you can omit extglob and globstar (and if you know there's at least one .txt in each directory then also nullglob) and go ahead with pattern you posted; but you still need to use mv $f $(dirname $f)/notes.txt to rename file.
NOTE: When experimenting with things like these always make backup beforehand. If you have multiple .txt files in any of directories they all will get renamed to notes.txt so you might lose data in that case.

Related

find and delete empty files in directory and its subdirs without find

I am trying to make a bash script that finds and removes empty files in a directory including subdirectories, without using the find command.
This is part of the script using the find command but I am unsure how to convert this line without using find.
find . -type f -empty -delete
Try this code:
# enable recursive globstar matching
shopt -s globstar
# directory to delete files from
dir="/tmp"
# loop through files recusively
for f in ${dir}/* ${dir}/**/* ; do
# check if file is empty
if [ ! -s "$f" ]; then
# remove file
rm "$f"
fi
done

Bash: Loop through each file in each subfolder and rename

I'm in a directory with 3 subdirectories: sub1, sub2, and sub3. Each subdirectory has files in it. I would like to rename each file by prepending sample_ to it.
Here's what I have:
for d in */; do
for f in "$d"; do
mv "$f" "sample_$f"
done
done
This prepends to the folder name, which isn't what I want. What am I doing incorrectly?
Thanks!
You can easily accomplish this with find and brace expansion (part of shell expansion):
find . -type f -execdir mv {,sample_}{} \;
This should recursively find only files (-type f) within each subdirectory then move them (renaming them) using the -execdir option (see below), prepending sample_ to each filename. The remaining mv {,_sample}{} is the Cartesian product way of doing mv {} sample_{}.
-execdir command {} + Like -exec, but the specified command is run from the subdirectory
containing the matched file, which is not normally the directory in
which you started find. This a much more secure method for invoking
commands, as it avoids race conditions during resolution of the paths
to the matched files. As with the -exec option, the '+' form of
-execdir will build a command line to process more than one matched file, but any given invocation of command will only list files that
exist in the same subdirectory. If you use this option, you must
ensure that your $PATH environment variable does not reference the
current directory; otherwise, an attacker can run any commands they
like by leaving an appropriately-named file in a directory in which
you will run -execdir.
↳ GNU : Brace / Shell Expansions
you need to use dirname and basename to split your file name.
for d in */; do
for f in $d/*; do
mv "$f" "$d/sample_$(basename $f)"
done
done

Trying to rename certain file types within recursive directories

I have a bunch of files within a directory structure as such:
Dir
SubDir
File
File
Subdir
SubDir
File
File
File
Sorry for the messy formatting, but as you can see there are files at all different directory levels. All of these file names have a string of 7 numbers appended to them as such: 1234567_filename.ext. I am trying to remove the number and underscore at the start of the filename.
Right now I am using bash and using this oneliner to rename the files using mv and cut:
for i in *; do mv "$i" "$(echo $i | cut -d_ -f2-10)"; done
This is being run while I am CD'd into the directory. I would love to find a way to do this recursively, so that it only renamed files, not folders. I have also used a foreach loop in the shell, outside of bash for directories that have a bunch of folders with files in them and no other subdirectories as such:
foreach$ set p=`echo $f | cut -d/ -f1`
foreach$ set n=`echo $f | cut -d/ -f2 | cut -d_ -f2-10`
foreach$ mv $f $p/$n
foreach$ end
But that only works when there are no other subdirectories within the folders.
Is there a loop or oneliner I can use to rename all files within the directories? I even tried using find but couldn't figure out how to incorporate cut into the code.
Any help is much appreciated.
With Perl‘s rename (standalone command):
shopt -s globstar
rename -n 's|/[0-9]{7}_([^/]+$)|/$1|' **/*
If everything looks fine remove -n.
globstar: If set, the pattern ** used in a pathname expansion context will
match all files and zero or more directories and subdirectories. If
the pattern is followed by a /, only directories and subdirectories
match.
bash does provide functions, and these can be recursive, but you don't need a recursive function for this job. You just need to enumerate all the files in the tree. The find command can do that, but turning on bash's globstar option and using a shell glob to do it is safer:
#!/bin/bash
shopt -s globstar
# enumerate all the files in the tree rooted at the current working directory
for f in **; do
# ignore directories
test -d "$f" && continue
# separate the base file name from the path
name=$(basename "$f")
dir=$(dirname "$f")
# perform the rename, using a pattern substitution on the name part
mv "$f" "${dir}/${name/#???????_/}"
done
Note that that does not verify that file names actually match the pattern you specified before performing the rename; I'm taking you at your word that they do. If such a check were wanted then it could certainly be added.
How about this small tweak to what you have already:
for i in `find . -type f`; do mv "$i" "$(echo $i | cut -d_ -f2-10)"; done
Basically just swapping the * with `find . -type f`
Should be possible to do this using find...
find -E . -type f \
-regex '.*/[0-9]{7}_.*\.txt' \
-exec sh -c 'f="${0#*/}"; mv -v "$0" "${0%/*}/${f#*_}"' {} \;
Your find options may be different -- I'm doing this in FreeBSD. The idea here is:
-E instructs find to use extended regular expressions.
-type f causes only normal files (not directories or symlinks) to be found.
-regex ... matches the files you're looking for. You can make this more specific if you need to.
exec ... \; runs a command, using {} (the file we've found) as an argument.
The command we're running uses parameter expansion first to grab the target directory and second to strip the filename. Note the temporary variable $f, which is used to address the possibility of extra underscores being part of the filename.
Note that this is NOT a bash command, though you can of course run it from the bash shell. If you want a bash solution that does not require use of an external tool like find, you may be able to do the following:
$ shopt -s extglob # use extended glob format
$ shopt -s globstar # recurse using "**"
$ for f in **/+([0-9])_*.txt; do f="./$f"; echo mv "$f" "${f%/*}/${f##*_}"; done
This uses the same logic as the find solution, but uses bash v4 extglob to provide better filename matching and globstar to recurse through subdirectories.
Hope these help.

How to retain folder structure in bash scripting?

I made a program to convert the bitrate of music. The program is as follows..
for f in *.mp3 ;
do lame --mp3input -b $bitrate "$f" $path_to_destination/"$f" ;
done;
But this works for only one folder; I have music in different folders. How to modify the code so that it can recursively make conversions happen yet retain the folder structure in the output?
If you have a new enough Bash (version 4.3 works; version 3.x does not), you can use:
shopt -s globstar nullglob
for file in *.mp3 **/*.mp3
do
lame --mp3input -b $bitrate "$file" "$path_to_destination/$file"
done
The globstar option means that the ** notation works recursively; the nullglob option means that if there are no .mp3 files in any of the subdirectories (or no sub-directories), you get nothing generated instead of a name **/*.mp3 which would happen by default.
Because this uses globbing, it is safe even with paths or file names that contain spaces, newlines or other awkward characters.
If the sub-directories don't necessarily exist under $path_to_destination, then you need to create them. Add:
mkdir -p $(dirname "$path_to_destination/$file")
before the invocation of lame. This creates all the missing directories on the path leading to the target file (no error if all the directories already exist), leaving lame to create the file in that directory.
find . -type f -name '*.mp3' | while IFS= read -r src
do
dst="$path_to_destination/$src"
mkdir -p $(dirname "$dst")
lame --mp3input -b $bitrate "$src" "$dst"
done

Bash scripting, loop through files in folder fails

I'm looping through certain files (all files starting with MOVIE) in a folder with this bash script code:
for i in MY-FOLDER/MOVIE*
do
which works fine when there are files in the folder. But when there aren't any, it somehow goes on with one file which it thinks is named MY-FOLDER/MOVIE*.
How can I avoid it to enter the things after
do
if there aren't any files in the folder?
With the nullglob option.
$ shopt -s nullglob
$ for i in zzz* ; do echo "$i" ; done
$
for i in $(find MY-FOLDER/MOVIE -type f); do
echo $i
done
The find utility is one of the Swiss Army knives of linux. It starts at the directory you give it and finds all files in all subdirectories, according to the options you give it.
-type f will find only regular files (not directories).
As I wrote it, the command will find files in subdirectories as well; you can prevent that by adding -maxdepth 1
Edit, 8 years later (thanks for the comment, #tadman!)
You can avoid the loop altogether with
find . -type f -exec echo "{}" \;
This tells find to echo the name of each file by substituting its name for {}. The escaped semicolon is necessary to terminate the command that's passed to -exec.
for file in MY-FOLDER/MOVIE*
do
# Skip if not a file
test -f "$file" || continue
# Now you know it's a file.
...
done

Resources