Confused about use of return status code shell script? - linux

In a book I'm reading the below line
ls "$1" 2>/dev/null | grep "$1" 2>/dev/null 1>&2
when written in a script - by the book it says "The command is executed to check whether the file passed as the command line argument exists. The standard error is redirected to /dev/null (the unix black hole), and standard output is redirected to standard error by using 1>&2. Thus, the command does not produce any output or error message; its only puprose is to set the command returns status value $?."
But running the code:
if [ $? -eq 0 ]
would I not know it otherwise, I have tried without the cmd at beginning and with it as well with having no impact on the results. I'm sure the author would have written for some purpose. I cannot just figure what?

This looks like a very bad book, giving code that noone sane would ever write to poorly illustrate concepts that are generally used in completely different ways in shell scripts.
The line:
ls "$1" 2>/dev/null | grep "$1" 2>/dev/null 1>&2
is as described -- it has no visible effect other than setting the return code. Is your question about what this does in detail to get a return code or something else?
The line:
if [ $? -eq 0 ]
is an incomplete fragment that checks the return code of the previous command. It's incomplete as there is no then or fi, without which the shell will reject it as a syntax error and not do anything (if you type the above at a prompt, you'll get the secondary prompt, telling you the shell is waiting for more input to get a complete command). So without more code there's no apparent effect. Something more complete like:
if [ $? -eq 0 ]; then echo YES; else echo NO; fi
would output YES or NO based on that return code.
A more sensible way of doing the 6 lines starting with the ls would be:
if [ ! -e "$1" ]; then
echo "$1: not found"
exit 1
fi
As to what the ls line actually does, it runs ls (list files) with the name in $1 as an argument, then uses grep to search that listing for the same filename.
So if the file does not exist, ls gives an error and outputs nothing, so the grep fails (setting $? to 1). If the filename exists and is not a directory, the grep will succeed (setting $? to 0). Finally, if the filename exists and is a directory, it will search the contents of that directory, looking for any file or subdirectory with the same name as a substring -- which is probably just a bug. In addition, if $1 is a string beginning with -, it will do something fairly useless and unpredictable.
Overall, a prime example of a shell script that should never be written -- any student that turned in such a monstrosity should get an immediate F.

Related

linux device which don't allow to write on it

I have a script which write some warnings to separate file (it's name is passed as an argument). I want to make this script fail if there is a warning.
So, I need to pass some file name which must raise an error if someone try to write there.
I want to use some more or less idiomatic name (to include it to man page of my script).
So, let say my script is
# myScript.sh
echo "Hello" > $1
If I call it with
./myScript.sh /dev/stdin
it is not fail because /dev/stdin is not read-only device (surprisingly!)
After
./myScript.sh /
it is failed, as I want it (because / is a directory, you can't write there). But is is not idiomatic.
Is there some pretty way to do it?
if [ -w "$1" ]
then
echo "$Hello" > "$1" # Mind the double-quotes
fi
is what you're looking for. Below would even be better in case you've only
one argument.
if [ -w "$*" ]
then
echo "$Hello" > "$*" # Mind the double-quotes
fi
$* is used to accommodate nonstandard file names. "$*" combines multiple arguments into a single word. Check [ this ].

Problems comparing strings (BASH)

I'm trying to compare 2 strings. One comes from a file through the grep command and the other one never changes because I'm always comparing it with the ones I create while reading file texts. If they are equal, the program should print the data associated with the content that the new string contains. I've tried with all the syntax that bash allows (cause I'm new at bash) but it is just not working like I expected. It looks like the second if doesn't work, because I tried earlier without that and only print the strings (echo $text) and it worked, but not the proper way as the exercise I'm doing asks for. I have to show in the console only the pid and state of the processes that are running.
cd
cd /proc
run="State: S (sleeping)"
for i in $( ls -d */);
do
cd $i
if [ -f /proc/$i/status ];
then text="`grep -w "S" status`"
if [ "$text" == "$run" ]
then grep -w "Pid" status
grep -w "State" status
fi
cd /proc
else cd /proc
fi
done
;;
Your run variable contains State:<space>S (sleeping), but /proc/<pid>/status files contain State:<tab>S (sleeping) (at least, on my system). So you should replace that space character with tab character.

Is the directory NOT writable

Can anyone tell me why this is always saying that the directory is not writable, when it absolutely is?
$dnam="/home/bryan/renametest/C D"
# Is the directory writable
err=0
if [ ! -w $dnam ]
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
You need double-quotes around $dnam -- without them, it's interpreted as two separate shell words, "/home/bryan/renametest/C" and "D", which makes an invalid test expression and hence fails. This should work:
if [ ! -w "$dnam" ]
#tink's suggestion of [[ ]] is a cleaner way of doing tests like this, but is only available in bash (and some other shells with extended syntax). The fact that you get [[: not found means you're using a fairly basic shell, not bash.
I see multiple problems:
You are using a space inside your variable. This is not illegal, but in combination line you use the variable unescaped and generate the following command:
if [ ! -w /home/bryan/renametest/C D ]
This is not a valid syntax. The simplest way to fix this is changing the line to
if [ ! -w "$dnam" ]
The next problem is worse: On my system, help test returns the text:
-w FILE True if the file is writable by you.
Which means, the command doesn't support directories but only files. If you want to check if a directory is writable, you will have to use a different command
As everyone else said, the $dnam variable needs double quotes. Here's why:
The [ ... ] is an alias to the test command. If you look in your system, you will see a file called /bin/[ or maybe /bin/usr/[. On some systems, this is a hard link to /bin/test or /bin/usr/test. The if statement executes what comes after the if, and if that command returns a zero exit status, the if statement will execute the then clause. Otherwise, if there is an else clause, that will execute instead.
To allow for boolean testing, Unix included the test command, so you could do this:
if test -d "$directory"
then
echo "Directory $directory exists!"
fi
Later on, the /bin/[ was added as syntactic sugar. This is identical to the above:
if [ -d "$directory" ]
then
echo "Directory $directory exists!"
fi
Now, both [ and test are builtin commands, but they are *still commands. This means that the shell interpolates the command and then executes it.
Try executing the following:
$ set -xv # Turns on shell debugging
$ dnam="/home/bryan/renametest/C D"
dnam="/home/bryan/renametest/C D"
+ dnam='/home/bryan/renametest/C D'
$ test -d $dnam
test -d $dnam
+ test -d /home/bryan/renametest/C D
$ echo $?
echo $?
+ echo 1
1
$ test -d "$dnam" # Now with quotes
test -d $dnam
+ test -d "/home/bryan/renametest/C D"
$ echo $?
echo $?
+ echo 0
0
$ set +xv # Turn off the debuggin
Each command is echoed twice. The first time as written, and the second time after the line is interpolated. As part of the interpolation, the shell splits parameters on white space. As you can see, the test command is testing the presence of /home/bryan/renamtest/C which doesn't exist and thus not writable. I'm actually surprised that the test command didn't print an error message because you passed it an extra parameter.
In the second attempt, you added quotes. These quotes prevented the shell from splitting your parameters on the space and keep the directory name as a single parameter.
Since [ ... ] is a command, you have to take into account the shell's interpolation of variables and other issues. And, if you're not absolutely careful, you can end up with errors.
Even worse, sometimes the [ ... ] might work and sometimes it might not. If your directory name didn't contain spaces, it will work as expected. Imagine you're writing a program, and you test it and everything works because all directories you've tried don't have spaces. Then, someone uses your program, but has a space in the directory. A substantial number of shell script bugs are do to this type of issue in if statements.
This is why Bash introduced the [[ ... ]] tests. The [[ isn't a command but a statement. This means that the shell doesn't directly interpolate the results. Instead, the parameters are parsed, and then any interpolation is done. Thus, this would have worked:
dnam="/home/bryan/renametest/C D" # No "$" in front of the variable!
# Is the directory writable
if [[ ! -w $dnam ]] # No quotation marks needed!
then
# Not writable. Pop the error and exit.
echo "Directory $dnam is not writable"
err=1
fi
It's almost always better to use the [[ ... ]] test rather than the [ ... ] test, so go ahead and get into the habit.
One more minor error, you had:
$dnam="/home/bryan/renametest/C D"
This gets interpolated by the shell, so the variable being set is whatever the value of $dnam just happens to be. If $dnam happened to equal "foo", you would been doing this:
foo="/home/bryan/renametest/C D"
Not what you want.
You want to leave the $ off when you set variables:
dnam="/home/bryan/renametest/C D"

Redirecting stdout only if command failed?

I'm writing a bash script that is supposed to be "transparent" to the user. It reads commands from the user and intercepts them, allowing only some of them to be executed by bash, depending on some criteria. It (basically) works like this:
while true; do
read COMMAND
can_be_done $COMMAND
if [ $? == 0 ]; then
eval $COMMAND
if [ $? != 0 ]; then
echo "Error: command not found"
fi
fi
done
The problem is, when the command fails, you also get stuff printed to the console. BUT, if I keep the result in a variable and only print it when it doesn't fail, like so:
RESULT=$(eval $COMMAND)
Then there's another problem: The special formatting gets lost (for example, "ls --color" doesn't show colors anymore)
My question is: Is there a way to have the command print to STDOUT if successful, but to /dev/null if it fails?
Do you really need the second part, replacing the output of the command with an error message? Linux commands print their own error messages, which aren't necessarily "command not found". You'd be hiding the true error (permission denied, file not found, out of memory, segfault, etc.) with an oftentimes incorrect error message (command not found).
If you remove that check, you could simplify the loop to something like this:
while true; do
read -e COMMAND
if can_be_done "$COMMAND"; then
eval "$COMMAND"
fi
done
read -e uses readline to obtain the command, making the prompt a lot more shell-like (&uparrow; and &downarrow; for history, for instance).
command; if [ $? == 0 ]; then is more idiomatically written as if <command>; then.
Quoting makes sure special characters and whitespace are handled properly.
I would argue strongly that you should not do this. If you do not want to see output, redirect it to /dev/null. If you do want to see errors, do not redirect stderr. If you are using a program that prints its error messages on stdout instead of stderr, FIX THE PROGRAM! Error messages belong on stderr. Note that this means your program is broken, as it ought to read:
echo "Error: command not found" >&2
I'm not sure if it is rule number 1, but it certainly belongs in the top 10, and it may be the most often violated rule: Error messages belong on stderr. A program which prints error messages on stdout is broken.
if false > /dev/null;then echo 1; else echo 2; fi 2> /dev/null
Will output 2
if true > /dev/null;then echo 1; else echo 2; fi 2> /dev/null
Will output 1
remove the > /dev/null to print the command also to stdout
for example
if echo 123;then echo 1; else echo 2; fi 2> /dev/null
Will output
123 & 1
Assuming that the command is not very expensive to run you can do this:
test `ls /mooo 2>/dev/null` || echo moo not found
test will return true only if the command exits with 0, in this case ls is the command. You could have put this in an if statement too like so:
if [ `ls /moo 2>/dev/null` ];then
echo moo is a folder
fi

Bash shell `if` command returns something `then` do something

I am trying to do an if/then statement, where if there is non-empty output from a ls | grep something command then I want to execute some statements. I am do not know the syntax I should be using. I have tried several variations of this:
if [[ `ls | grep log ` ]]; then echo "there are files of type log";
Well, that's close, but you need to finish the if with fi.
Also, if just runs a command and executes the conditional code if the command succeeds (exits with status code 0), which grep does only if it finds at least one match. So you don't need to check the output:
if ls | grep -q log; then echo "there are files of type log"; fi
If you're on a system with an older or non-GNU version of grep that doesn't support the -q ("quiet") option, you can achieve the same result by redirecting its output to /dev/null:
if ls | grep log >/dev/null; then echo "there are files of type log"; fi
But since ls also returns nonzero if it doesn't find a specified file, you can do the same thing without the grep at all, as in D.Shawley's answer:
if ls *log* >&/dev/null; then echo "there are files of type log"; fi
You also can do it using only the shell, without even ls, though it's a bit wordier:
for f in *log*; do
# even if there are no matching files, the body of this loop will run once
# with $f set to the literal string "*log*", so make sure there's really
# a file there:
if [ -e "$f" ]; then
echo "there are files of type log"
break
fi
done
As long as you're using bash specifically, you can set the nullglob option to simplify that somewhat:
shopt -s nullglob
for f in *log*; do
echo "There are files of type log"
break
done
Or without if; then; fi:
ls | grep -q log && echo 'there are files of type log'
Or even:
ls *log* &>/dev/null && echo 'there are files of type log'
The if built-in executes a shell command and selects the block based on the return value of the command. ls returns a distinct status code if it does not find the requested files so there is no need for the grep part. The [[ utility is actually a built-in command from bash, IIRC, that performs arithmetic operations. I could be wrong on that part since I rarely stray far from Bourne shell syntax.
Anyway, if you put all of this together, then you end up with the following command:
if ls *log* > /dev/null 2>&1
then
echo "there are files of type log"
fi

Resources