0

So I'm a beginner and i have a project due next week. I have to print only the filename of the files that contain #!/bin/bash on the first line. So far I tried this

head -n 1 $filename | grep -l "pattern"

but when I execute it instead of the name of the files I receive

(standard input)

Like I said, I'm a beginner and so far I'm familiar with simple commands. So I would like to know where I f****** up and if there is a way of acheiving what I want without using harder commands like awk.

for fis in  find -perm -a+x -name "*.sh" -type f
do
head -n 1 $fis | grep -l "#!/bin/bash"
done
Hauke Laging
  • 90,279
Laura
  • 1

6 Answers6

4

In GNU grep you can use the --label option to set the label/filename if the input comes from stdin.

Instead of using an outer loop with a command substitution, you could use the -exec action to start a small script to loop over the filenames and execute the commands.

find -perm -a+x -name "*.sh" -type f -exec sh -c '
  for fis; do
    head -n1 "$fis" | grep --label="$fis" -Fl "#!/bin/bash"
  done
' sh {} +

I added the -F option to grep since we're looking for a fixed string.

Freddy
  • 25,565
2

I know you have blacklisted awk, but for other people seeking help who come across your question, this is a quite efficient way to print the names of the executable .sh files that contain #!/bin/bash in their first line:

find . -perm -a+x -name '*.sh' -type f -exec \
    awk 'NR == 1{ if ($0 == "#!/bin/bash") print FILENAME; exit }' {} \;

Or, to print the name of files that have #!/bin/bash in their first line without further restrictions, drop the -perm and -name options:

find . -type f -exec awk ...
Ed Morton
  • 31,617
Quasímodo
  • 18,865
  • 4
  • 36
  • 73
  • @steeldriver More than that, it's the correct way, otherwise files with #!/bin/bash in 2nd line or after (and not in the 1st line) would also be printed. Thank you for the correction. – Quasímodo May 22 '20 at 03:43
  • @steeldriver True as well! I've set it as Community Wiki in case you see any more possible improvement. – Quasímodo May 22 '20 at 12:00
1
find . -perm -a+x -name '*.sh' -type f |
    while IFS= read -r file; do
        test -f "$file" || continue
        if head -n 1 "$file" | grep -qFx '#!/bin/bash'; then
            echo "$file"
        fi
    done
Hauke Laging
  • 90,279
  • YES!! oh my god thank you so much... I can t express in words how grateful I am. this works!! – Laura May 21 '20 at 23:24
  • @Laura I modified the files it is workingt on. Freddy's find approach is less readable but safer (regarding "evil" characters in file and directory names). – Hauke Laging May 21 '20 at 23:31
  • See https://mywiki.wooledge.org/BashFAQ/001 for how to read the output of a command using a loop without a pipe. Not sure what the point of testing for file being a file is when find is outputting file names. – Ed Morton May 26 '20 at 00:34
  • @EdMorton That is not about testing the file object type again but about detecting problems resulting from newlines in path names as that is not a safe -print0 solution. – Hauke Laging May 26 '20 at 00:48
  • 1
    If your file names can contain newlines then test -f "$file" || continue isn't a solution as it'll silently not test real files output by find and will let head ... execute on files that weren't output by find but might exist and share a common pre-newline part with the real file names (eg. find outputs foo\nbar.sh and there's a file named foo in the directory). IMHO you should just say "this won't work if your file names contain newlines" and get rid of that test line. – Ed Morton May 26 '20 at 00:57
0

with -n and passing multiple files the output will be in this format:

file_name:line_number:matched_text

-x matches the whole line

grep '#!/bin/bash' -nx *.sh |cut -d ':' -f 1,2 | grep ":1$" | cut -d ':' -f 1

binarysta
  • 3,032
  • Thank you for your suggestion, but I just tried it and it displays the numar of files that contains the pattern on the first line... i need the name of the filles :( – Laura May 21 '20 at 23:04
  • no this is correct, could you please try again? I added some details. – binarysta May 21 '20 at 23:05
  • @Quasímodo no the output will have only lines with #!/bin/bash from first grep. – binarysta May 21 '20 at 23:10
  • @Quasímodo yes, I edited. Thanks for mentioning – binarysta May 21 '20 at 23:16
  • Thank you for your effort, I really appreciate it and hopefully it will help others, it just didn't work for my program.. – Laura May 21 '20 at 23:25
  • @Laura thanks but what is the issue? this is just grep and cut should work – binarysta May 21 '20 at 23:27
  • I don't know... but the code from the other person that answered my questions worked immediately. – Laura May 21 '20 at 23:29
  • Maybe this doesn't work with the rest of my program, I really don't know. Programming is way harder than I thought.. – Laura May 21 '20 at 23:30
  • 1
    @EdMorton if multiple files passed to grep -n the output will include the filenames too, as I have mentioned in the answer. but we can add -H (not -h) to make it more clear. Thanks for other points, I fixed it. – binarysta May 26 '20 at 04:49
0

I realise this is very late to the party, but I've been doing exactly the same thing, and have had good results with this:

find . -perm -a+x -name "*.sh" -exec \
    bash -c 'grep -q "pattern" < <(head -n 1 "$1")' _ {} \; -printf '%p\n'

As understand it, the -q flag prevents writing to standard output, allowing you to print the filename (%p from find -printf).

AdminBee
  • 22,803
ps_tw
  • 1
  • 2
0

In zsh:

set -o extendedglob
zmodload zsh/system
bash_shebang_pattern=$'\\#! #(/usr/bin/env ##|(/usr(/local|)|/opt/gnu|)/bin/)bash[ \n]*'
improperly_named_bash_scripts=(
  **/*.sh(ND.L+11f[a+x]e['
    sysread -s80 shebang < $REPLY &&
      [[ $shebang = $~bash_shebang_pattern ]]
  '])
)
print -rl -- "These executable files have a .sh extension even though they're bash scripts:" $improperly_named_bash_scripts

That's widening a bit the possibilities of bash shebang, but note that possibilities are infinite if you want to consider BSD/GNU's env -S like:

#! /usr/bin/env --split-str=-u\_BASH_ENV\_-u\_ENV\_-u\_POSIXLY_CORRECT\_-u\_LC_ALL\_PATH=/opt/gnu/bin:${PATH}\_LC_CTYPE=C\_LC_COLLATE=C\_bash\_-o\_errexit\_-o\_nounset\_-o\_pipefail\_--

Which some might prefer over:

#! /bin/bash -

(let alone #!/bin/bash which strictly speaking is incorrect) to sanitize the environment a bit.

Details:

  • **/ any level of subdirectories.
  • *.sh files whose name ends in .sh (note: that includes the hidden file whose name is .sh).
  • (ND.L+11f[a+x]e['code']): glob qualifiers:
    • N: nullglob
    • D: dotglob (include hidden files)
    • .: regular files only (no directory, symlink, fifo, device...)
    • L+11: Length greater than 11 bytes (the size of #!/bin/bash)
    • f[a+x]: for which the permissions include x for all.
    • e['code']: runs the code to decide whether the file (in $REPLY) should be selected.
  • sysread -s80 only read the first 80 bytes. Should be enough even for a #! /usr/bin/env bash one. Not one line, as those could be huge for binary files which are not guaranteed to contain newline characters.