@sphen and
@cadreher76
One thing I will say is:
Using the output from
ls
isn't a very good way of getting a list of files to use in a script, or to pass to other commands, because the output from the
ls
command isn't predictable, or consistent and can cause problems if file-names have spaces, or other special characters in them.
If you're using the shell in interactive mode, poking around looking for files, then by all means - use
ls
and filter the results with
grep
, to find what you're looking for. But you shouldn't attempt to parse the output of
ls
in scripts in any way, shape or form.
For more information, see this link:
For getting a list of files, it's better to create/populate an array using either
readarray
with data redirected from the
find
command, or by using a
for
loop with globbing.
E.g.
Using
readarray
with
find
:
Bash:
readarray -d '' -t fileList < <(find ./ -maxdepth 1 -type f -iname "*.txt" -print0)
numFiles=${#fileList[@]}
In the above, the
readarray
command populates an array called
fileList
with results that are redirected from a
find
command.
In the above, there are actually two redirections going on.
The left-most redirection
<
is redirecting a stream of data to the
readarray
command. The right-most redirection
<(find ......blah blah blah)
is a process redirection, which redirects the output from
find
, into the left-most redirection
<
. Which effectively feeds
readarray
the output from
find
.
To explain the options used with
readarray[icode]:
The [icode]-d
option specifies what delimiter is to be used, so it knows where each array element starts/ends. In this case, we've used two single quotes
''
, which specifies nothing as a delimiter. An empty delimiter. This causes
readarray
to use any
null
characters (
\0
) in the received stream of data as delimiters.
The
-t fileList
parameter, specifies that the array to create/populate is called
fileList
.
In the
find
command:
The
-maxdepth 1
parameter tells
find
NOT to recurse into any sub-directories. So we'll only get results for files in the current directory.
-type f
specifies that we're only looking for files.
-iname "*.txt"
specifies that we're looking for any files that end with
.txt
(Obviously
@cadreher76 - you should substitute
"*.txt"
with whatever pattern you're looking for).
The
-print0
option causes
find
to append a
null
character (
\0
) to the end of each array item, so each file-name will end with a null character.
And because
readarray
is set up to use
null
characters as separators, we can guarantee that each array element will contain the full filename of each file.
That way, any spaces, or special characters present in filenames will be safely preserved in the filenames in the array,with no fear of problems with shell expansions, globbing, or word-splitting.
So by using
find
to get the filenames and using
readarray
to populate the array, we don't risk any of the problems that could be caused by parsing and using the output from
ls
, which can cause intermittent errors/side-effects in scripts.
So that is why the combination of
readarray
and
find
is better and safer than using the output from
ls
.
The other option is to use a
for
loop with shell globbing:
Bash:
fileList=()
for file in ./*.txt; do
# Ensure that we're definitely dealing with a text file
# and not a directory or some other file-system object using .txt in the name.
if [[ -f "$file" ]]; then
fileList+=("$file") # it's a file, so append it to the array
fi
done
numFiles=${#fileList[@]}
Not really a lot to say about this. This is a slightly more old-school approach.
We declare an array variable called
fileList
, we use a
for
loop, to loop through each file-system object in the current directory that contains
.txt
at the end of it's name.
We then check each object and ensure it is
definitely a file that we're dealing with, before storing the filename in the array.
The reason we ensure it's a file is:
Because you may have a directory that has .txt in the name, or a symbolic link, or a named pipe etc. Some other file-system object that isn't actually a file and that you might not actually want to be copying later in your script! So, in this particular situation, it's a good idea to ensure the obects we're dealing with are actually files, before storing their paths/names in the array.
Personally, I tend to use
readarray
with process redirection, because there are less lines of code required. Also
find
is an absolute
beast when it comes to finding files. You can set up some insanely complicated and powerful searches and define multiple actions for different types of files.
Anyway - regardless of which method you choose to use - both methods I've demonstrated will populate an array called
fileList
with the filenames of any text files in the current directory. And will do so in a better, safer, more reliable and predictable way than if you tried to parse/process the output from
ls
.
Once you've got your array populated with a bunch of filenames, you can use a for loop to output the indices and filenames, before prompting the user to make a selection and perform the rest of the actions in your script.
I hope this helps!
EDIT:
An additional tip for preventing bugs in your scripts.... Always ensure you parse, validate and range-check ALL user input. Even if you're going to be the only user of the script!
All user input should be treated with absolute suspicion. The rule of thumb is "All user input is guilty, until proven innocent!". So when reading numeric input from a user - ensure that it is actually all numeric and that the values entered are within the expected ranges. If the user enters something out of range, or unexpected - your script needs to be able to handle it gracefully.
In my experience, users are utter bum-holes!