This question already has answers here:
Determine if relative or absolute path in shell program
(4 answers)
Closed 6 years ago.
As the title says, I am trying to determine if my bash script receives a full path or a relative file to a directory as a parameter.
For some reasons the following doesn't seem to work for me:
#!/bin/bash
DIR=$1
if [ "$DIR" = /* ]
then
echo "absolute"
else
echo "relative"
fi
When I run my script with either a full path or absolute path it says:
./script.sh: line 5: [: too many arguments
relative
For some reasons I can't seem to figure this bug. Any ideas?
[ ... ] doesn't do pattern matching. /* is being expanded to the contents of /, so effectively you have
if [ "$DIR" = /bin /boot /dev /etc /home /lib /media ... /usr /var ]
or something similar. Use [[ ... ]] instead.
if [[ "$DIR" = /* ]]; then
For POSIX compliance, or if you just don't have a [[ that does pattern matching, use a case statement.
case $DIR in
/*) echo "absolute path" ;;
*) echo "something else" ;;
esac
Just test on the first character:
if [ "${DIR:0:1}" = "/" ]
One more case is paths started from ~ (tilde). ~user/some.file or ~/some.file are some kind of absolute paths.
if [[ "${dir:0:1}" == / || "${dir:0:2}" == ~[/a-z] ]]
then
echo "Absolute"
else
echo "Relative"
fi
ShellCheck automatically points out that "[ .. ] can't match globs. Use [[ .. ]] or grep."
In other words, use
if [[ "$DIR" = /* ]]
This is because [ is a regular command, so /* is expanded by the shell beforehand, turning it into
[ "$DIR" = /bin /dev /etc /home .. ]
[[ is handled specially by the shell, and doesn't have this problem.
Writing tests is fun:
#!/bin/bash
declare -a MY_ARRAY # declare an indexed array variable
MY_ARRAY[0]="/a/b"
MY_ARRAY[1]="a/b"
MY_ARRAY[2]="/a a/b"
MY_ARRAY[3]="a a/b"
MY_ARRAY[4]="/*"
# Note that
# 1) quotes around MY_PATH in the [[ ]] test are not needed
# 2) the expanded array expression "${MY_ARRAY[#]}" does need the quotes
# otherwise paths containing spaces will fall apart into separate elements.
# Nasty, nasty syntax.
echo "Test with == /* (correct, regular expression match according to the Pattern Matching section of the bash man page)"
for MY_PATH in "${MY_ARRAY[#]}"; do
# This works
if [[ $MY_PATH == /* ]]; then
echo "'$MY_PATH' is absolute"
else
echo "'$MY_PATH' is relative"
fi
done
echo "Test with == \"/*\" (wrong, becomes string comparison)"
for MY_PATH in "${MY_ARRAY[#]}"; do
# This does not work at all; comparison with the string "/*" occurs!
if [[ $MY_PATH == "/*" ]]; then
echo "'$MY_PATH' is absolute"
else
echo "'$MY_PATH' is relative"
fi
done
echo "Test with = /* (also correct, same as ==)"
for MY_PATH in "${MY_ARRAY[#]}"; do
if [[ $MY_PATH = /* ]]; then
echo "'$MY_PATH' is absolute"
else
echo "'$MY_PATH' is relative"
fi
done
echo "Test with =~ /.* (pattern matching according to the regex(7) page)"
# Again, do not quote the regex; '^/' would do too
for MY_PATH in "${MY_ARRAY[#]}"; do
if [[ $MY_PATH =~ ^/[:print:]* ]]; then
echo "'$MY_PATH' is absolute"
else
echo "'$MY_PATH' is relative"
fi
done
Related
What would be the best way to extract all the directory names from a path and compare it with directory name given as argument:
find_dir() {
path='/download/1/2/3/4/5/6'
for dir in logic ${path}
if [ dir = $1 ]; then
echo "Directory $1 found in $path"
exit(0)
else
pass
fi
echo "Directory $1 not found in $path"
}
Results:
pompt$ find_dir 4
Directory 4 found in /download/1/2/3/4/5/6
pompt$ find_dir 8
Directory 8 not found in /download/1/2/3/4/5/6
Just use regex match wrapping directory name within / (note the if [[ $dir =~ .*/$1/.* ]] ; then syntax, double brackets, space before semi-colon and =~ for regex test):
#!/bin/bash
find_dir() {
path='/download/1/2/3/4/5/6'
for dir in logic ${path}
do
if [[ "$dir" =~ .*/$1/.* ]] ; then
echo "Directory $1 found in $path"
exit 0
fi
done
echo "Directory $1 not found in $path"
}
find_dir 8
find_dir 4
result:
Directory 8 not found in /download/1/2/3/4/5/6
Directory 4 found in /download/1/2/3/4/5/6
note: I preserved the for loop but the logic argument doesn't make any sense...
You could do this:
#!/usr/bin/env bash
find_dir() {
path='/download/1/2/3/4/5/6'
lastdir=$(rev <<< "$path" | cut -f1 -d/)
n=1
while [[ $dir != $lastdir ]]; do
dir=$(cut -f$n -d/ <<< "$path")
((n++))
if [[ $dir == $1 ]]; then
echo "Directory $1 found in $path"
exit 0
fi
done
echo "Directory $1 not found in $path"
}
#!/bin/bash
# Code to generate script usage
if [[ "$#" -ne 1 ]] && [[ "$#" -ne 2 ]]; then
flag=1;
elif ! [[ "$1" == "abcd" || "$1" == "dcba" ]]; then
echo "Invalid"
flag=1;
fi
while [ $# -gt 1 ]
do
case $2 in
'streams')
;;
*)
echo "unrecognised optional arg $2"; flag=1;
;;
esac
shift
done
if [ "$flag" == "1" ]; then
echo "Usage:"
exit
fi
function main {
arg1=$1
streams=$2
if [ "${streams}" == "streams" ]; then
echo entering here
else
echo entering there
fi
}
parent_dir=`pwd`
find $parent_dir -name "*" -type d | while read d; do
cd $denter code here
main $1 $2
done
Why the code does not enter "entering here" when script run with arguments "abcd" and "streams" ?
I feel that function having two arguments is causing the problem, code was working fine with one argument
Several things you might want to fix in your code, before attempts are made to find the specific problem. It is possible that it will disappear after modifying your script accordingly. If the problem is still alive, I'll edit my answer with a solution. If you decide to apply the following changes, please update your code in the question.
Consistent usage of either [[ or [. [[ is a Bash keyword similar to (but more powerful than) the [ command.
See
Bash FAQ 31
Tests And Conditionals
Unless you're writing for POSIX sh, I recommend [[.
Use (( for arithmetic expressions. ((...)) is an arithmetic command, which returns an exit status of 0 if the expression is nonzero, or 1 if the expression is zero. Also used as a synonym for let, if assignments are needed. See Arithmetic Expression.
Use the variable PWD instead of pwd. PWD is a builtin variable in all POSIX shells that contains the current working directory. pwd(1) is a POSIX utility that prints the name of the current working directory to stdout. Unless you're writing for some non-POSIX system, there is no reason to waste time executing pwd(1) rather than just using PWD.
The function keyword is not portable. I suggest you to avoid using it and simply write function_name() { your code here; } # Usage
$parent_dir is not double-quoted. "Double quote" every literal that contains spaces/metacharacters and every expansion: "$var", "$(command "$var")", "${array[#]}", "a & b". See
Quotes
Arguments
ShellCheck your code before uploading.
Replace the while condition logic with an if condition, so that shift is no longer required. Shift was the devil I was facing I found.
#!/bin/bash
# Code to generate script usage
if [[ "$#" -ne 1 ]] && [[ "$#" -ne 2 ]]; then
flag=1;
elif ! [[ "$1" == "abcd" || "$1" == "dcba" ]]; then
echo "Invalid"
flag=1;
fi
#while [[ $# -gt 1 ]]
#do
# case $2 in
# 'streams')
# ;;
# *)
# echo "unrecognised optional arg $2"; flag=1;
# ;;
# esac
# shift
#done
if [[ $2 == "streams" ]]; then
:
elif [[ (-z $2) ]]; then
:
else
echo "unrecognised optional arg $2"; flag=1;
fi
if [[ "$flag" == "1" ]]; then
echo "Usage:"
exit
fi
function main {
streams=$2
if [[ "${streams}" == "streams" ]]; then
echo entering here
else
echo entering there
fi
}
parent_dir=`pwd`
find $parent_dir -name "*" -type d | while read d; do
cd $d
main $1 $2
done
I am writing a ksh function (that is placed in the .profile file) that will present a menu of subdirectories and permit the user to choose one into which to cd. Here is the code:
# Menu driven subdirectory descent.
d(){
# Only one command line argument accepted
[ "$1" = "--" ] && shift $# # Trap for "ls --" feature
wd=`pwd`; arg="${1:-$wd}"
dirs="`/bin/ls -AF $arg 2>/dev/null | grep /$ | tr -d \"/\"`"
# Set the names of the subdirectories to positional parameters
if [ "$dirs" ] ;then
set $dirs
if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd $arg/$1; return; fi # trap: it's obvious; do it
else echo "No subdirectories found" >&2; return 1
fi
# Format and display the menu
if [ `basename "${arg}X"` = "${arg}X" ] ;then arg="$wd/$arg"; fi # Force absolute path if relitive
echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
j=1; for i; do echo -e "$j\t$i"; j=`expr $j + 1`; done | pr -r -t -4 -e3
echo -e "\n\t\tEnter the number of your choice -- \c "
# Convert user-input to directory-name and cd to it
read choice; echo
dir=`eval "(echo $\{"$choice"\})"` # Magic here.
[ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd $arg/`eval echo "$dir"`
}
This function works reasonably well with the exception of directory names that contain space characters. If the directory name contains a space, the set command sets each space delimited element of the directory name (instead of the complete directory name) into a separate positional parameter; that is not useful here.
I have attempted to set the $IFS shell variable (which contains a space, tab, and newline by default) to a single newline character with:
IFS=`echo` # echo outputs a trailing newline character by default
Which appears to accomplish what is intended as verified with:
echo -e "$IFS\c" | hexdump -c
But despite my best efforts (over the course of several days work) I have failed to set the entire directory names that contain spaces as values for positional parameters.
What am I missing?
Suggestions are hereby solicited and most welcome.
ADVAthanksNCE
Bob
Short answer: You can't do that. Don't try. See the ParsingLs page for an understanding of why programmatic use of ls is inherently error-prone.
You can't get -F behavior without implementing it yourself in shell (which is indeed feasible), but the following is the correct way to put a list of subdirectories into the argument list:
set -- */
If you don't want to have a literal / on the end of each entry:
set -- */ # put list of subdirectories into "$#"
set -- "${#%/}" # strip trailing / off each
Even better, though: Use an array to avoid needing eval magic later.
dirs=( */ )
dirs=( "${dirs[#]%/}" )
printf '%s\n' "${dirs[$choice]}" # emit entry at position $choice
Let's tie this all together:
d() {
destdir=$(
FIGNORE= # ksh93 equivalent to bash shopt -s dotglob
while :; do
subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
(( ${#subdirs[#]} > 2 )) || break # . and .. are two entries
for idx in "${!subdirs[#]}"; do
printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
done
printf '\nSelect a subdirectory: ' >&2
read -r choice
if [[ $choice ]]; then
cd -- "${subdirs[$choice]}" || break
else
break
fi
done
printf '%s\n' "$PWD"
)
[[ $destdir ]] && cd -- "$destdir"
}
Although still not working, this version does pass shellcheck albeit with one exception:
3 # Menu driven subdirectory descent.
4 function d{
5 # Only one command line argument accepted
6 [ "$1" = "--" ] && shift $# # Trap for "ls --" feature
7 wd="$PWD"; arg="${1:-$wd}"
8 set -- "${#%/}" # Set the names of the subdirectories to positional parameters
9 if [ $# -eq 1 -a "$arg" = "$wd" ] ;then cd "$arg/$1" || exit 1; return; # trap: it's obvious; do it
10 else echo "No subdirectories found" >&2; return 1
11 fi
12 # Format and display the menu
13 if [[ $(basename "${arg}X") = "${arg}X" ]] ;then arg="$wd/${arg}"; fi # Force absolute path if relitive
14 echo -e "\n\t\tSubdirectories relative to ${arg}: \n"
15 j=1; for i; do echo -e "$j\t$i"; j=(expr $j + 1); done | pr -r -t -4 -e3
16 echo -e "\n\t\tEnter the number of your choice -- \c "
17 # Convert user-input to directory-name and cd to it
18 read -r choice; echo
19 dir=(eval "(echo $\{\"$choice\"\})") # Magic here.
20 [ "$choice" -a "$choice" -ge 1 -a "$choice" -le "$#" ] && cd "${arg}"/"$(eval echo "${dir}")" || exit 1
^SC2128 Expanding an array without an index only gives the first element.
21 }
Once I have incorporated your suggestions into the code, and made it functional, I'll post it here, and mark my question answered. Thank you for your kind assistance.
I've used the code you kindly wrote as a basis for the d function below. It pretty much does what I'd like, with a few little issues:
All subdirectory names that contain a SPACE character are surrounded by characters, but those that do not are not.
All subdirectory names that contain a SINGLE QUOTE character have that character escaped with a BACKSLASH character.
Given that 1 and 2 above cause no issues, they are acceptable, but not ideal.
After user input does the cd, the menu of subdirectory names is again looped through. This could be considered a feature, I suppose. I tried substituting a return for the brake commands in the sections of code following the cd commands, but was unsuccessful in overcoming the subsequent looped menu.
The inclusion of "." and ".." at the head of the menu of subdirectories is not ideal, and actually serves no good purpose.
------------------- Code Begins ------------------------------
d() {
if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
echo "$FUNCNAME: ksh only";return 1
fi
FIGNORE= # ksh93 equivalent to bash shopt -s dotglob
if [ ${#} -gt 0 ] ;then # Only one command line argument accepted
cd -- "$1" && return 0
fi
if [ `ls -AF1|grep /|wc -l` -eq 1 ] ;then # cd if only one subdirectory
cd -- `ls -AF1|grep /` && return 0
fi
destdir=$(
while :; do
subdirs=( ~(N)*/ ) # ksh93 equivalent to subdirs=( */ ) with shopt -s nullglob
(( ${#subdirs[#]} > 2 )) || break # . and .. are two entries
echo -e "\n\t\tSubdirectories below ${PWD}: \n" >&2
for idx in "${!subdirs[#]}"; do
printf '%d) %q\n' "$idx" "${subdirs[$idx]%/}" >&2
done
printf '\nSelect a subdirectory: ' >&2
read -r
if [[ $REPLY ]]; then
cd -- "${subdirs[$REPLY]}" || break # Continue to loop through subdirectories after cding
else
break
fi
done
printf '%s\n' "$PWD"
)
--------------------------- Code Ends ------------------------------------
So, overall I'm very pleased, and consider myself very fortunate to have received the knowledgeable assistance of such an accomplished Unix wizard. I can't thank you enough.
Is it possible to have two expressions in a bash if statement, using an 'or' operator, in which one of the expressions tests the exit status of grep?
For example, I want a script to process all directories in my PATH except for '.' and /home/$LOGNAME/bin. I can do it fine with two if statements, but I'd like to combine the two tests into a single statement joined by 'or'.
This is the working version (two separate if statements):
IFS=:
for VAR in $PATH ; do
if echo $VAR | grep /home/$LOGNAME/bin > /dev/null
then
echo SKIPPING YOUR OWN bin DIRECTORY \($VAR\)
elif [ "$VAR" = "." ]
then
echo SKIPPING CURRENT WORKING DIRECTORY \($VAR\)
else
echo processing $VAR
fi
done
.. which produces the following output:
SKIPPING CURRENT WORKING DIRECTORY (.)
SKIPPING YOUR OWN bin DIRECTORY (/home/bobo/bin)
processing /home/qa/utils
processing /usr/lib64/qt-3.3/bin
processing /usr/local/bin
processing /bin
processing /usr/bin
processing /usr/local/sbin
processing /usr/sbin
processing /sbin
Just a few of many failed attempts:
if [ ( echo $VAR | grep /home/$LOGNAME > /dev/null )] -o [ "$VAR" = "." ]
if [[ ( echo $VAR | grep /home/$LOGNAME > /dev/null ) ]] -o [[ "$VAR" = "." ]]
if ( echo $VAR | grep /home/$LOGNAME > /dev/null ) -o "$VAR" = "."
if ( echo $VAR | grep /home/$LOGNAME > /dev/null ) || "$VAR" = "."
The proper form for your if elif fi block can be like this. You also don't need to use an external binary command like grep.
IFS=:
for VAR in $PATH; do
if [[ $VAR == . ]]; then
echo "SKIPPING CURRENT WORKING DIRECTORY \($VAR\)"
elif [[ "$VAR" == "/home/$LOGNAME/bin"* ]]; then
echo "SKIPPING YOUR OWN bin DIRECTORY \($VAR\)"
else
echo "processing $VAR"
fi
done
If you want to do it with one line you could have:
IFS=:
for VAR in $PATH; do
if [[ $VAR != . && "$VAR" != "/home/$LOGNAME/bin"* ]]; then
echo "processing $VAR"
else
echo "SKIPPING CURRENT WORKING DIRECTORY OR YOUR OWN bin DIRECTORY \($VAR\)"
fi
done
Or
IFS=:
for VAR in $PATH; do
if [[ $VAR == . || "$VAR" == "/home/$LOGNAME/bin"* ]]; then
echo "SKIPPING CURRENT WORKING DIRECTORY OR YOUR OWN bin DIRECTORY \($VAR\)"
else
echo "processing $VAR"
fi
done
The else block is optional.
It is possible, but totally unnecessary. Just let if evaluate the exit status of grep directly:
if echo "$var" | grep -q "$HOME/bin" || test "$var" = . ; then
...
fi
or
if echo "$var" | grep -q "^$HOME"'/bin$\|^\.$'; then ...
Note that the semantics of these are slightly different, since the second requires that $HOME/bin be the entire line, but that is probably what you want. (The anchors should be in the first example, if that is the case.)
There are several ways to do this. Using your if construct, it could look something like this (although there are a couple other equally valid ways to do it):
if [[ "${VAR}" == "." ]] || grep -q /home/${LOGNAME}/bin <<< "${VAR}"
You might also consider something like this, which I think makes it a bit clearer what you're doing, and might even be less typing (it also dispenses with calling an external program, so will be slightly more efficient):
case "${VAR}" in
.|*/home/${LOGNAME}/bin*) .... ;;
*) .... ;;
esac
How does one test for the existence of files in a directory using bash?
if ... ; then
echo 'Found some!'
fi
To be clear, I don't want to test for the existence of a specific file. I would like to test if a specific directory contains any files.
I went with:
(
shopt -s dotglob nullglob
existing_files=( ./* )
if [[ ${#existing_files[#]} -gt 0 ]] ; then
some_command "${existing_files[#]}"
fi
)
Using the array avoids race conditions from reading the file list twice.
From the man page:
-f file
True if file exists and is a regular file.
So:
if [ -f someFileName ]; then echo 'Found some!'; fi
Edit: I see you already got the answer, but for completeness, you can use the info in Checking from shell script if a directory contains files - and lose the dotglob option if you want hidden files ignored.
I typically just use a cheap ls -A to see if there's a response.
Pseudo-maybe-correct-syntax-example-ahoy:
if [[ $(ls -A my_directory_path_variable ) ]] then....
edit, this will work:
myDir=(./*) if [ ${#myDir[#]} -gt 1 ]; then echo "there's something down here"; fi
You can use ls in an if statement thus:
if [[ "$(ls -a1 | egrep -v '^\.$|^\.\.$')" = "" ]] ; then echo empty ; fi
or, thanks to ikegami,
if [[ "$(ls -A)" = "" ]] ; then echo empty ; fi
or, even shorter:
if [[ -z "$(ls -A)" ]] ; then echo empty ; fi
These basically list all files in the current directory (including hidden ones) that are neither . nor ...
If that list is empty, then the directory is empty.
If you want to discount hidden files, you can simplify it to:
if [[ "$(ls)" = "" ]] ; then echo empty ; fi
A bash-only solution (no invoking external programs like ls or egrep) can be done as follows:
emp=Y; for i in *; do if [[ $i != "*" ]]; then emp=N; break; fi; done; echo $emp
It's not the prettiest code in the world, it simply sets emp to Y and then, for every real file, sets it to N and breaks from the for loop for efficiency. If there were zero files, it stays as Y.
Try this
if [ -f /tmp/foo.txt ]
then
echo the file exists
fi
ref: http://tldp.org/LDP/abs/html/fto.html
you may also want to check this out: http://tldp.org/LDP/abs/html/fto.html
How about this for whether directory is empty or not
$ find "/tmp" -type f -exec echo Found file {} \;
#!/bin/bash
if [ -e $1 ]; then
echo "File exists"
else
echo "Files does not exist"
fi
I don't have a good pure sh/bash solution, but it's easy to do in Perl:
#!/usr/bin/perl
use strict;
use warnings;
die "Usage: $0 dir\n" if scalar #ARGV != 1 or not -d $ARGV[0];
opendir my $DIR, $ARGV[0] or die "$ARGV[0]: $!\n";
my #files = readdir $DIR;
closedir $DIR;
if (scalar #files == 2) { # . and ..
exit 0;
}
else {
exit 1;
}
Call it something like emptydir and put it somewhere in your $PATH, then:
if emptydir dir ; then
echo "dir is empty"
else
echo "dir is not empty"
fi
It dies with an error message if you give it no arguments, two or more arguments, or an argument that isn't a directory; it's easy enough to change if you prefer different behavior.
# tested on Linux BASH
directory=$1
if test $(stat -c %h $directory) -gt 2;
then
echo "not empty"
else
echo "empty"
fi
For fun:
if ( shopt -s nullglob ; perl -e'exit !#ARGV' ./* ) ; then
echo 'Found some!'
fi
(Doesn't check for hidden files)