I'm trying to write a recursive function that given a process number, prints out the process numbers of subprocesses spawned from it for up to k generations. The following code does not seem to work, as what I want to do is print it in a sort of tree-like way, where I print the subprocesses of a given process right under it, as opposed to printing the whole generation first, then the second, then so on. What I am trying is more on the spirit of depth first search run through the "graph". Somehow I am not managing to keep track of the levels of recursion to stop at a certain depth. If anyone can spot the mistakes, please let me know.
function descendantsRecursive() {
level=$2
if [[ "$level" -ge 4 ]]
then
exit
fi
result=($(pgrep -P "$1" .))
for pid in "${result[#]}"; do
echo "$level"
echo "$pid"
if [[ "$(pgrep -P $pid)" == '' ]]
then
:
else
descendantsRecursive $pid $(( $level+1 ))
fi
done
}
The obvious problem is that your variables are global. That doesn't matter for all of them but the fact that level is modified by the call will create a bug.
Furthermore, calling exit kills the entire script, and you just want to stop the called function. Use return
A good habit is to declare all variables as local:
local level=$2 pid result
But you could also use fewer variables:
function descendantsRecursive() {
local pid
if (( $2 >= 4 )); then return; fi
for pid in $(pgrep -P "$1" .) ; do
echo "$2"
echo "$pid"
# This test is unneeded
if [[ "$(pgrep -P $pid)" == '' ]]
then
:
else
descendantsRecursive $pid $(( $2+1 ))
fi
done
}
Related
I'm a Bash newbie, and I'm puzzling through a Bash script where I see this:
while [ $# -ge 2 ]
if [ "x$1" = "x-a" ]; then
echo "STR_A = $2" >>tmpFile.txt
...do more stuff...
elif [ "x$1" = "x-b" ]; then
echo "STR_B = $2" >>tmpFile.txt
...do more stuff...
else
usage
done
The script takes in four (or five?) command line arguments, and that usage function is:
usage() {
echo "usage: myscript -a STR_A | -b STR_B" 1>&2
exit 1
}
So suppose I ran the script like this:
me#ubuntu1:~$./myscript -A apple -B banana
I'm guessing that this code processes the script's command line arguments. I think that the outer while() loop steps through the command line arguments after argument 1, which would be myscript. The inner if() statements check to see an -a or -b flag is used to supply arguments, and then records the text string that follows in tmpFile.txt. Anything outside of those parameters is rejected and the script exits.
A lot of these assumptions rest on the bet that the outer while() loop...
while [ $# -ge 2 ]
...means "parse the BASH argv[] after the first argument", (to put this in C terms.) If that's not a correct assumption, then I have no idea what's going on here. Any feedback is appreciated, thank you!
Some code explanation.
while [ $# -ge 2 ]
There is a missing do for the loop.
This should loop forever if there are two or more arguments, unless shift is used. If there are less than two arguments, the loop does not even start.
if [ "x$1" = "x-a" ]; then
In distant past, it was common to prevent empty strings by adding an extra letter. Nowadays you would if [ "$1" = "-a" ]; then.
else
usage
Note that the usage is called from within the loop. So, if I would call the script as myscript -a, I would not get a usage message. On the other hand, if I would myscript bla bla, I would get an endless stream of error messages, which is probably not what you want.
I would seriously edit the script; determine whether the while is indeed a loop-forever or whether it is used instead of an if, and try the getops for argument parsing.
This doesn't help understand the code you're reading, but I do option parsing like this:
# inititialize vars, not strictly required in this case
a=''
b=''
# process options
while getopts "ha:b:" opt; do
case $opt in
a) a=$OPTARG ;;
b) b=$OPTARG ;;
*) usage ;;
esac
done
# shift after the processing
shift $((OPTIND - 1))
# look for error conditions
if [[ -n $a && -n $b ]]; then
echo "only one of -a or -b should be given" >&2
exit 1
fi
getopts is a bash builtin
I was doing this little script in which the first argument must be a path to an existing directory and the second any other thing.
Each object in the path indicated in the first argument must be renamed so that the new
name is the original that was added as a prefix to the character string passed as the second argument. Example, for the string "hello", the object OBJECT1 is renamed hello.OBJECT1 and so on
Additionally, if an object with the new name is already present, a message is shown by a standard error output and the operation is not carried out continuing with the next object.
I have the following done:
#! /bin/bash
if [ "$#" != 2 ]; then
exit 1
else
echo "$2"
if [ -d "$1" ]; then
echo "directory"
for i in $(ls "$1")
do
for j in $(ls "$1")
do
echo "$i"
if [ "$j" = "$2"."$i" ]; then
exit 1
else
mv -v "$i" "$2"."$i"
echo "$2"."$i"
fi
done
done
else
echo "no"
fi
fi
I am having problems if I run the script from another file other than the one I want to do it, for example if I am in /home/pp and I want the changes to be made in /home/pp/rr, since that is the only way It does in the current.
I tried to change the ls to catch the whole route with
ls -R | sed "s;^;pwd;" but the route catches me badly.
Using find you can't because it puts me in front of the path and doesn't leave the file
Then another question, to verify that that object that is going to create new is not inside, when doing it with two for I get bash errors for all files and not just for coincidences
I'm starting with this scripting, so it has to be a very simple solution thing
An obvious answer to your question would be to put a cd "$2 in the script to make it work. However, there are some opportunities in this script for improvement.
#! /bin/bash
if [ "$#" != 2 ]; then
You might put an error message here, for example, echo "Usage: $0 dir prefix" or even a more elaborate help text.
exit 1
else
echo $2
Please quote, as in echo "$2".
if [ -d $1 ]; then
Here, the quotes are important. Suppose that your directory name has a space in it; then this if would fail with bash: [: a: binary operator expected. So, put quotes around the $1: if [ -d "$1" ]; then
echo "directory"
This is where you could insert the cd "$1".
for i in $(ls $1)
do
It is almost always a bad idea to parse the output of ls. Once again, this for-loop will fail if a file name has a space in it. A possible improvement would be for i in "$1"/* ; do.
for j in $(ls $1)
do
echo $i
if [ $j = $2.$i ]; then
exit 1
else
The logic of this section seems to be: if a file with the prefix exists, then exit instead of overwriting. It is always a good idea to tell why the script fails; an echo before the exit 1 will be helpful.
The question is why you use the second loop? a simple if [ -f "$2.$i ] ; then would do the same, but without the second loop. And it will therefore be faster.
mv -v $i $2.$i
echo $2.$i
Once again: use quotes!
fi
done
done
else
echo "no"
fi
fi
So, with all the remarks, you should be able to improve your script. As tripleee said in his comment, running shellcheck would have provided you with most of the comment above. But he also mentioned basename, which would be useful here.
With all that, this is how I would do it. Some changes you will probably only appreciate in a few months time when you need some changes to the script and try to remember what the logic was that you had in the past.
#!/bin/bash
if [ "$#" != 2 ]; then
echo "Usage: $0 directory prefix" >&2
echo "Put a prefix to all the files in a directory." >&2
exit 1
else
directory="$1"
prefix="$2"
if [ -d "$directory" ]; then
for f in "$directory"/* ; do
base=$(basename "$f")
if [ -f "Sdirectory/$prefix.$base" ] ; then
echo "This would overwrite $prefix.$base; exiting" >&2
exit 1
else
mv -v "$directory/$base" "$directory/$prefix.$base"
fi
done
else
echo "$directory is not a directory" >&2
fi
fi
So, I have a script for moving files from one directory to another. I want to check whether the moving was successful or not, so I use this block of code:
...
mv "${!i}" "$dirname" 2>/dev/null
echo $? # Check MV exit code
if (( $?==1 ))
then
...
The problem is that whether moving was successful or not, then does not work. If I do this instead
if (( $?==0 ))
it instead works in any case. I have read that it may be because $? is treated like a string, and strings have 0 value. However, if I change it to this
if (( $?=="1" ))
it does not work either. I have tried using [[ ... ]] and [ ... ] instead of (( ... )), and -eq instead of ==, removing and adding spaces, adding and removing quotes, but nothing worked.
What am I doing wrong? Maybe there is another way of responding to certain exit code?
The problem is here:
echo $?
if (( $?==1 ))
The first echo $? will echo the return value of your mv command; however, in the if statement, using $? again will give you the return value of the echo command! $? is always the return value of the last command. The last command is the echo and it is always succeeding so you are always getting a 0 return value in that if statement.
What you should do instead is save the value into a variable and then compare things to that variable:
mv "${!i}" "$dirname" 2>/dev/null
ret_val=$?
echo ${ret_val}
if (( ${ret_val}==1 ))
You can check the exit status of your command directly:
if mv "${!i}" "$dirname" 2>/dev/null; then
# Code for successful move
else
# Code for unsuccessful move
fi
Or, to keep the happy path less indented:
if ! mv "${!i}" "$dirname" 2>/dev/null; then
# Code for unsuccessful move
return 1 # Or maybe exit 1 if in a script, not a function
fi
# Code for successful move
As for how the exit status of echo messes up your code, Tyler's answer has that covered.
I have a bash script to control Linux perf. As you may know, perf takes core list which can be specified in 1 of the three ways.
-C1 #core 1 only
-C1-4 # core 1 through 4
-C1,3 # core 1 and 3
Currently, I have an environment variable CORENO which will control -C$CORENO.
However, I need to offset CORENO by a fix offset (I.e.2)
I could do ((CORENO+=2)) but that only work for case 1.
Is there a Linux/bash trick to allow me to apply fix offset to every number in a bash variable?
Since you're on Linux, here's some GNU sed:
addtwo() {
sed -re 's/[^0-9,-]//g; s/[0-9]+/$((\0+2))/g; s/^/echo /e;' <<< "$1"
}
addtwo "1"
addtwo "1-4"
addtwo "3,4,5"
It will output:
3
3-6
5,6,7
It works by replacing all numbers with $((number+2)) and evaluating the result as a shell command. A whitelisting of allowed characters is applied first to avoid any security issues.
Take a look at seq
for core in `seq 2 10`; do
echo CORENO=$core
done
I’ve upvoted the sed-based answer from #that other guy because I like it more than mine, which is a “pure bash” solution, consisting of a recursive function.
function increment () {
local current="$1" n=$(($2))
if [[ "$current" =~ ^[0-9]+$ ]]; then
echo $((current+n))
elif [[ $current == *,* ]]; then
echo $(increment ${current%%,*} $n),$(increment ${current#*,} $n)
elif [[ $current == *-*-* ]]; then
echo ERROR
elif [[ $current == *-* ]]; then
echo $(increment ${current%-*} $n)-$(increment ${current#*-} $n)
else
echo ERROR
fi
}
CORENO=3-5
CORENO=$(increment $CORENO 2)
echo $CORENO
increment 3-5,6-8 3
My function will print ERROR when given an illegal argument. The one from #that other guy is much more liberal...
Problem
In some bash scripts, I don't want to set -e. So I write variable declarations like
var=$(false) || { echo 'var failed!' 1>&2 ; exit 1 ; }
which will print var failed! .
But using declare, the || is never taken.
declare var=$(false) || { echo 'var failed!' 1>&2 ; exit 1 ; }
That will not print var failed!.
Imperfect Solution
So I've come to using
declare var=$(false)
[ -z "${var}" ] || { echo 'var failed!' 1>&2 ; exit 1 ; }
Does anyone know how to turn the Imperfect Solution two lines into a neat one line ?
In other words, is there a bash idiom to make the declare var failure neater?
More Thoughts
This seems like an unfortunate mistake in the design of bash declare.
Firstly, the issue of two lines vs. one line can be solved with a little thing called Mr. semicolon (also note the && vs. ||; pretty sure you meant the former):
declare var=$(false); [ -z "${var}" ] && { echo 'var failed!' 1>&2 ; exit 1 ; }
But I think you're looking for a better way of detecting the error. The problem is that declare always returns an error code based on whether it succeeded in parsing its options and carrying out the assignment. The error you're trying to detect is inside a command substitution, so it's outside the scope of declare's return code design. Thus, I don't think there's any possible solution for your problem using declare with a command substitution on the RHS. (Actually there are messy things you could do like redirecting error infomation to a flat file from inside the command substitution and reading it back in from your main code, but just no.)
Instead, I'd suggest declaring all your variables in advance of assigning them from command substitutions. In the initial declaration you can assign a default value, if you want. This is how I normally do this kind of thing:
declare -i rc=-1;
declare s='';
declare -i i=-1;
declare -a a=();
s=$(give me a string); rc=$?; if [[ $rc -ne 0 ]]; then echo "s [$rc]." >&2; exit 1; fi;
i=$(give me a number); rc=$?; if [[ $rc -ne 0 ]]; then echo "i [$rc]." >&2; exit 1; fi;
a=($(gimme an array)); rc=$?; if [[ $rc -ne 0 ]]; then echo "a [$rc]." >&2; exit 1; fi;
Edit: Ok, I thought of something that comes close to what you want, but if properly done, it would need to be two statements, and it's ugly, although elegant in a way. And it would only work if the value you want to assign has no spaces or glob (pathname expansion) characters, which makes it quite limited.
The solution involves declaring the variable as an array, and having the command substitution print two words, the first of which being the actual value you want to assign, and the second being the return code of the command substitution. You can then check index 1 afterward (in addition to $?, which can still be used to check the success of the actual declare call, although that shouldn't ever fail), and if success, use index 0, which elegantly can be accessed directly as a normal non-array variable can:
declare -a y=($(echo value-for-y; false; echo $?;)); [[ $? -ne 0 || ${y[1]} -ne 0 ]] && { echo 'error!'; exit 1; }; ## fails, exits
## error!
declare -a y=($(echo value-for-y; true; echo $?;)); [[ $? -ne 0 || ${y[1]} -ne 0 ]] && { echo 'error!'; exit 1; }; ## succeeds
echo $y;
## value-for-y
I don't think you can do better than this. I still recommend my original solution: declare separately from command substitution+assignment.