Dealing with a whole bunch of two-line config files, I’d like a way to exclude any files that have a different number of lines.
So, something like:
mv * destdir only if file contains exactly two lines
Or:
wc -l * | grep '^ *2' | xargs mv {} destdir
Except that neither of those is actual working code.
While writing this I realized I do have a way to do this, which is ugly as heck, and I’ve included it below as an answer.
Is there an easy/clean way to do this?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
Your kludgy solution isn’t too bad for starters… you are just missing the fact that not only can awk give you the number of lines, you can also instruct it to exit with the right status code so that you can then chain it with the cp command:
for file in * ; do awk 'NR==3{exit}END{exit NR!=2}' "$file" && cp "$file" /tmp; done
NR is the number of records, and as suggested in @don_crissti‘s answer, we can use the NR==3 check to stop further processing once we encounter a third line.
NR!=2 looks funny, because awk‘s true/false values are 1/0, but in the shell, we need 0 to represent a success status for && to work correctly. The inverse of that works too (depending on how strongly do you react to seeing !=):
for file in * ; do awk 'NR==3{exit}END{exit NR==2}' "$file" || cp "$file" /tmp; done
Method 2
You could use awk, exit on line 3 (the END rule is still executed) and exit 1 in the END block if no. of lines is not 2 e.g. with zsh:
print -rl -- *(.e_'awk "NR==3{exit}END{if(NR!=2){exit 1}}" $REPLY'_)
will list two-line files in the current directory; replace print -rl with mv and add the destination if you want to move them.
With other shells:
for file in ./*; do [[ -f $file ]] &&
awk 'NR==3{exit};END{if(NR!=2){exit 1}}' "$file" && mv "$file" "$dest"
done
Other ways, e.g. with z shell
and gnu awk:
awk 'ENDFILE{if(FNR==2){print FILENAME}}' ./*(.)
or gnu sed (v. 4.2.2 or later):
sed -ns '2{$F}' ./*(.)
to list the two-line files1 and e.g.:
for f (./*(.))
sed -n '2{$Q 1};3q' $f || mv $f $dest
to move them.
1: those would both go through the whole input so not really suited if you’re working with huge files; in that case, you may want to sed -n '2{$F};3q' for each file or use the first awk solution
Method 3
if your filenames are fairly sane, and you can delimit on both : and a newline, then:
grep -m3 '' ./* ./*/* |
cut -d: -f1 | uniq -c |
grep -v '^ *[13] '
^that command will list all not-dot files in the current directory and in all immediate child directories which contain only two lines.
You don’t really need to worry about sorting for uniq, because globs are sorted. I use the GNU -max match option because it is much faster if grep quits at the third input line than it is if it continues through to the end, but it will work without it as well. The idea is to get grep to print the filenames for each line they contain, then to count the occurences of each filename in its output, and then to filter out anything more or less than 2.
I ran it against some random source code dirs, and, of all of them, I had two files which contained only the two lines:
2 ./dex/coll.sh
2 ./jimtcl/jim-config.h.in
it would be neater to replace the last line with:
... |
sed -ne's/^ *2 *//p'
…though.
Method 4
I worked out the following kludgy solution:
for file in * ; do if [ "$(wc -l "$file" | awk '{print $1}')" == "2" ] ; then cp "$file" /tmp/ ; fi; done
There must be a better way which doesn’t start two processes for every single file in the current directory.
Method 5
Using the -t target directory option on mv with xargs:
wc -l * | sed -n 's/^[[:space:]]*2[[:space:]]+//p' | xargs mv -t "$DESTDIR"
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0