I have a directory containg several files, including those files some are bash scripts but their name doesn't include the .sh extension. I know I need to use the file command to know the type of files with -type option, I also know that I can redirect the output with xargs so I can rename the files with their origianl name + the .sh extension. The problem is I don't know how to pipe all the commands together, and how to find files type of only bash ASCII text executable. Any idea how to do that with a simple command?
2 Answers
If you've set up any of the standard shebang lines it looks like file
(at least version 5.39) should be able to detect them:
$ echo '#!/usr/bin/env bash' > 1
$ echo '#!/bin/bash' > 2
$ echo '#!/usr/bin/bash' > 3
$ file *
1: Bourne-Again shell script, ASCII text executable
2: Bourne-Again shell script, ASCII text executable
3: Bourne-Again shell script, ASCII text executable
So we should be able to do this:
for path in ./*
do
if [[ "$(file "$path")" = "${path}: Bourne-Again shell script, ASCII text executable" ]]
then
mv "$path" "${path}.bash"
fi
done
This works for the above example files (and should work for unusual filenames, including those containing space characters, starting with a hyphen, or ending with a newline):
$ ls
1.bash 2.bash 3.bash
This will probably not work if your files do not have shebang lines (which is reasonable for files which are sourced by other scripts or the shebang lines are wrong (such as #!/bin/sh
).
(I've used ".bash" since that is more explicit than ".sh".)
-
Be careful. I just wrote a tiny script with accented characters, and I get told
/tmp/xxx: Bourne-Again shell script, UTF-8 Unicode text executable
. The file is not executable, by the way (Fedora 33, UTF-8).– vonbrandFeb 16, 2021 at 0:51 -
@vonbrand
file
is fallible for sure - after all, it's a heuristic. I suspect "executable" here is probably meant in the "it's something you can run" sense rather than "it's a file with the executable flag set." Which is unfortunate but not technically wrong.– l0b0Feb 16, 2021 at 5:43 -
@alecxs You might want to post that as a separate answer with a bit of context.– l0b0Feb 16, 2021 at 20:19
-
what I'm concerned about is that the pattern you are looking for is not correct.– vonbrandFeb 28, 2021 at 20:30
-
@vonbrand The actual output of
file
is irrelevant here, the important thing is that it accurately identifies Bash scripts.– l0b0Feb 28, 2021 at 23:58
although that is not fulfilling your homework there is no need for find
as grep
kann search directories for files recursively with -r
(and even that is not needed when file names are passed with globbing)
grep -lIw '^#!.*bash' dir/!(*.sh) | xargs -I_ mv _ _.sh
grep -r
dir will search all files in all subdirectories (not used here)
grep -l
is similar to -H
print the file name for each match, but suppress normal output. furthermore it stops after first match similar to -q
or -m1
grep -I
exclude binary files, process text files only
grep -w
bash will match whole word only (won't match bashed)
regex ^
#! match begin of line only (won't match ##!)
regex .
match any byte *
any occurrence = .*
any string
pathname expansion (globbing) *.sh
matches pattern file.sh
extended globbing !(pattern)
matches anything except pattern
ls dir/!(*.sh)
list all files in dir excluding dir/file.sh files
xargs -I
pattern will insert arguments on each position pattern is used _
in this example:
mv _ _ .sh
mv file1 file1.sh
mv file2 file2.sh
-
linux.die.net/man/1/grep linux.die.net/man/1/xargs linuxjournal.com/content/bash-extended-globbing– alecxsFeb 16, 2021 at 22:29
-
-
... will also match '#!/bin/sh # this is not a bash script' or '#! like fruits of calabash tree' but prefer regex kiss for demonstration. focus was more on avoiding 'script.sh.sh.sh' on multiple runs– alecxsFeb 19, 2021 at 12:43
file
command is not overly reliable. For example, create a script that starts with twenty comment lines other than#!/bin/bash
.file
will categorize it as ASCII text (at least my version offile
does).file
, you can generate the list of shell scripts withfile * | grep shell | cut -f1 -d:
(assuming filenames don't contain colons). You step through this list with afor
loop and rename the files.#!/bin/bash
line must always be the first line; anything not on the first line is ignored (and the kernel will run the script with/bin/sh
)