Passing the wildcard * unevaluated to inner commands - bash

I'm writing a script that requires me to use an unevaluated *.
Here's what I've tried:
show() {
for file in $(find . -name $1); do
echo $file
done
}
If I run show *.pdf, the code will only look for PDFs in the current directory, and pass them into find . -name.
What I need is for the wildcard to be transferred to find unevaluated, so that it will look for PDFs recursively in the current folder.
My current solution involves doing this:
show() {
for file in $(find . -name "*$1"); do
echo $file
done
}
Which lets me write show .pdf, and that's fine, except nearly all bash commands that allow wildcards use the * operator. So for the sake of keeping it consistent with the rest of the system.
Is there a way for me to do this? I'm pulling my hairs out over this issue.
I've also tried show "*.pdf" but that yields the exact same result.
NOTE: It might look strange that I'm just echoing the result, when I could just have it run find . -name "*.pdf". That's true, but this is the first step in a larger script. I want to get this to work before continuing.

Related

How to use 'find' to print only matching directories?

I am trying to use find to recursively search the file names in a directory for a particular pattern (wp-config.*). When I do so using:
find `wp-config.*`
it seems to print all directories to the screen. How can I ensure that only the matching directories are printed to the screen?
Thanks.
From the answers in this outside post, I was able to use this command to do what I want:
find . -name 'wp-config.*' -printf "%h\n"
One of my main issues was that I originally did not understand that find does not print results to the screen. So it is necessary to pipe results to some kind of output.
Correct usage of find:
find -type d -name "wp-config.*"
The '-type d' will give you the directories.
The '-name "wp-config.*"' will give you the names requested.
Always use the man pages to search up commands. In this case:
man find
One last thing. The backticks ` are serving a totally different purpose. What you need here are regular quotes ".

output from while read loop using iterative filename/timestamp

I am attempting to create a script that will iteratively run a command against a variable (which is a fully qualified filename) and output the results of that command to an individually named/timestamped file (to %S accuracy). Im not great with this stuff at all
here is what I do:
find /vmfs/volumes/unlistedpathname/unlistedfoldername |
while read list;do
vmkfstools -D "$list" >> duringmigration_10mins_"$list".$(date +"%Y.%m.%d.%H.%M.%S");
done
the output im hoping for is something like
duringmigration_10mins_blahblahblah.vmx.2016.09.25.21.26.35
of course it doesnt work, and im not exactly sure how to solve it. I know the problem outright is $list as the filename variable will reprint the fullpath, so I need some sort of way to tell the loop "hey just use the filename as the variable NOT the full path" but im not sure how to do that in this case. Im also hoping to be able to run this from any location not specific path.
There are two problems preventing the behaviour you are looking for:
As you saw the filenames returned by find include the full path.
Your find command will return all the files and the directory name.
We solve #1 by calling basename on $list in the output filename.
We solve #2 by adding -type f to the find command to only return files and not directories.
find /vmfs/volumes/unlistedpathname/unlistedfoldername -type f |
while read list ; do
vmkfstools -D "${list}" >> "duringmigration_10mins_$(basename "${list}").$(date +"%Y.%m.%d.%H.%M.%S")"
done

Exactly why does result=$($shell_command) fail?

Why does assigning command output work in some cases and seemingly not in others? I created a minimal script to show what I mean, and I run it in a directory with one other file in it, a.txt. Please see the ??? in the script below and let me know what's wrong, perhaps try it. Thanks.
#!/bin/bash
## setup so anyone can copy/paste/run this script ("complete" part of MCVE)
tempdir=$(mktemp -d "${TMPDIR:-/tmp}"/demo.XXXX) || exit # make a temporary directory
trap 'rm -rf "$tempdir"' 0 # delete temporary directory on exit
cd "$tempdir" || exit # don't risk changing non-temporary directories
touch a.txt # create a sample file
cmd1="find . -name 'a*' -print"
eval $cmd1 # this produces "./a.txt" as expected
res1=$($cmd1)
echo "res1=$res1" # ??? THIS PRODUCES ONLY "res1=" , $res1 is blank ???
# let's try this as a comparison
cmd2="ls a*"
res2=$($cmd2)
echo "res2=$res2" # this produces "res2=a.txt"
Let's look at exactly what this does:
cmd1="find . -name 'a*' -print"
res1=$($cmd1)
echo "res1=$res1" # ??? THIS PRODUCES ONLY "res1=" , $res1 is blank ???
As per BashFAQ #50, execution of res1=$($cmd1) does the following, assuming you have no files with names starting with 'a and ending with ' (yes, with single quotes as part of the name), and that you haven't enabled the nullglob shell option:
res1=$( find . -name "'a*'" -print )
Note the quoting around than name? That quoting represents that the 's are treated as data, rather than syntax; thus, rather than having any effect on whether the * is expanded, they're simply an additional element required to be part any filename for it to match, which is why you get a result with no matches at all. Instead, as the FAQ tells you, use a function:
cmd1() {
find . -name 'a*' -print
}
res1=$(cmd1)
...or an array:
cmd1=( find . -name 'a*' -print )
res1=$( "${cmd1[#]}" )
Now, why does this happen? Read the FAQ for a full explanation. In short: Parameter expansion happens after syntactic quotes have already been applied. This is actually a Very Good Thing from a security perspective -- if all expansions recursively ran through full parsing, it would be impossible to write secure code in bash handling hostile data.
Now, if you don't care about security, and you also don't care about best practices, and you also don't care about being able to correctly interpret results with unusual filenames:
cmd1="find . -name 'a*' -print"
res1=$(eval "$cmd1") # Force parsing process to restart from beginning. DANGEROUS if cmd1
# is not static (ie. constructed with user input or filenames);
# prone to being used for shell injection attacks.
echo "res1=$res1"
...but don't do that. (One can get away with sloppy practices only until one can't, and the point when one can't can be unpleasant; for the sysadmin staff at one of my former jobs, that point came when a backup-maintenance script deleted several TB worth of billing data because a buffer overflow had placed random garbage in the name of a file that was due to be deleted). Read the FAQ, follow the practices it contains.

Issue saving result of "find" function in a shell script

I'm pretty new to shell scripting, but it's been great in helping me automating cumbersome tasks in OS X.
One of the functions I'm trying to write in a new script needs to find the specific filename in a subdirectory given a regex string. While I do know that the file exists, the version (and therefore filename itself) is being continually updated.
My function is currently as follows:
fname(){
$2=$(find ./Folder1 -name "$1*NEW*")
}
Which I'm then calling later in my script with the following line:
fname Type1 filename1
What I'm hoping to do is save the filename I'm looking for in variable filename1. My find syntax seems to be correct if I run it in Terminal, but I get the following error when I run my script:
./myscript.sh: line 13: filename1=./Folder1/Type1-list-NEW.bin: No such file or directory
I'm not sure why the result of find is not just saving to the variable I've selected. I'd appreciate any help (regardless of trivial this question may end up being). Thanks!
EDIT: I have a bunch of files in the subdirectory, but with the way I'm setting that folder up I know my "find" query will return exactly 1 filename. I just need the specific filename to do various tasks (updating, version checking, etc.)
The syntax for writing output to a file is command > filename. So it should be:
fname() {
find ./Folder1 -name "$1*NEW*" > "$2"
}
= is for assigning to a variable, not saving output in a file.
Are you sure you need to put the output in a file? Why not put it in a variable:
fname() {
find ./Folder1 -name "$1*NEW*"
}
var=$(fname Type1)
If you really want the function to take the variable name as a parameter, you have to use eval
fname() {
eval "$2='$(find ./Folder1 -name "$1*NEW*")'"
}
Okay, so I'm reading this as, you want to take the output of the find and save it in a shell variable given by $2.
You can't actually use a shell variable to dynamically declare the name of a new shell variable to rename, when the shell sees an expansion at the beginning of a line it immediately begins processing the words as arguments and not as an assignment.
There might be some way of pulling this off with declare and export but generally speaking you don't want to use a shell variable to hold n file names, particularly if you're on OS X, those file names probably have whitespaces and you're not protecting for that in the way your find is outputting.
Generally what you do in this case is you take the list of files find is spitting out and you act on them immediately, either with find -exec or as a part of a find . -print0 | xargs -0 pipeline.

Organized logging in Bash

I have this script that deletes files 7 days or older and then logs them to a folder. It logs and deletes everything correctly but when I open up the log file for viewing, its very sloppy.
log=$HOME/Deleted/$(date)
find $HOME/OldLogFiles/ -type f -mtime +7 -delete -print > "$log"
The log file is difficult to read
Example File Output: (when opened in notepad)
/home/u0146121/OldLogFiles/file1.txt/home/u0146121/OldLogFiles/file2.txt/home/u0146121/OldLogFiles/file3.txt
Is there anyway to log the file nicer and cleaner? Maybe with the Filename, date deleted, and how old it was?
try
log=$HOME/Deleted/$(date);
# change it to echo -e and insert new line if needed
find $HOME/OldLogFiles/ -type f -mtime +7 -exec echo {} \; > "$log"
Use logger for logging and leave the actual log handling to syslog and logrotate.
In you deletion script you can add 2 variables at the top, lets call them "timeDate" and "logDestination". They would look like this:
timeDate=$(date "+%d/%m/%Y %T")
logDestination=/home/$USER/.deleteLog; touch $logDestination
Now the $(date "+%d/%m/%Y %T") part simply gets the current date and time. It goes off to get the system date as; date (+%d) then the month and year (%m) (%Y) b.t.w the capital Y returns a full year as in YYYY. It then stores that date and time in the variable for later use.
The logDestination variable is holding a directory or file path for us, pointing at a file called .deleteLog . Now the touch part that follows isn't entirely necessary but it will make the file exist if it does not exist or has been deleted or renamed by accident.
In bash scripting and in a host of programming languages one makes methods or functions, which are just simply sections of code that usually do just one job. This function below is designed to write a message to a logfile:
## Logging function
function _logger () {
echo -e "$timeDate $user - $#\r" >> $log
}
A simply explanation of the function is it is able to receive a form of information. If you look at the above function, note the "$#" symbol this is a placeholder if you like, it will put any and all strings (text) you point at the function. Here is a more cut down version of a function that can receive several strings or input:
function _example () {
echo -e "$#"
}
To call this function and give it a message we can literally type (anywhere under the function in your script):
example "hello"
This string "hello" is given to the function and the echo line in the function outputs the "hello" to your terminal or screen. The -e in the echo line helps the echo to distinguish active parts, Tho best use "man echo" to give you a understanding of its behavior modification.
So back to your script.
Lets say you have a line that deletes the contents of a directory (I advise care and caution and discourage such line but meh).
after it has done a delete you can call;
_logger "deletion of file (or $FILE) successful."
and the _logger function will nicely place the date time, message and start a new line for you (\r). The $user in the function is placed by your current view. as in it will say your username.
Functions can be called numerous times, saving you code duplication and makes the scripts look neater.
You're asking a few things here...
Why does my log file look funny in notepad?
Probably because of the tool you're using to view it, but it's hard to say without more information. Notepad is a Windows application, and Windows uses a strategy than unix for ending lines in a text file. If you want to know for sure, use a tool like hexdump or od to see what's really going on inside your file. And as was suggested in comments, head filename.log on the unix host may show you what you expect, in which case you will have confirmed the line ending theory.
If Notepad is your tool of choice, then we can show you how to convert the files so that they're more compatible with Windows. Otherwise, let us know what you really need.
How do I include this extra data in my log?
You're asking for a few items:
filename - easy enough, you've already got that.
date deleted - easy enough, you just need a timestamp for each of the entries.
age of file - harder. Will the timestamp of the file be sufficient, or do you want, for example, the date extracted from the first line of the file itself? If the latter, you'll need to include a sample in your question. I'll go with the former for now.
First off, bash version 4 and up lets you use time formats as part of a printf, so you could do the following:
printf -v log '%s/Deleted/%(%F)T' "$HOME"
But honestly, I think this was a hack, and what you really want is a single text file that gets rotated using some other tool.
log=/var/log/whatever.log
The "some other tool" will depend on your operating system. In FreeBSD, look in /etc/newsyslog.conf. In Linux, refer to the documentation for your distribution, looking for things like "logrotate".
The output from your find command can be the files which were successfully deleted, but you don't have to have just filenames. Consider the following:
find "$HOME/OldLogFiles/" -type f -mtime +7 -ls -delete
The output of this is the entire ls line, before the file is deleted. It includes the file's date and inode number. Preface this with a timestamp, and you may be good to go:
find "$HOME/OldLogFiles/" -type f -mtime +7 -ls -delete | sed "s/^/[$(printf '%(%F %T)T')] /"
Or if you prefer to do this without sed:
while read line; do
printf '[%(%F %T)T] %s\n' -1 "$line"
done < <(find "$HOME/OldLogFiles/" -type f -mtime +7 -ls -delete)
In this version, the -1 is interpreted by printf's %T as "now".
Redirect to $log as you like.
Finally, and this is probably the "best" option, is to avoid managing logfiles in bash entirely, and use your system's logger command instead. The POSIX version is pretty bare-bones, but you can control things syslog style in BSD, Linux, etc.

Resources