Bash Scripting

cadreher76

New Member
Joined
Feb 27, 2023
Messages
3
Reaction score
0
Credits
30
I have a script with <ls | grep ".*.fileext" | nl> the intent is for a user to select a number associated with the file, like the output below, to finish executing the script. I can already have them type in the filename and finish the script. I am attempting to google it, but not sure I have the correct terminology (i.e. array (static/dynamic), variable). I am looking to be pointed in the correct direction as opposed to an answer. Thanks in advance

1 file1
2 file2
3 file3
etc....
 


Moved to the 'Command Line' sub-forum, where questions about scripting are best suited.
 
he intent is for a user to select a number associated with the file, like the output below, to finish executing the script.
What actions is the script supposed to perform? To me it seems that you are trying to figure out some sort of flow control. Flow control in bash is mostly done with either if/else statements, loops, or case. If you want the user to select from several options (kind of like you have listed at the bottom of your post) then you probably best off with case statements.

One thing you could do, is once you've figured out how the user will select the file, is that you could use the bash command to run a particular script, if that is what you are looking for...or you could use functions which can be called by the script.
 
Below is a example of what I am trying to do. I can change $number to $filename and it works fine. Was just trying to clean it up for a single keystroke to eliminate user error. Some of my users won't read any errors.

#! /bin/bash

#list contents of directory using numbers
cd <path> && ls | grep ".*.<ext>" | nl

#user selects number associated with filename
read -p "Enter a number: " number

# copy file to our home directory
sudo cp -v <currentpath>/$number <newpath>/$number

# take ownership of moved file
sudo chown -v <user>:<user> <path>/$number
 
Below is a example of what I am trying to do. I can change $number to $filename and it works fine. Was just trying to clean it up for a single keystroke to eliminate user error. Some of my users won't read any errors.

#! /bin/bash

#list contents of directory using numbers
cd <path> && ls | grep ".*.<ext>" | nl

#user selects number associated with filename
read -p "Enter a number: " number

# copy file to our home directory
sudo cp -v <currentpath>/$number <newpath>/$number

# take ownership of moved file
sudo chown -v <user>:<user> <path>/$number
Im curious, is there some other process on your system that writes lists of files to a separate file? Because as it is, it doesn't seem like it will work.
 
I'm also confused with this whole "finish the script" thing, it appears like you want users to switch to files when the end result is changing the ownership? Clearly, your work is cut out for you...
 
Here is the script from start to finish. I may be overthinking it, but wanted a user to enter the correlating number to the file.

Snap 2023-02-28 at 11.24.48 (2).png
 
Here is the script from start to finish. I may be overthinking it, but wanted a user to enter the correlating number to the file.

View attachment 15166
Okay, it is used to copy and change file ownership between directories, i get it now...

I personally always use a fake username during installation for this purpose, saves typing and clicking when i ask people for help on the internet.
 
I would like to make it very very clear that I am an utter novice at shell scripting.

I was intrigued by the problem, and here is my crude solution to selecting an individual file. There is probably a much better and more standard way to do this, but it works. Tested on Mac (bash, not zsh) and Linux Mint. Copy this script to a separate file and run it to see how it works:

Code:
#! /bin/bash

# Copy the list of files into an array.
# CRUDE!! Does not filter out directories, etc. Fix this to get only the desired files.
filearray=($(ls))

# Display the list of files in the current directory, with numbers
echo $(ls | nl)

# Copied from cadreher76's original code:
#user selects number associated with filename
read -p "Enter a number: " number

# Check to see that user entered a valid number
filecount=${#filearray[@]}
if [[ $number -lt 1 ]] || [[ $number -gt $filecount ]]
  then
    echo "Enter a valid number please!"
    echo "End of program."
    exit 1
fi
  
# Numbers range from 1 to n, but array indices range from 0 to n-1
index=$(($number-1))

# Get the filename that matches the chosen number
chosenfile=${filearray[$index]}
echo "Your chosen file is: $chosenfile"

exit 0  # remove later

# cadreher76's original code below, not used. 

#list contents of directory using numbers
cd <path> && ls | grep ".*.<ext>" | nl

#user selects number associated with filename
read -p "Enter a number: " number

# copy file to our home directory
sudo cp -v <currentpath>/$number <newpath>/$number

# take ownership of moved file
sudo chown -v <user>:<user> <path>/$number

Does that help?
 
@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!
 
Last edited:
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.

Yup. This gets mentioned on my site a few times. Parsing 'ls' is a bad idea. So, to add to your comment...

This is the link I use for that:

 
@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!
Your skills continue to impress me...
 

Staff online

Members online


Top