Bash.Bad result of command substitution - bash

I want to replace spaces in filenames. My test directory contains files with spaces:
$ ls
'1 2 3.txt' '4 5.txt' '6 7 8 9.txt'
For example this code works fine:
$ printf "$(printf 'spaces in file name.txt' | sed 's/ /_/g')"
spaces_in_file_name.txt
I replace spaces on underscore and command substitution return result to double quotes as text. This construction with important substitution is essential in the next case. Such commands as find and xargs have substitution mark like {}(curly braces). Therefore the next command can replace spaces in files.
$ find ./ -name "*.txt" -print0 | xargs --null -I '{}' mv '{}' "$( printf '{}' | sed 's/ /_/g' )"
mv: './6 7 8 9.txt' and './6 7 8 9.txt' are the same file
mv: './4 5.txt' and './4 5.txt' are the same file
mv: './1 2 3.txt' and './1 2 3.txt' are the same file
But I get error. In order to more clearly consider error, instead of mv I just use echo(or printf):
$ find ./ -name "*.txt" -print0 | xargs --null -I '{}' echo "$( printf '{}' | sed 's/ /_/g' )"
./6 7 8 9.txt
./4 5.txt
./1 2 3.txt
As we can see, spaces were not replaced on underscore. But without command substitution, the replacing will be correct:
$ find ./ -name "*.txt" -print0 | xargs --null -I '{}' printf '{}\n' | sed 's/ /_/g'
./6_7_8_9.txt
./4_5.txt
./1_2_3.txt
So the fact of the command substitution with curly braces is corrupt the result(because in the first command was correct result), but without command substitution the result is correct. But why???

Your command substitution is run before find and you're executing
mv '{}' "{}"
You could change the find command to match .txt files with at least one space character and use -exec and a small bash script to rename the files:
find . -type f -name "* *.txt" -exec bash -c '
for file; do
fname=${file##*/}
mv -i "$file" "${file%/*}/${fname// /_}"
done
' bash {} +
${file##*/} remove the parent directories (longest prefix pattern */) and leaves the filename (like the basename command)
${file%/*} removes the filename (shortest suffix pattern /*) and leaves the parent directories (like the dirname command)
${fname// /_} replaces all spaces with underscores

it's quite fast and simple with loop just replace absolute_path with your path :
for f in absolute_path/*.txt; do mv "$f" "${f// /_}";done
The ${f// /_} part utilizes bash's parameter expansion mechanism to replace a pattern within a parameter with supplied string.

Related

Bash Recursively Remove Leading Underscore

I'm attempting to recursively remove a leading underscore from some scss files. Here's what I have.
find . -name '*.scss' -print0 | xargs -0 -n1 bash -c 'mv "$0" `echo $0 | sed -e 's:^_*::'`'
When I'm in a specific directory this works perfectly:
for FILE in *.scss; do mv $FILE `echo $FILE | sed -e 's:^_*::'`; done
What am I doing wrong in the find?
As the starting point is ., all paths that find prints start with a dot. Thus ^_* doesn't match anything and sed returns its input unchanged.
I wouldn't bother with sed or xargs though.
The script below works with any find and sh that isn't terribly broken, and properly handles filenames with underscores in the middle as well.
find . -name '_*.scss' -exec sh -c '
for fp; do # pathname
fn=${fp##*/} # filename
fn=${fn#"${fn%%[!_]*}"} # filename w/o leading underscore(s)
echo mv "$fp" "${fp%/*}/$fn"
done' sh {} +
A less portable but shorter and much cleaner alternative in bash looks like:
shopt -s globstar extglob nullglob
for fp in ./**/_*.scss; do
echo mv "$fp" "${fp%/*}/${fp##*/+(_)}"
done
Drop echo if the output looks good.
Look at the syntax highlighting of the sed command. Single quotes can't be nested. Easiest fix: switch to double quotes.
find . -name '*.scss' -print0 | xargs -0 -n1 bash -c 'mv "$0" `echo $0 | sed -e "s:^_*::"`'
I would recommend leaving $0 as the program name and using $1 for the first argument. That way if bash prints an error message it'll prefix it with bash:.
find . -name '*.scss' -print0 | xargs -0 -n1 bash -c 'mv "$1" `echo "$1" | sed -e "s:^_*::"`' bash
You can also simplify this with find -exec.
find . -name '*.scss' -exec bash -c 'mv "$1" `echo "$1" | sed -e "s:^_*::"`' bash {} ';'
You could also let bash do the substitution with its ${var#prefix} prefix removal syntax:
find . -name '*.scss' -exec bash -c 'mv "$1" "${1#_}"' bash {} ';'
You don't need find at all (unless you are trying to do this with an ancient version of bash, such as what macOS ships):
shopt -s globstar extglob
for f in **/*.scss; do
mv -- "$f" "${f##*(_)}"
done
${f##...} expands f, minus the longest prefix matching .... The extended pattern *(_) matches 0 or more _, analogous to the regular expression _*.

Recursive removal of trailing whitespace in directory

Structure
/some/dir/a b c d /somedir2/somedir4
/some/dir/abcderf/somedir123/somedir22
Problem
Need to recursively remove the trailing whitespace in directories, in the example "a b c d" has a whitespace at the end, and "somedir22" could have a whitespace on its end which needs removal.
There's hundreds of directories and would like to recursively iterate each directory to check if the directory has a trailing whitespace, and if it does, to rename the directory without the whitespace. Bash is my only option at the moment as this is running on a Western Digital NAS.
I think the worst part is, that each time you mv a directory, the directories within that directory change the path.
So we need to make find process each subdirectory before the directory itself. Thank you #thatotherguy for the -depth option which needs to be passed to find. With some fancy -exec sh script, we can just find all directories that end with trailing space and process each directory's conetnt before the directory itself. For each directory, run a shell script, which removes trailing spaces and mvs the directory:
find . -type d -regex '.* ' -depth \
-exec sh -c 'mv -v "$1" "$(echo "$1" | sed "s/ *$//")"' -- {} \;
#edit I leave my previous answers as a reference:
find . -type d -regex '.* ' -printf '%d\t%p\n' |
sort -r -n -k1 | cut -f2- |
xargs -d '\n' -n1 sh -c 'mv -v "$1" "$(echo "$1" | sed "s/ *$//")"' --
The first two lines get the paths sorted in reverse order according to the depth of the path. So that "./a /b " is renamed to "./a /b " before "./a " get's renamed to "./a". The last command removes the trailing spaces from the path using sed and then calls mv. Tested it on tutorialspoint.
I think we can make the xargs line simpler by using perl's rename utility (but it has to be perls, not the one from util-linux):
.... |
xargs -d '\n' rename 's/ *$//'
Well we could rename ' ' '' with util-linux rename, but that would remove all the spaces, we want trailing ones only.

Getting rid of spaces in filenames (Unix/Linux)

The problem is well-known yet I cannot find a solution: I wish to rename directories and files, replacing the spaces by underscores. The below code does not work, for some unclear reason. This is in bash on Solaris 11.
find $BASEDIR -type d |grep ' ' | awk '{print "\"" $0 "\""}' >$TMPFIL
cat $TMPFIL | while read FILNAM ; do
F2=$(echo $FILNAM | sed -e 's/ /_/g' -e 's/\"//g')
CMD="mv "$FILNAM" "$F2
echo "Will execute: "$CMD
$CMD
done
Output looks like
Will execute:
mv "/opt/pakket/smbdata/INTSCAN/ipmdata/Errors/Herstart 2015-01-07" /opt/pakket/smbdata/INTSCAN/ipmdata/Errors/Herstart_2015-01-07
mv: /opt/pakket/smbdata/INTSCAN/ipmdata/Errors/Herstart_2015-01-07 not found
it complains about not finding the second argument. When I copy and paste the command it generates, and launch it "manually", everything works fine, though.
This should do:
find /path/to/dir -depth -name '*[[:space:]]*' -execdir bash -c 'echo mv "$0" "${0//[[:space:]]/_}"' {} \;
This will not rename anything, only show the commands that will be executed. Remove the echo in front of mv to actually perform the renaming.
If your find doesn't support -execdir (which seems to be the case on your Solaris), this should do:
find -depth -name '*[[:space:]]*' -exec bash -c 'd=${0%/*} b=${0##*/}; echo mv "$d/$b" "$d/${b//[[:space:]]/_}"' {} \;

batch rename folder deleting trailing characters

I have a lot of directory that end with "_ and 6 digits", eg:
diff_gb_and_pf_2voids_158543
I would like to find all that folders in the current folder, and rename them by deleting the "_" and the 6 digits at the end.
So far I'm stuck with this command:
find . -type d -print |grep '.*[0-9]\{6\}$' |xargs -I {} bash -c 'for i in {}; do mv "$i" ????; done;'
I can't find how to do the last step. I would try and call sed, but how ?
Also, if there is a nicer way, please tell.
Thanks
Here's one way using your shell:
for i in $(find . -mindepth 1 -type d -regextype posix-extended -regex '.*_[0-9]{6}'); do
mv "$i" "${i%_*}";
done
Here you go:
find /path -regex '.*_[0-9]\{6\}' -exec sh -c 'n="{}"; echo mv "{}" "${n%_*}"' \;
Check the output, if it looks good then drop the echo in there.
Explanation: for each matched file, we run a sub-shell, where we assign the filename to variable n, so that we can use pattern substitution ${n%_*}, which cuts off the last _ character and everything after it until the end of the filename.
Or here's a more portable way that should work in older systems too:
find /path -name '*_[0-9][0-9][0-9][0-9][0-9][0-9]' | sed -ne 's/\(.*\)_[0-9]\{6\}$/mv "&" "\1"/p'
Check the output, if it looks good than pipe it to sh (append this: | sh)
Explanation:
The sed command receives the list of files to rename
In the pattern we capture the first part of the filename within \( ... \)
We replace the pattern with the text mv "&" "\1", where & is substituted with the pattern that was matched, in this case the entire original filename, and \1 is substituted with the part we captured within \( ... \)
#!/usr/bin/env bash
set -x
ls -d diff* > dirlist
while IFS='_' read field1 field2 field3 field4 field5 field6
do
mv -v "${field1}_${field2}_${field3}_${field4}_${field5}_${field6}" \
"${field1}_${field2}_${field3}_${field4}_${field5}"
done < dirlist

execute shell command with variable

To execute 'find' with some variables from txt file i made this
but it doesn't work.
is that wrong with execute statement?
#/bin/bash
while read line;
do
echo tmp_name: $line
for ST in 'service.getFile("'$line;
do
find ./compact/ -type f -exec grep -l $ST {} \;
done
done < tmpNameList.txt
Try and quote $ST in your find command.
What's more:
since you operate from the current directory, ./ is not necessary;
you don't seem to have any special regex character (the ( needs to be quoted in grep's classical regex mode, and I assume you did mean a literal dot), so use fgrep instead (or grep -F). Ie:
find compact/ -type f -exec fgrep -l "$ST" {} \;
grep can read multiple patterns from a file (-f option):
find ./compact/ -type f -exec grep -f patterns.txt {} +
where patterns.txt (prepend 'service.getFile(' to each line) is:
sed 's/^/service.getFile(/' tmpNameList.txt >patterns.txt

Resources