I need to rename a list of files changing any file extension to lowercase:
ie: from My_TEST.ONE.two.Three.fOuR.FIve to My_TEST.one.two.three.four.five
At the moment the way I've found is this one
#!/bin/bash
sourcefilename="My_TEST.ONE.two.Three.fOuR.FIve"
newfilename=""
for word in $(echo $sourcefilename | tr '.' '\n'); do
if [ -z "$newfilename" ]; then
newfilename="$word"
else
newfilename="$newfilename.$(echo $word | tr [:upper:] [:lower:])"
fi
done
Is there a better (and maybe elegant) approach?
Use bash Parameter Expansion features.
fileName='My_TEST.ONE.two.Three.fOuR.FIve'
first="${fileName%%.*}"
rest="${fileName#*.}"
echo mv -v "${fileName}" "${first}.${rest,,[A-Z]}"
Related
My current code:
while read -r rbv_line || [[ -n "$rbv_line" ]]; do
if [[ "${rbv_line}" =~ ${rbv_reg} ]]; then
rbv_downcase="${BASH_REMATCH[0],,}" &&
ruby_version="${rbv_downcase//[^0-9a-z\.\-]/}" &&
((reg_matches="${reg_matches}"+1))
printf "\n"
printf "Setting Ruby version: %s\n" "${ruby_version}"
break
fi
done < "${1}"
It does what I want. But I would love to know if I can simplify this code even more, hoping someone can help me understand the syntax.
If you see these two lines:
rbv_downcase="${BASH_REMATCH[0],,}" &&
ruby_version="${rbv_downcase//[^0-9a-z\.\-]/}" &&
Initially I tried to combine those into one using something like this:
ruby_version="${BASH_REMATCH[0],,//[^0-9a-z\.\-]/}"
That does not work.
Is there a way to combine those two parameter expansions (,, and the //[^0-9a-z\.\-]/) or is passing it through an intermediary variable the right approach?
You can view the latest version of the code here:
https://github.com/octopusnz/scripts
You cannot combine multiple parameter expansions, but...
... you can simplify this code!
The biggest gain is by using already available tools.
Instead of looping, let's use grep. It's supposed to do something when RegEx pattern is occurred, so:
grep -E "$rbv_reg" "$1" # -E is for extended RegEx
I guess your pattern isn't case sensitive, so let's disable it with -i flag.
The loop breaks after match, so let's pass -m 1 to stop processing file after first match.
You want to convert uppercase to lowercase, so let's pipe it through tr:
grep -m 1 -E -i "$rbv_reg" "$1" | tr '[:upper:]' '[:lower:]'
You then replace some characters with //[^0-9a-z\.\-]/, piping it to sed will do the trick:
grep -m 1 -E -i "$rbv_reg" "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^0-9a-z\.\-]//g'
And at the very end, let's grab the output to variable:
ruby_version="$( grep -m 1 -E -i '$rbv_reg' '$1' | tr '[:upper:]' '[:lower:]' | sed 's/[^0-9a-z\.\-]//g' )"
Since you are printing new line anyway, let's use simple echo instead of printf
All what's left is if [ -n "$ruby_version" ] to increment reg_matches
At the end, we got:
ruby_version="$(
grep -m 1 -E -i '$rbv_reg' '$1' |
tr '[:upper:]' '[:lower:]' |
sed 's/[^0-9a-z\.\-]//g'
)"
if [ -n "$ruby_version" ]; then
reg_matches="$((reg_matches+1))"
echo
echo "Setting Ruby version: $ruby_version"
fi
The advantage of above code is the fact it isn't really Bash dependent and should work in any POSIX Bourne compatible shell.
Iam trying a shell script which will rename all the files in the current directory whose name contains upper-case characters into all lower case. For example, if the directory contains a file whose name is CoUnt.c, it should be renamed to count.c.
for f in *;
do
if [ -f "$f" ]; then
tr 'A-Z' 'a-z'
fi
done
but it is not working.
is there is any better solution for this?
You are not passing any data into the tr program, and you are not capturing any output either.
If you are using sh:
for f in *[A-Z]*
do
if [ -f "$f" ]; then
new_name=$(echo "$f"|tr 'A-Z' 'a-z')
mv "$f" "$new_name"
fi
done
Note the indentation - it makes code easier to read.
If you are using bash there is no need to use an external program like tr, you can use bash expansion:
for f in *[A-Z]*
do
if [[ -f $f ]]; then
new_name=${f,,*}
mv "$f" "$new_name"
fi
done
The problem is tr accepts values from stdin. So in order to translate upper to lower in each filename, you could do something like:
#!/bin/sh
for f in *
do
[ -f "$f" ] || continue
flc=$(echo "$f" | tr 'A-Z' 'a-z') ## form lower-case name
[ "$f" != "$flc" ] && echo mv "$f" "$flc"
done
(note: remove the echo before mv to actually move the files after you are satisfied with the operation)
Since I am unable to add comment posting here,
Used sed and it works for me
#!/bin/bash
for i in *
do
if [ -f $i ]
then
kar=$(echo "$i" | sed 's/.*/ \L&/')
mv "$i" "$kar"
done
The following code works fine.
for f in *
do
if [ -f $f ]; then
echo "$f" | tr 'A-Z' 'a-z' >/dev/null
fi
done
I would recommend rename because it is simple, efficient and also will check for clashes when two different files resolve to the same result:
You can use it with a Perl regex:
rename 'y/A-Z/a-z/' *
Documentation and examples available here.
I am trying to write a bash script that convert all file names to lowercase, but I have a problem because it does not work for one case.
When you have your file1 and FILE1, and you will use it on the FILE1 it will replace letters file1.
#!/bin/bash
testFILE=""
FLAG="1"
for FILE in *
do
testFILE=`echo FILE | tr '[A-Z]' '[a-z]'`
for FILE2 in *
do
if [ `echo $testFILE` = `echo $FILE2` ]
then
FLAG="0"
fi
done
if [ $FLAG = "1" ]
then
mv $FILE `echo $FILE | tr '[A-Z]' '[a-z]'`
fi
FLAG="1"
done
Looks like
testFILE=`echo FILE | tr '[A-Z]' '[a-z]'`
should be
testFILE=`echo "$FILE" | tr '[A-Z]' '[a-z]'`
Re-writing your script to fix some other minor things
#!/bin/bash
testFILE=
FLAG=1
for FILE in *; do
testFILE=$(tr '[A-Z]' '[a-z]' <<< "$FILE")
for FILE2 in *; do
if [ "$testFILE" = "$FILE2" ]; then
FLAG=0
fi
done
if [ $FLAG -eq 1 ]; then
mv -- "$FILE" "$(tr '[A-Z]' '[a-z]' <<< "$FILE")"
fi
FLAG=1
done
Quote variables to prevent word-splitting ("$FILE" instead of $FILE)
Generally preferable to use $() instead of tildes
Don't use string comparison where you don't have to
Use -- to delimit arguments in commands that accept it (in order to prevent files like -file from being treated as options)
By convention, you should really only use capital variable names for environment variables, though I kept them in above.
Pipes vs here strings (<<<) doesn't matter so much here, but <<< is slightly faster and generally safer.
Though more simply, I think you want
#!/bin/bash
for file in *; do
testFile=$(tr '[A-Z]' '[a-z]' <<< "$file")
[ -e "$testFile" ] || mv -- "$file" "$testFile"
done
Or on most modern mv implementations (not technically posix)
#!/bin/bash
for file in *; do
mv -n -- "$file" "$(tr '[A-Z]' '[a-z]' <<< "$file")"
done
From the man page
-n, --no-clobber
do not overwrite an existing file
Below script :
find . -mindepth 1 -maxdepth 1 -print0 | while read -d '' filename
do
if [ -e ${filename,,} ]
then
mv --backup ${filename} ${filename,,} 2>/dev/null
# create a backup of the desination only if the destination already exist
# suppressing the error caused by moving the file to itself
else
mv ${filename} ${filename,,}
fi
done
may do the job for you.
Advantages of this script
It will parse files containing newlines.
It avoids a prompt by doing selective backup destination that already exists.
How can I tokenize $PATH by using awk?
I tried 3 hours, but it totally screwed out.
#!/bin/bash
i=1
while true; do
token=$($echo $PATH | awk -F ':' '{print $"$i"}')
if [ -z "$token" ]; then
break
fi
((i++))
if [ -a "$TOKEN/$1" ]; then
echo "$TOKEN/$1"
break
fi
break
done
When I run this code, I got
/home/$USERID/bin/ff: line 6: /home/$USERID/bin:/usr/local/symlinks:/usr/local/scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/$USERID/bin: No such file or directory
How can I change my program?
What are you trying to do?
This will let you iterate against the individual paths:
echo $PATH | tr ':' '\n' | while read line; do echo $line; done
As #SiegeX notes, an even shorter version works
echo $PATH | while read -d ':' line; do echo $line; done
Do the whole thing in awk
#!/bin/bash
awk -v addPath="$1" 'BEGIN{RS=":";ORS=addPath "\n"}{$1=$1}1' <<< $PATH
Proof of Concept
$ addPath="/foo"
$ awk -v addPath="$addPath" 'BEGIN{RS=":";ORS=addPath "\n"}{$1=$1}1' <<< $PATH
/usr/local/bin/foo
/usr/bin/foo
/bin/foo
/usr/games/foo
/usr/lib/java/bin/foo
/usr/lib/qt/bin/foo
/usr/share/texmf/bin/foo
./foo
/sbin/foo
/usr/sbin/foo
/usr/local/sbin/foo
I think simple tr : \\n would suffice. Pipe it with sed 's#$#blabla#g' to add something to the lines and that's it.
You don't need to use external tools such as awk or tr to tokenize the PATH. Bash is capable of doing so:
#!/bin/sh
IFS=:
for p in $PATH
do
if [ -a "$p/$1" ]; then
echo "$p/$1"
break
fi
done
The IFS is a bash built-in variable which bash use as an input field separator (IFS).
I need a command that will return the top level base directory for a specified path in bash.
I have an approach that works, but seems ugly:
echo "/go/src/github.myco.com/viper-ace/psn-router" | cut -d "/" -f 2 | xargs printf "/%s"
It seems there is a better way, however all the alternatives I've seen seem worse.
Thanks for any suggestions!
One option is using awk:
echo "/go/src/github.myco.com/viper-ace/psn-router" |
awk -F/ '{print FS $2}'
/go
As a native-bash approach forking no subshells and invoking no other programs (thus, written to minimize overhead), which works correctly in corner cases including directories with newlines:
topdir() {
local re='^(/+[^/]+)'
[[ $1 =~ $re ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}
Like most other solutions here, invocation will then look something like outvar=$(topdir "$path").
To minimize overhead even further, you could pass in the destination variable name rather than capturing stdout:
topdir() {
local re='^(/+[^/]+)'
[[ $1 =~ $re ]] && printf -v "$2" '%s' "${BASH_REMATCH[1]}"
}
...used as: topdir "$path" outvar, after which "$outvar" will expand to the result.
not sure better but with sed
$ echo "/go/src/github.myco.com/viper-ace/psn-router" | sed -E 's_(/[^/]+).*_\1_'
/go
Here's a sed possibility. Still ugly. Handles things like ////////home/path/to/dir. Still blows up on newlines.
$ echo "////home/path/to/dir" | sed 's!/*\([^/]*\).*!\1!g'
/home
Newlines breaking it:
$ cd 'testing '$'\n''this'
$ pwd
/home/path/testing
this
$ pwd | sed 's!/*\([^/]*\).*!/\1!g'
/home
/this
If you know your directories will be rather normally named, your and anubhava's solutions certainly seem to be more readable.
This is bash, sed and tr in a function :
#!/bin/bash
function topdir(){
dir=$( echo "$1" | tr '\n' '_' )
echo "$dir" | sed -e 's#^\(/[^/]*\)\(.*\)$#\1#g'
}
topdir '/go/src/github.com/somedude/someapp'
topdir '/home/somedude'
topdir '/with spaces/more here/app.js'
topdir '/with newline'$'\n''before/somedir/somefile.txt'
Regards!