Recursive function based on 2 conditions - recursion

I want to write a recursive function which exits on 2 conditions.
Let's say I want to make a directory and ask the user for input. He may enter something like this:
Valid: /existing-dir1/existing-DIR2/non-existence-dir1/non-existence-dir2
Invalid: /existing-dir1/existing-FILE1/non-existence-dir1/non-existence-dir2
To loop through the filename, I have the function dirname() which take /foo/bar and returns /foo. I also have function exist() to check if a filename exist and isdir() to see if it is a file or directory.
Basically, I need to loop recursively from the end of the filename, ignore non-existence nodes, and check if any node is a file - which is invalid. The recursion ends when one of the 2 conditions happens, whichever comes first:
A file is found
dirname() returns /
I am not familiar with recursion, and 2 conditions is a bit too much for me. I am using POSIX script but code samples in C++ / Java / C# are all good.
Edit: I know I can do a mkdir -p and get its status code, but it will create the directory. Nontheless, I want to do that in recursion for the purpose of learning.

In JS, you might write the recursion like this:
const isValid = (path) =>
path === '/'
? 'valid'
: exist(path) && !isdir(path)
? 'invalid'
: isValid(dirname(path))
You might be able to skip the exist check depending upon how your isdir function works.
This should show the logic, but I'm not sure how to write this in your Posix script environment.

I solved it myself. Written in POSIX script, but it is quite easy to read and port to other languages:
RecursivelyCheckFilename ()
{
if [ -e "$1" ] # if exists
then
if [ -d "$1" ] # if is directory
then
if [ "$1" = "/" ]
then
return 0;
else
RecursivelyCheckFilename "$(dirname -- "$1")";
return $?; # returns the value returned by previous function
fi
else
return 1;
fi
else
RecursivelyCheckFilename "$(dirname -- "$1")";
return $?;
fi
}
There are still a few issues with filename ending with a trailing slash, which I must find a way to deal with. But the code above is how I want it to work.

Related

ZSH: prompt expansion return code greater than

The problem is (theoretically) simple. All I want is for my zsh prompt to print the return code if it is less than or equal to 128 and the corresponding signal when greater than 128. I cannot find any example of this being done and the zsh docs only specify how to do it
if the exit status of the last command was n
The only version I have got (somewhat) working is the following (which only works for SIGINT):
PROMPT='%130(?.[$(kill -l $?)].$?)> '
I have also tried using precmd but completely failed with that (it appears the return code is interfered with when zsh is executing the function but don't quote me on that).
The solution was indeed simple and just involved creating a different function (to which I passed the return code) rather than using precmd. Below is the final version of my zsh prompt, including the return code / signal behaviour:
code () {
if (( $1 > 128 )); then
echo "SIG$(kill -l $1)"
else
echo $1
fi
}
setopt promptsubst
PROMPT='%F{green}%n%f#%m %F{cyan}%~%f> '
RPROMPT='%(?..%F{red}[$(code $?)]%f'

Changing the global “path” from within a function?

My zshenv file has a bunch of lines like
if [[ -d "$HOME/bin" ]]; then
path=($HOME/bin $path)
fi
I thought I’d try to factor this pattern out into a function. I replaced it with
function prepend_to_path_if_exists() {
if [[ -d $1 ]]; then
path=($1 $path)
fi
}
prepend_to_path_if_exists("$HOME/bin")
but this gives the error
/Users/bdesham/.zshenv:8: missing end of string
where line 8 is the one where I’m calling prepend_to_path_if_exists. What exactly is causing this error, and how can I make this function work? I’m using zsh 5.0.5 on OS X 10.10.1.
You could call functions as with usual command executions like this (without ()):
prepend_to_path_if_exists "$HOME/bin"
It seems that zsh try to expand the glob prepend_to_path_if_exists(…) rather than to call the function.
TL;DR: Prepending emelemnts to $path would be accomplished by a little cryptic way:
(I'm not quite sure that the below form is preferable for anyone though.)
# `typeset -U` uniqify the elements of array.
# It could be good for $path.
typeset -U path
# prepending some paths unconditionally,
path[1,0]=(\
$HOME/bin \
$HOME/sbin \
)
# then filtering out unnecessary entries afterward.
path=(${^path}(-/N))
The $path[x,0]=… is prepending(splicing) element(s) to array taken from the below:
So that's the same as VAR[1,0]=(...) ? It doesn't really "look" very
much like prepend to me.
-- Greg Klanderman (http://www.zsh.org/mla/workers/2013/msg00031.html)
The ${^path}(-/N) expands the glob qualifires -/N on the each $path elements.
(Without ^ in the parameter expansion, the last elements of array will be evaluated, so it is mandatory in this case.)
The glob qualifires -/N means that "symbolic links and the files they point to"(-) the "directory"(/). And when it does not match anything do not raise errors (N).
In short, it would keep exsisting directories only for $path.

How to create options in KSH script

I am creating a KSH interface script that will call other scripts based on the users input. The other scripts are Encrypt and Decrypt. Each one of these scripts receive parameters. I have seen someone execute a script before using "-" + first letter of a script name before. How do I do this for my script? So for example if my script is called menu and the user typed in : menu -e *UserID Filename.txt* the script would run and the encrypt script would be executed along with the associated parameters. So far my script takes in the encrypt/decrypt script option as a parameter. Here is my script:
#!/bin/ksh
#I want this parameter to become an
action=$1
if [ $1 = "" ]
then
print_message "Parameters not satisfied"
exit 1
fi
#check for action commands
if [ $1 = "encrypt" ]
then
dest=$2
fileName=$3
./Escript $dest $fileName
elif [ $1 = "decrypt" ]
then
outputF=$2
encryptedF=$3
./Dscript $outputF $encryptedF
else
print "Parameters not satisfied. Please enter encrypt or decrypt plus-n arguments"
fi
Thanks for the help!
There isn't any kind of automatic way to turn a parameter into another script to run; what you're doing is pretty much how you would do it. Check the parameter, and based on the contents, run the two different scripts.
You can structure it somewhat more nicely using case, and you can pass the later parameters directly through to the other script using "$#", with a shift to strip off the first parameter. Something like:
[ $# -ge 1 ] || (echo "Not enough parameters"; exit 1)
command=$1
shift
case $command in
-e|--encrypt) ./escript "$#" ;;
-d|--decrypt) ./dscript "$#" ;;
*) echo "Unknown option $command"; exit 1 ;;
esac
This also demonstrates how you can implement both short and long options, by providing two different strings to match against in a single case statement (-e and --encrypt), in case that's what you were asking about. You can also use globs, like -e*) to allow any option starting with -e such as -e, -encrypt, -elephant, though this may not be what you're looking for.

How can I remove a path from $PATH in Zsh and add it to the beginning without duplication?

I have:
PATH=/bar:/foo
I want:
PATH=/foo:/bar
I don't want:
PATH=/foo:/bar:foo
So I'm thinking, given the default path is PATH=/bar, I can modify $path (which is $PATH as an associative array):
function prepend_to_path() {
unset $path[(r)$1]
path=($1 $path)
}
prepend_to_path /foo
But that complains with:
prepend_to_path:unset:1: not enough arguments
It's been so long that I don't even remember what (r) is for, but without it (unset $path[$1]) I get:
prepend_to_path:1: bad math expression: operand expected at `/home/nerd...'
What am I doing wrong?
You can replace the body of your function with:
path=($1 ${(#)path:#$1})
Related answer: https://stackoverflow.com/a/3435429/1107999
This also works (and is arguably easier to read when you go back to it after a couple of months):
prepend_to_path () {
path[1,0]=$1
typeset -U path
}
typeset -U will automatically deduplicate the array, keeping only the first occurrence of each element.
Since export is equivalent to typeset -gx, you could also export -U path to kill two birds with one stone.
Edit: typeset -U needs only to be applied to a particular array once, so one can do that somewhere in one's shell startup and remove the line from the function above.

Capturing and testing output command in ZSH

I have tried countless ways to get what I want, but nothing seems to work. I always end up with something like 2:not found.
I want to capture the output of a command, and then test if it equals "!", like so:
function test() {
local testv=$(command) 2>/dev/null
if [ $(#testv) == "!" ]; then
echo "Exclamation mark!"
else
echo "No exclamation mark."
fi
}
How should I rewrite the code above to avoid the error test:2: = not found?
This should work:
if [ $testv = '!' ]; then
There were several problems here:
$(...) runs a command and substitutes its output; you want to substitute a variable value, so use $var or ${var}.
I have no idea what the # was doing there. ${#var} will get the length of $var, but that's not what you want here.
The test command (which [ is a synonym for) doesn't understand ==, so use = (if you're a C programmer that'll look wrong, but this is shell not C).
I don't think this is a problem in a script, but for interactive input "!" doesn't do what you expect. I used '!' to make sure the exclamation mark couldn't be interpreted as a history option.
Alternately, you could use [[ ]] instead of [ ], since it understands == (and has somewhat cleaner syntax in general):
if [[ $testv == '!' ]]; then
BTW, I'm trusting from the tag that this script is running in zsh; if not, the syntax will be a bit different (basic shells don't have [[ ]], and anything other than zsh will do unwanted parsing on the value of $testv unless it's in double-quotes). If you're not sure (or want it to be portable), here's a version that should work in any posix-compliant shell:
if [ "$testv" = '!' ]; then
Try with this:
local testv=$(command 2>/dev/null)
since it's the output of the command you want to redirect.
(I have no idea what you mean by $(#testv) though.)

Resources