5

I have to find files that containing exactly 16 lines in Bash.

My idea is:

find -type f | grep '/^...$/'

Does anyone know how to utilise find + grep or maybe find + awk?

Then,

  • Move the matching files another directory.
  • Deleting all non-matching files.
doubleDown
  • 8,048
  • 1
  • 32
  • 48

7 Answers7

7

I would just do:

wc -l **/* 2>/dev/null | awk '$1=="16"'
Benoit
  • 76,634
  • 23
  • 210
  • 236
  • +1 but you need `==` rather than `=`, and you may want `2>/dev/null` on `wc` in case there are subdirectories. – cmbuckley Jun 17 '13 at 12:42
  • what does **/* and 2>/dev/null do? – user2493340 Jun 18 '13 at 08:51
  • @user2493340 : **/* in Bash expands to all files including in subfolders (can be unset: man bash can tell you about globbing options). 2>/dev/null redirects errors (due to directories) to nowhere. – Benoit Jun 21 '13 at 09:26
5

Keep it simple:

find . -type f |
while IFS= read -r file
do
    size=$(wc -l < "$file")
    if (( size == 16 ))
    then
        mv -- "$file" /wherever/you/like
    else
        rm -f -- "$file"
    fi
done

If your file names can contain newlines then google for the find and read options to handle that.

Ed Morton
  • 188,023
  • 17
  • 78
  • 185
  • Hi! Personally I do not really like the `something | while` stuff. It makes a `fork` (actually `clone`), which it not really needed and the user will be surprised if the variable defined in `while`-loop will not appear later. So I may suggest to use some file redirection stuff (`exec 10< <(find...)` + `read -u 10 file` or even put `find` into `heredoc` etc.). – TrueY Jun 18 '13 at 08:51
  • Oops I forgot the simplest one: `while ... done < <(find...)`. – TrueY Jun 19 '13 at 10:47
2

You should use grep instead of wc because wc counts newline characters \n and will not count if the last line doesn't ends with a newline.

e.g.

grep -cH '' * 2>/dev/null | awk -F: '$2==16'

for more correct approach (without error messages, and without argument list too long error) you should combine it with the find and xargs commands, like

find . -type f -print0 | xargs -0 grep -cH '' | awk -F: '$2==16'

if you don't want count empty lines (so only lines what contains at least one character), you can replace the '' with the '.'. And instead of awk, you can use second grep, like:

find . -type f -print0 | xargs -0 grep -cH '.' | grep ':16$'

this will find all files what are contains 16 non-empty lines... and so on..

cajwine
  • 3,100
  • 1
  • 20
  • 41
1

GNU sed

sed -E '/^.{16}$/!d' file
Community
  • 1
  • 1
Endoro
  • 37,015
  • 8
  • 50
  • 63
0

A pure version:

#!/usr/bin/bash

for f in *; do # Look for files in the present dir
  [ ! -f "$f" ] && continue # Skip not simple files
  cnt=0
  # Count the first 17 lines
  while ((cnt<17)) && read x; do ((++cnt)); done<"$f"
  if [ $cnt == 16 ] ; then echo "Move '$f'"
  else echo "Delete '$f'"
  fi
done
TrueY
  • 7,360
  • 1
  • 41
  • 46
  • That's looking for lines that are 16 characters long, not files that are 16 lines long. It has other issues too. – Ed Morton Jun 17 '13 at 12:52
  • @EdMorton: Unfortunately it was not clear from the question. Thanks! – TrueY Jun 17 '13 at 12:54
  • agreed. The question says one thing but then the attempted solution is for the other. I'm just assuming it's more likely that the question is correct than that the attempted solution is. – Ed Morton Jun 17 '13 at 12:58
  • @EdMorton: Anyway thx! Corrected for 16 lines... :) Still in bash. – TrueY Jun 17 '13 at 13:02
0

I would go with

find . -type f | while read f ; do
  [[ "${f##*/}" =~ ^.{16}$ ]] && mv "${f}" <any_directory>  || rm -f "${f}"
done

or

find . -type f | while read f ; do
  [[ $(echo -n "${f##*/}" | wc -c) -eq 16 ]] && mv "${f}" <any_directory>  || rm -f "${f}"
done

Replace <any_directory> with the directory you actually want to move the files to.

BTW, find command will go sub-directories. if you don't want this, then you should change the find command to fit your need.

Jose
  • 123
  • 1
  • 12
0

This snippet will do the work:

find . -type f -readable -exec bash -c \
    'if(( $(grep -m 17 -c "" "$0")==16 )); then echo "file $0 has 16 lines"; else echo "file $0 doesn'"'"'t have 16 lines"; fi' {} \;

Hence, if you need to delete the files that are not 16 lines long, and move those who are 16 lines long to folder /my/folder, this will do:

find . -type f -readable -exec bash -c \
    'if(( $(grep -m 17 -c "" "$0")==16 )); then mv -nv "$0" /my/folder; else rm -v "$0"; fi' {} \;

Observe the quoting for "$0" so that it's safe regarding any file name with funny symbols in it (spaces, ...).

I'm using the -v option so that rm and mv are verbose (I like to know what's happening). The -n option to mv is no-clobber: a security to not overwrite an existing file; this option might not be available if you have an old system.

The good thing about this method. It's really safe regarding any filename containing funny symbols.

The bad thing(s). It forks a bash and a grep and an mv or rm for each file found. This can be quite slow. This can be fixed using trickier stuff (while still remaining safe regarding funny symbols in filenames). If you really need it, I can give you a possible answer. It will also break if file can't be (re)moved.

Remark. I'm using the -readable option to find, so that it only considers files that are readable. If you have this option, use it, you'll have a more robust command!

gniourf_gniourf
  • 44,650
  • 9
  • 93
  • 104