Sup mates,
Building a "find" command in bash
Currently have it working when a directory is passed but if it isn't passed, running "find" is suppose to default to "find ./"
What occurs with my code is that my recursion function (a func that adds all items in its file tree) works with a for loop that uses $#
recurse(){
oldIFS=$IFS
IFS=$'\n'
for f in $#
do
list="`echo -e "$list\n$PWD/${f}"`"
if [[ -d "${f}" ]]
then
cd "${f}"
recurse $(ls -1 ".")
cd ..
fi
done
IFS=$oldIFS
}
So is there a way to add a command line argument so i keep the code the same
or
How do i create a variable that holds $# so i can use that in the for loop above
and then i can just set that variable to "./" if i detect $# == 0
Use an array, to match the array in $#.
args=("$#")
if (( ${#args[#]} == 0 ))
then
args=(./)
fi
for f in "${args[#]}"
do
...
done
Related
I'm new in ShellScripting and have the following script that i created based on a simpler one, i want to pass it an argument with the path to count files. Cannot find my logical mistake to make it work right, the output is always "1"
#!/bin/bash
i=0
for file in $0/*
do
let i=$i+1
done
echo $i
To execute the code i use
sh scriptname.sh /path/to/folder/to/count/files
$0 is the name with which your script was invoked (roughly, subject to several exceptions that aren't pertinent here). The first argument is $1, and so it's $1 that you want to use in your glob expression.
#!/bin/bash
i=0
for file in "$1"/*; do
i=$(( i + 1 )) ## $(( )) is POSIX-compliant arithmetic syntax; let is deprecated.
done
echo "$i"
That said, you can get this number more directly:
#!/bin/bash
shopt -s nullglob # allow globs to expand to an empty list
files=( "$1"/* ) # put list of files into an array
echo "${#files[#]}" # count the number of items in the array
...or even:
#!/bin/sh
set -- "$1"/* # override $# with the list of files matching the glob
if [ -e "$1" ] || [ -L "$1" ]; then # if $1 exists, then it had matches
echo "$#" # ...so emit their number.
else
echo 0 # otherwise, our result is 0.
fi
If you want to count the number of files in a directory, you can run something like this:
ls /path/to/folder/to/count/files | wc -l
As an exercise I have set myself the task of recursively listing files using bash builtins. I particularly don't want to use ls or find and I would prefer not to use setopt extendedglob. The following appears to work but I cannot see how to extend it with /.* to list hidden files. Is there a simple workaround?
g() { for k in "$1"/*; do # loop through directory
[[ -f "$k" ]] && { echo "$k"; continue; }; # echo file path
[[ -d "$k" ]] && { [[ -L "$k" ]] && { echo "$k"; continue; }; # echo symlinks but don't follow
g "$k"; }; # start over with new directory
done; }; g "/Users/neville/Desktop" # original directory
Added later: sorry - I should have said: 'bash-3.2 on OS X'
Change
for k in "$1"/*; do
to
for k in "$1"/* "$1"/.[^.]* "$1"/..?*; do
The second glob matches all files whose names start with a dot followed by anything other than a dot, while the third matches all files whose names start with two dots followed by something. Between the two of them, they will match all hidden files other than the entries . and ...
Unfortunately, unless the shell option nullglob is set, those (like the first glob) could remain as-is if there are no files whose names match (extremely likely in the case of the third one) so it is necessary to verify that the name is actually a file.
An alternative would be to use the much simpler glob "$1"/.*, which will always match the . and .. directory entries, and will consequently always be substituted. In that case, it's necessary to remove the two entries from the list:
for k in "$1"/* "$1"/.*; do
if ! [[ $k =~ /\.\.?$ ]]; then
# ...
fi
done
(It is still possible for "$1"/* to remain in the list, though. So that doesn't help as much as it might.)
Set the GLOBIGNORE file to exclude . and .., which implicitly turns on "shopt -u dotglob". Then your original code works with no other changes.
user#host [/home/user/dir]
$ touch file
user#host [/home/user/dir]
$ touch .dotfile
user#host [/home/user/dir]
$ echo *
file
user#host [/home/user/dir]
$ GLOBIGNORE=".:.."
user#host [/home/user/dir]
$ echo *
.dotfile file
Note that this is bash-specific. In particular, it does not work in ksh.
You can specify multiple arguments to for:
for k in "$1"/* "$1"/.*; do
But if you do search for .* in directories , you should be aware that it also gives you the . and .. files. You may also be given a nonexistent file if the "$1"/* glob matches, so I would check that too.
With that in mind, this is how I would correct the loop:
g() {
local k subdir
for k in "$1"/* "$1"/.*; do # loop through directory
[[ -e "$k" ]] || continue # Skip missing files (unmatched globs)
subdir=${k##*/}
[[ "$subdir" = . ]] || [[ "$subdir" = .. ]] && continue # Skip the pseudo-directories "." and ".."
if [[ -f "$k" ]] || [[ -L "$k" ]]; then
printf %s\\n "$k" # Echo the paths of files and symlinks
elif [[ -d "$k" ]]; then
g "$k" # start over with new directory
fi
done
}
g ~neville/Desktop
Here the funky-looking ${k##*/} is just a fast way to take the basename of the file, while local was put in so that the variables don't modify any existing variables in the shell.
One more thing I've changed is echo "$k" to printf %s\\n "$k", because echo is irredeemably flawed in its argument handling and should be avoided for the purpose of echoing an unknown variable. (See Rich's sh tricks for an explanation of how; it boils down to -n and -e throwing a spanner in the works.)
By the way, this will NOT print sockets or fifos - is that intentional?
I am looping through a folder and depending on the length of files do certain condition. I seem not to come right with that. I evaluate and output the length of a string in the terminal.
echo $file|wc -c gives me the answer of all files in the terminal.
But incorporating this into a loop is impossible
for file in `*.zip`; do
if [[ echo $file|wc -c ==9]]; then
some commands
where I want to operate on files that have a length of nine characters
Try this one:
for file in *.zip ; do
wcout=$(wc -c "$file")
if [[ ${wcout%% *} -eq 9 ]] ; then
# some commands
fi
done
The %% operator in variable expansion deletes everything that match the pattern after it. This is glob pattern, not regular expression.
Opposite to natural good sense of typical programmers the == operator in BASH compares strings, not numbers.
Alternatively (following the comment) you can:
for file in *.zip ; do
wcout=$(wc -c < "$file")
if [[ ${wcout} -eq 9 ]] ; then
# some commands
fi
done
Additional observation is that if BASH cannot expand *.zip as there is no ZIP files in the current directory it will pass "*.zip" into $file and let single iteration of the loop. That leads to the error reported by wc command. So it would be recommended to add:
if [[ -e ${file} ]] ; then ...
as a prevention mechanism.
Comments leads to another form of this solution (plus I added my safety mechanism):
for file in *.zip ; do
if [[ -e "$file" && (( $(wc -c < "$file") == 9 )) ]] ; then
# some commands
fi
done
using filter outside the loop
ls -1 *.zip \
| grep -E '^.{9}$' \
| while read FileName
do
# Your action
done
using filter inside loop
ls -1 *.zip \
| while read FileName
do
if [ ${#FileName} -eq 9 ]
then
# Your action
fi
done
alternative to ls -1 that is always a bit dangereous, find . -name '*.zip' -print [ but you neet to add 2 char length or filter the name form headin ./ and maybe limit to current folder depth ]
Suppose I have defined an array, like this:
DIR=(A B Supercalifragilistic)
and I need to invoke the script as
./script A B Supercalifragilistic
where the arguments are processed by internal functions func1() and func2(). Is there a way to make an alias (or anything, however it's called) S for Supercalifragilistic so that when I invoke:
./script A B S
the internal functions will process/interpret S as Supercalifragilistic?
Thank you in advance.
[edit]
I should add that the script is invoked via terminal, not inside a script, and the arguments A B Supercalifragilistic, or (hopefully) S, are passed on to the script in the terminal. I'm sorry for the confusion.
[edit2]
The script is here: Bash script: if any argument is "N" then function has extra options , in the answer below. What it does is explained in the OP there, below the script. Finally, instead of DIR=(A B C D E F) it's DIR=(A B Clarification D E F) (it's just an example) and the folder Clarification is the only one in a different path than the rest. I hope it's more clear now, if not, please tell me.
[final edit, I hope]
I think I can shout "Evrika!". Your word "hardcoded" made me realize I have to modify the script anytime a new folder gets added/deleted, so I thought of making the array dynamic, as in
./script a b "d e" g results in array=(a b "d e" g)
but also that it should replace the long paths with some short ones (Clarification >> C), so I made this test script based on also the answers here:
#!/bin/bash
array=()
for i in "$#"
do
if [[ "$i" == C ]]
then
array+=("Clarification")
else
array+=("$i")
fi
done
echo ${array[*]}
echo
for i in $(seq 0 $(( $# - 1 )))
do
echo ${array["$i"]}
done
and this is what it shows at command prompt:
$ ./x.sh abc C "d f" e
abc Clarification d f e
abc
Clarification
d f
e
I think now I can finally make the script to do what I want. Thank you, all, for the answers.
I really have no idea what you exactly want to achieve! But I had a look at the script you linked in your last edit. Since you have a hard-coded array you might as well instead use an associative array:
declare -A dir_h
dir_h["A"]=A
dir_h["B"]=B
dir_h["C"]=../path/Clarification
dir_h["D"]=D
dir_h["E"]=E
to loop on the keys of dir_h, i.e., on A B C D E:
for k in "${!dir_h[#]}"; do
echo "$k => ${dir_h[$k]}"
done
Try it, this might help you with your "alias" problem (or not).
Here's your script from your other post, using this technique and in a more consistent and readable form (note: I haven't tried it, there might be some minor typos, let me know if it's the case):
#!/bin/bash
# ./test.sh = 1. searches for existing archives
# 1.a. if they exist, it backups them into BKP/.
# 1.b. if not, displays a message
# 2. archives all the directories in the array list
# ./test.sh N = 1. deletes all the folder's archives existent and
# specified in the array list
# 2. archives all the directories in the array list
# ./test.sh {A..F} = 1. searches for existing archives from arguments
# 1.a. if they exist, it backups them into BKP/.
# 1.b. if not, displays a message
# 2. archives all the directories passed as arguments
# ./test.sh {A..F} N = 1. deletes all the archives matching $argument.zip
# 2. archives all the directories passed as arguments
# The directories to be backed-up/archived, all in the current (script's) path
# except "C", on a different path
declare -A dir_h
dir_h["A"]=A
dir_h["B"]=B
dir_h["C"]=../path/Clarification
dir_h["D"]=D
dir_h["E"]=E
dir_h["F"]=F
declare -A nope_h
nope_h["A"]=bogus
nope_h["B"]=bogus
nope_h["C"]=nope
nope_h["D"]=bogus
nope_h["E"]=bogus
nope_h["F"]=bogus
die() {
(($#)) && printf >&2 "%s\n" "$#"
exit 1
}
bak() {
if [[ "$1" != N ]]; then
# Check that arg is in dir list:
[[ -n ${dir_h["$1"]} ]] || die "Error in bak: argument \`$1' not handled"
if [[ -f $1.zip ]]; then
mv -vi "$1.zip" "BKP/$1.zip_$(date +"%H-%M")" || die
else
echo "$(tput setaf 1) no $1.zip$(tput sgr0)"
fi
fi
}
# The archive function, if any argument is "N", processing it is omitted. Folder
# "C" has special treatment
archive() {
if [[ $1 != N ]]; then
7z a -mx=9 "$1.zip" "${dir_h["$1"]}" -r -x\!"$1/${nope_h["$1"]}" || die
fi
}
# Let's check once for all whether N is in the arg list
foundN=0
for a in "$#"; do [[ $a = N ]] && foundN=1 && break; done
if (($#==0)); then
# case #1: no arguments
for d in "${!dir_h[#]}"; do
echo "$(tput setaf 2) backup$(tput sgr0)"
bak "$d"
archive "$d"
done
elif (($#==1)) && ((foundN)); then
# case #2: one argument, "N"
for d in "${!dir_h[#]}"; do
echo "$(tput setaf 1) no backup needed, removing$(tput sgr0)"
rm -v "$d".zip || die
archive "$d"
done
elif (($#>1)) && ((foundN)); then
# case #3: folders as arguments with "N"
for f in "$#"; do
if [[ $f != N ]]; then
echo "$(tput setaf 1) no backup needed, removing$(tput sgr0)"
rm -v "$f.zip" || die
fi
archive "$f"
done
else
for f in "$#"; do
echo "$(tput setaf 2) backup$(tput sgr0)"
bak "$f"
archive "$f"
done
fi
From this you can do a lot, and have pretty much infinite "alias" handling possibilities.
No need to use an alias. You could try something like :
$ cat test.sh
#!/bin/bash
declare -a args
for arg in "$#"; do
[[ $arg = "S" ]] && arg="Supercalifragilistic"
args+=( "$arg" )
done
for arg in "${args[#]}"; do
echo "$arg"
done
$ ./test.sh a b S e
a
b
Supercalifragilistic
e
You don't need alias here. Just set variable S to your string:
S=Supercalifragilistic
and then use:
./script A B "$S"
OR else call your script directly using array:
./script ${DIR[#]}
PS: It is not a good habit to use all caps variable names in shell and you can accidentally overwrite PATH variable some day.
You can do this:
processed_directories=()
for dir in "${directories[#]}"
do
if [ "$dir" = 'S' ]
then
dir='Supercalifragilistic'
fi
processed_directories+=("$dir")
done
It'll replace the value "S" with "Supercalifragilistic" anywhere in the array.
I want to check whether a directory has files or not in bash.
My code is here.
for d in {,/usr/local}/etc/bash_completion.d ~/.bash/completion.d
do
[ -d "$d" ] && [ -n "${d}/*" ] &&
for f in $d/*; do
[ -f "$f" ] && echo "$f" && . "$f"
done
done
The problem is that "~/.bash/completion.d" has no file.
So, $d/* is regarded as simple string "~/.bash/completion.d/*", not empty string which is result of filename expansion.
As a result of that code, bash tries to run
. "~/.bash/completion.d/*"
and of course, it generates error message.
Can anybody help me?
If you set the nullglob bash option, through
shopt -s nullglob
then globbing will drop patterns that don't match any file.
# NOTE: using only bash builtins
# Assuming $d contains directory path
shopt -s nullglob
# Assign matching files to array
files=( "$d"/* )
if [ ${#files[#]} -eq 0 ]; then
echo 'No files found.'
else
# Whatever
fi
Assignment to an array has other benefits, including desirable (correct!) handling of filenames/paths containing white-space, and simple iteration without using a sub-shell, as the following code does:
find "$d" -type f |
while read; do
# Process $REPLY
done
Instead, you can use:
for file in "${files[#]}"; do
# Process $file
done
with the benefit that the loop is run by the main shell, meaning that side-effects (such as variable assignment, say) made within the loop are visible for the remainder of script. Of course, it's also way faster, if performance is an issue.
Finally, an array can also be inserted in command line arguments (without splitting arguments containing white-space):
$ md5sum fileA "${files[#]}" fileZ
You should always attempt to correctly handle files/paths containing white-space, because one day, they will happen!
You could use find directly in the following way:
for f in $(find {,/usr/local}/etc/bash_completion.d ~/.bash/completion.d -maxdepth 1 -type f);
do echo $f; . $f;
done
But find will print a warning if some of the directory isn't found, you can either put a 2> /dev/null or put the find call after testing if the directories exist (like in your code).
find() {
for files in "$1"/*;do
if [ -d "$files" ];then
numfile=$(ls $files|wc -l)
if [ "$numfile" -eq 0 ];then
echo "dir: $files has no files"
continue
fi
recurse "$files"
elif [ -f "$files" ];then
echo "file: $files";
:
fi
done
}
find /path
Another approach
# prelim stuff to set up d
files=`/bin/ls $d`
if [ ${#files} -eq 0 ]
then
echo "No files were found"
else
# do processing
fi