I make an array of files as follows, and then "source" those files. I am also trying to make a script as close to POSIX possible so I don't have issues running it in different shells.
set -- path alias function
for file in "${#}"; do
[ -r "${SELF_PATH_DIR}.${file}" ] && [ -f "${SELF_PATH_DIR}.${file}" ] && . "${SELF_PATH_DIR}.${file}";
done
It works, but I don't like the fact that I have to specify ${SELF_PATH_DIR}. many times, so that the string takes the real path to the files (/Users/karlsd/dotfiles/.path, etc.).
Is there any way to make it simpler? For example, to add /Users/karlsd/dotfiles/. to each item before the loop?
Just assign "${SELF_PATH_DIR}.${file}" to a temporary variable or modify the same variable.
By the way: If the loop is the only spot where you are using set --/$# then you can iterate directly over the list:
for file in path alias function; do
file="${SELF_PATH_DIR}.${file}"
[ -r "$file" ] && [ -f "$file" ] && . "$file";
done
You actually have a potentially larger problem than a mere repetition of the variables: If one of the entries in $# happens to be the absolute path to some file, your code would break.
I would use realpath inside the loop:
absfile=$(realpath "$file")
[ -r "$absfile" ] && [ -f "$absfile" ] && . "$absfile"
Of course this would silently skip "buggy" entries of file, for instance if the name denotes a directory instead of a plain file or those you have no read access. Unless this is the desired behaviour, I would omit the tests and just source the file. If things aren't right, you get at least an explicit error message.
Related
From the following link, I tried to use the following solution to compare a group of source files (here fortran90 = *.f90).
To do this and see the source which are different, I have put into my ~/.bashrc :
function diffm { for file in "$1"/"$2"; do diff -qs "$file" "$3"/"${file##*/}"; done ;}
But unfortunately, if I am in the current directory for argument $1, i.e by execute :
$ diffm . '*.f90' ../../dir2
The result is : impossible to access to './*.f90'. However, the sources *.f90 exist but wildcards are not taken into account.
Surely a problem with double quotes on arguments of my function ($1, $2, $3)?
More generally, this function doesn't work well.
How can I modify this function in order to make it work in all cases, even being in the current directory "." for the first argument $1 or the third $3?
If i understood what you are trying to do, this should work
diffm () {
dir="$1"; shift
for file in "$#"; do
filename=$(basename "$file")
diff -qs "$file" "$dir/$filename"
done
}
Usage
diffm ../../dir2 ./*.f90
Filename generation does not occur within quotes. Hence, you pass the literal string *.f90 to the function, and this string is used there literally too. If you know for sure that there is exactly one f90 files in your directory, don't use quotes and write
diffm . *.f90 ../../dir2
Things get ugly if the file name has a space embedded (which, BTW, is one reason why I prefer Zsh over bash - you don't have to care about this in Zsh). To deal with this case, you could do a
myfile=*.f90
diffm . "$myfile" ../../dir2
But sooner or later, you will be bitten by the fact, that for whatever reason, you have more than one f90 file, and your strategy will break. Therefore, a better solution would be to use a loop, which also works for the border case of having only one file:
iterating=0
for myfile in *.f90
do
if ((iterating == 0))
then
((iterating+=1))
diffm . "$myfile" ../../dir2
elif [[ ! -f $myfile ]]
then
echo "No files matching $myfile in this directory"
else
echo "ERROR: More than one f90-file. Don't know which one to diff" 1>&2
fi
done
The elif part just cares for the case that you don't have any f90 files. In this case, the loop body is executed once, and myfile contains the wildcard pattern.
I have a folder with a ton of old photos with many duplicates. Sorting it by hand would take ages, so I wanted to use the opportunity to use bash.
Right now I have the code:
#!/bin/bash
directory="~/Desktop/Test/*"
for file in ${directory};
do
for filex in ${directory}:
do
if [ $( diff {$file} {$filex} ) == 0 ]
then
mv ${filex} ~/Desktop
break
fi
done
done
And getting the exit code:
diff: {~/Desktop/Test/*}: No such file or directory
diff: {~/Desktop/Test/*:}: No such file or directory
File_compare: line 8: [: ==: unary operator expected
I've tried modifying working code I've found online, but it always seems to spit out some error like this. I'm guessing it's a problem with the nested for loop?
Also, why does it seem there are different ways to call variables? I've seen examples that use ${file}, "$file", and "${file}".
You have the {} in the wrong places:
if [ $( diff {$file} {$filex} ) == 0 ]
They should be at:
if [ $( diff ${file} ${filex} ) == 0 ]
(though the braces are optional now), but you should allow for spaces in the file names:
if [ $( diff "${file}" "${filex}" ) == 0 ]
Now it simply doesn't work properly because when diff finds no differences, it generates no output (and you get errors because the == operator doesn't expect nothing on its left-side). You could sort of fix it by double quoting the value from $(…) (if [ "$( diff … )" == "" ]), but you should simply and directly test the exit status of diff:
if diff "${file}" "${filex}"
then : no difference
else : there is a difference
fi
and maybe for comparing images you should be using cmp (in silent mode) rather than diff:
if cmp -s "$file" "$filex"
then : no difference
else : there is a difference
fi
In addition to the problems Jonathan Leffler pointed out:
directory="~/Desktop/Test/*"
for file in ${directory};
~ and * won't get expanded inside double-quotes; the * will get expanded when you use the variable without quotes, but since the ~ won't, it's looking for files under an directory actually named "~" (not your home directory), it won't find any matches. Also, as Jonathan pointed out, using variables (like ${directory}) without double-quotes will run you into trouble with filenames that contain spaces or some other metacharacters. The better way to do this is to not put the wildcard in the variable, use it when you reference the variable, with the variable in double-quotes and the * outside them:
directory=~/"Desktop/Test"
for file in "${directory}"/*;
Oh, and another note: when using mv in a script it's a good idea to use mv -i to avoid accidentally overwriting another file with the same name.
And: use shellcheck.net to sanity-check your code and point out common mistakes.
If you are simply interested in knowing if two files differ, cmp is the best option. Its advantages are:
It works for text as well as binary files, unlike diff which is for text files only
It stops after finding the first difference, and hence it is very efficient
So, your code could be written as:
if ! cmp -s "$file" "$filex"; then
# files differ...
mv "$filex" ~/Desktop
# any other logic here
fi
Hope this helps. I didn't understand what you are trying to do with your loops and hence didn't write the full code.
You can use diff "$file" "$filex" &>/dev/null and get the last command result with $? :
#!/bin/bash
SEARCH_DIR="."
DEST_DIR="./result"
mkdir -p "$DEST_DIR"
directory="."
ls $directory | while read file;
do
ls $directory | while read filex;
do
if [ ! -d "$filex" ] && [ ! -d "$file" ] && [ "$filex" != "$file" ];
then
diff "$file" "$filex" &>/dev/null
if [ "$?" == 0 ];
then
echo "$filex is a duplicate. Copying to $DEST_DIR"
mv "$filex" "$DEST_DIR"
fi
fi
done
done
Note that you can also use fslint or fdupes utilities to find duplicates
Trying to create a while loop to remove 8 files from a specified test folder. I keep getting the error no such file or directory even though I am positive I am in the right folder because I can use the ls command to see the files... Anyways here is what I have
#!/bin/bash
var=(`ls ~/Random/Unit1/Test`)
x=${#var[#]}
i=0
while [ $i -lt $x ] ; do
rm $var # this line is incorrect and needs changing
((i++))
done
var is an array variable, right now you're accessing it as a scalar, which in bash returns the first value, so you remove the first file and then try to remove it again once for every file in the directory. If you want to remove every file you need to get the value of the array at every index, so in the loop you would get the nth value in the array, ie
rm ${var[$i]}
There are a couple of problems with your script, the first one is that you should cd to the folder from where you want to remove the files, you can use pushd and popd for it. Second, you should enclose the var variable with double quotes. Also, as stated in #redball's answer, you are accessing an array, you have to use array notation on it.
#!/bin/bash
DIRECTORY=~/Random/Unit1/Test
var=(`ls $DIRECTORY`)
x=${#var[#]}
i=0
# Saves current directory and change it to the one pointed by "$DIRECTORY"
push "$DIRECTORY"
while [ $i -lt $x ] ; do
rm "${var[$i]}"
((i++))
done
# Restores the previously saved directory
popd
I have a directory config with the following file listing:
$ ls config
file one
file two
file three
I want a bash script that will, when given no arguments, iterate over all those files; when given names of files as arguments, I want it to iterate over the named files.
#!/bin/sh
for file in ${#:-config/*}
do
echo "Processing '$file'"
done
As above, with no quotes around the list term in the for loop, it produces the expected output in the no-argument case, but breaks when you pass an argument (it splits the file names on spaces.) Quoting the list term (for file in "${#:-config/*}") works when I pass file names, but fails to expand the glob if I don't.
Is there a way to get both cases to work?
For a simpler solution, just modify your IFS variable
#!/bin/bash
IFS=''
for file in ${#:-config/*}
do
echo "Processing '$file'"
done
IFS=$' \n\t'
The $IFS is a default shell variable that lists all the separators used by the shell. If you remove the space from this list, the shell won't split on space anymore. You should set it back to its default value after you function so that it doesn't cause other functions to misbehave later in your script
NOTE: This seems to misbehave with dash (I used a debian, and #!/bin/sh links to dash). If you use an empty $IFS, args passed will be returned as only 1 file. However, if you put some random value (i.e. IFS=':'), the behaviour will be the one you wanted (except if there is a : in your files name)
This works fine with #!/bin/bash, though
Set the positional parameters explicitly if none are given; then the for loop is the same for both cases:
[ $# -eq 0 ] && set -- config/*
for file in "$#"; do
echo "Processing '$file'"
done
Put the processing code in a function, and then use different loops to call it:
if [ $# -eq 0 ]
then for file in config/*
do processing_func "$file"
done
else for file in "$#"
do processing_func "$file"
done
fi
What is the best way to look inside a directory and determine whether its content are directories or files. Its a homework question.
My homework requires me to write a script that counts the number of directories, files, how many are executable, writeable, and readable.
Assuming you're talking about the bourne shell family, take a look at the -d, -x, -w... and I'm guessing -r tests. Look up how a for loop works in bash to see how to iterate over the files... the general idea is
for var in directory/*; do
#stuff with $var
done
But there are some particulars relating to spaces in filenames that can make this trickier.
Use the -X style operators:
[ -d "${item}" ] && echo "${item} is a directory"
See the bash man page (search for "CONDITIONAL EXPRESSIONS") to see the complete list.
Looping through contents of a directory and counting looks like this:
dirs=0
writeable=0
for item in /path/to/directory/*; do
[ -d "${item}" ] && dirs=$(( dirs + 1 )) # works in bash
[ -w "${item}" ] && writeable=`expr ${writeable} + 1` # works in bourne shell
# Other tests
done
echo "Found ${dirs} sub-directories"
echo "Found ${writeable} writeable files"