for name in `ls` and filenames with spaces - bash

next code doesnt work because of spaces in file names, How to fix?
IFS = '\n'
for name in `ls `
do
number=`echo "$name" | grep -o "[0-9]\{1,2\}"`
if [[ ! -z "$number" ]]; then
mv "$name" "./$number"
fi
done

Just don't use command substitution: use for name in *.

Replace
for name in `ls`
with:
ls | while read name
Notice: bash variable scoping is awful. If you change a variable inside the loop, it won't take effect outside the loop (in my version it won't, in your version it will). In this example, it doesn't matter.
Notice 2: This works for file names with spaces, but fails for some other strange but valid file names. See Charles Duffy's comment below.

Looks like two potential issues:
First, the IFS variable and it's assignment should not have space in them. Instead of
IFS = '\n' it should be IFS=$'\n'
Secondly, for name in ls will cause issues with filename having spaces and newlines. If you just wish to handle filename with spaces then do something like this
for name in *
I don't understand the significance of the line
number=`echo "$name" | grep -o "[0-9]\{1,2\}"`
This will give you numbers found in filename with spaces in new lines. May be that's what you want.

For me, I had to move to use find.
find /foo/path/ -maxdepth 1 -type f -name "*.txt" | while read name
do
#do your stuff with $name
done

Related

Sed for loop GNU Linux on Synology NAS

I am working on a short script to search a large number of folders on a NAS for this odd character  and delete the character. I am on a Synology NAS running Linux. This is what I have so far.
#!/bin/bash
for file in "$(find "/volume1/PLNAS/" -depth -type d -name '**')";
do
echo "$file";
mv "$file" "$(echo $file | sed s/// )";
done
Current problem is that the Kernel does not appear to be passing each MV command separately. I get a long error message that appears to list every file in one command, truncated error message below. There are spaces in my file path and that it why I have tried to quote every variable.
mv: failed to access '/volume1/PLNAS/... UT Thickness Review ': File name too long
Several issues. The most important is probably that for file in "$(find...)" iterates only once with file set to the full result of your search. This is what the double quotes are for: prevent word splitting.
But for file in $(find...) is not safe: if some file names contain spaces they will be split...
Assuming the character is unicode 0xf028 (  ) try the following:
while IFS= read -r -d '' file; do
new_file="${file//$'\uf028'}"
printf 'mv %s %s\n' "$file" "$new_file"
# mv "$file" "$new_file"
done < <(find "/volume1/PLNAS/" -depth -type d -name $'*\uf028*' -print0)
Uncomment the mv line if things look correct.
As your file names are unusual we use the -d '' read separator and the print0 find option. This will use the NUL character (ASCII code zero) as separator between the file names instead of the default newline characters. The NUL character is the only one that you cannot find in a full file name.
We also use the bash $'...' expansion to represent the unwanted character by its unicode hexadecimal code, it is safer than copy-pasting the glyph. The new name is computed with the bash pattern substitution (${var//}).
Note: do not use echo with unusual strings, especially without quoting the strings (e.g. your echo $file | ...). Prefer printf or quoted here strings (sed ... <<< "$file").

How to sort output of the find command in a script

I have the following simple script to find all directories (at a depth of 2) that were added in the last N days...
#!/bin/bash
DAYS_PRIOR=180
DIR='/mydir'
FILES=`find $DIR -mindepth 2 -maxdepth 2 -type d -mtime -$DAYS_PRIOR -printf '%f\\\n'`
echo
echo "Files added in the last $DAYS_PRIOR days:"
echo
echo -e $FILES
echo
To get it to add newlines I had to double-escape the printf and use echo -e. That seems odd to me but it was the only way I could get it to print one directory per line on the output.
Everything works up to this point and I get a list of directories as expected. Now I want to sort the list alphabetically. I tried changing the printf in the find command to...
FILES=`find <xxx> -printf '%f\\\n' | sort`
however this doesn't sort the directory names. Based on other posts I tried the following..
FILES=`find <xxx> -printf %f\\\n | sort -t '\0' | awk -F '\0' '{print $0; print "\\\n"}'`
This is very close but leaves an extra space at the start of each line and seems horribly awkward.
Is there a simple method to add a sort to the original find command?
First: double-quote your variable references! When you use echo -e $FILES, the variable FILE's value gets split into "words" based on whitespace (spaces, tabs, and newlines), and then echo sticks those words back together with spaces between them. This has the effect of converting newlines into spaces. In order to wind up with newlines at the end, you're having to use \n instead of a true newline, and use echo -e to convert it. Just use real newlines, and put double-quotes around the variable reference to avoid all this mess:
FILES=$(find "$DIR" -mindepth 2 -maxdepth 2 -type d -mtime "-$DAYS_PRIOR" -printf '%f\n')
# ...
echo "$FILES"
Note that I put double-quotes around all variable references, since this is almost always a good idea. I also used $( ) instead of backticks -- it's easier to read, and avoids some parsing oddities that backticks have.
Anyway, with this format you're using proper newlines throughout, so piping through sort should work as expected.
BTW, I'd also recommend switching from uppercase variable names to lower- or mixed-case names, since there are a bunch of all-caps names that have special meanings, and if you accidentally use one of them bad things can happen.

Bash: Nested variable expansion

How can I nest operations in bash? e.g I know that
$(basename $var)
will give me just the final part of the path and
${name%.*}
gives me everything before the extension.
How do I combine these two calls, I want to do something like:
${$(basename $var)%.*}
As #sid-m 's answer states, you need to change the order of the two expansions because one of them (the % stuff) can only be applied to variables (by giving their name):
echo "$(basename "${var%.*}")"
Other things to mention:
You should use double quotes around every expansion, otherwise you run into trouble if you have spaces in the variable values. I already did that in my answer.
In case you know or expect a specific file extension, basename can strip that off for you as well: basename "$var" .txt (This will print foo for foo.txt in $var.)
You can do it like
echo $(basename ${var%.*})
it is just the order that needs to be changed.
Assuming you want to split the file name, here is a simple pattern :
$ var=/some/folder/with/file.ext
$ echo $(basename $var) | cut -d "." -f1
file
If you know the file extension in advance, you can tell basename to remove it, either as a second argument or via the -s option. Both these yield the same:
basename "${var}" .extension
basename -s .extension "${var}"
If you don't know the file extension in advance, you can try to grep the proper part of the string.
### grep any non-slash followed by anything ending in dot and non-slash
grep -oP '[^/]*(?=\.[^/]*$)' <<< "${var}"

How to surround find's -name parameter with wildcards before and after a variable?

I have a list of newline-separated strings. I need to iterate through each line, and use the argument surrounded with wildcards. The end result will append the found files to another text file. Here's some of what I've tried so far:
cat < ${INPUT} | while read -r line; do find ${SEARCH_DIR} -name $(eval *"$line"*); done >> ${OUTPUT}
I've tried many variations of eval/$() etc, but I haven't found a way to get both of the asterisks to remain. Mostly, I get things that resemble *$itemFromList, but it's missing the second asterisk, resulting in the file not being found. I think this may have something to do with bash expansion, but I haven't had any luck with the resources I've found so far.
Basically, need to supply the -name parameter with something that looks like *$itemFromList*, because the file has words both before and after the value I'm searching for.
Any ideas?
Use double quotes to prevent the asterisk from being interpreted as an instruction to the shell rather than find.
-name "*$line*"
Thus:
while read -r line; do
line=${line%$'\r'} # strip trailing CRs if input file is in DOS format
find "$SEARCH_DIR" -name "*$line*"
done <"$INPUT" >>"$OUTPUT"
...or, better:
#!/usr/bin/env bash
## use lower-case variable names
input=$1
output=$2
args=( -false ) # for our future find command line, start with -false
while read -r line; do
line=${line%$'\r'} # strip trailing CR if present
[[ $line ]] || continue # skip empty lines
args+=( -o -name "*$line*" ) # add an OR clause matching if this line's substring exists
done <"$input"
# since our last command is find, use "exec" to let it replace the shell in memory
exec find "$SEARCH_DIR" '(' "${args[#]}" ')' -print >"$output"
Note:
The shebang specifying bash ensures that extended syntax, such as arrays, are available.
See BashFAQ #50 for a discussion of why an array is the correct structure to use to collect a list of command-line arguments.
See the fourth paragraph of http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html for the relevant POSIX specification on environment and shell variable naming conventions: All-caps names are used for variables with meaning to the shell itself, or to POSIX-specified tools; lowercase names are reserved for application use. That script you're writing? For purposes of the spec, it's an application.

Replace underscores to whitespaces using bash script

How can I replace all underscore chars with a whitespace in multiple file names using Bash Script? Using this code we can replace underscore with dash. But how it works with whitespace?
for i in *.mp3;
do x=$(echo $i | grep '_' | sed 's/_/\-/g');
if [ -n "$x" ];
then mv $i $x;
fi;
done;
Thank you!
This should do:
for i in *.mp3; do
[[ "$i" = *_* ]] && mv -nv -- "$i" "${i//_/ }"
done
The test [[ "$i" = *_* ]] tests if file name contains any underscore and if it does, will mv the file, where "${i//_/ }" expands to i where all the underscores have been replaced with a space (see shell parameter expansions).
The option -n to mv means no clobber: will not overwrite any existent file (quite safe). Optional.
The option -v to mv is for verbose: will say what it's doing (if you want to see what's happening). Very optional.
The -- is here to tell mv that the arguments will start right here. This is always good practice, as if a file name starts with a -, mv will try to interpret it as an option, and your script will fail. Very good practice.
Another comment: When using globs (i.e., for i in *.mp3), it's always very good to either set shopt -s nullglob or shopt -s failglob. The former will make *.mp3 expand to nothing if no files match the pattern (so the loop will not be executed), the latter will explicitly raise an error. Without these options, if no files matching *.mp3 are present, the code inside loop will be executed with i having the verbatim value *.mp3 which can cause problems. (well, there won't be any problems here because of the guard [[ "$i" = *_* ]], but it's a good habit to always use either option).
Hope this helps!
The reason your script is failing with spaces is that the filename gets treated as multiple arguments when passed to mv. You'll need to quote the filenames so that each filename is treated as a single agrument. Update the relevant line in your script with:
mv "$i" "$x"
# where $i is your original filename, and $x is the new name
As an aside, if you have the perl version of the rename command installed, you skip the script and achieve the same thing using:
rename 's/_/ /' *.mp3
Or if you have the more classic rename command:
rename "_" " " *.mp3
Using tr
tr '_' ' ' <file1 >file2

Resources