Bash test in pipe results in weird behaviour - linux

I am trying to write a coloured output library for bash with various styling options allowing colouring and styling using redirection.
e.g.
echo "Red" | red outputs red text
and
echo "Bold" | bold outputs bold text
and
echo "Yellow bold" | yellow | bold outputs bold yellow text
The code I wrote so far is as follows:
#shellcheck shell=bash
# set debug
# set -o xtrace
# number of colors supported
__colors=$(tput colors 2> /dev/null)
# colors
__black="$(tput setaf 0)"
__red="$(tput setaf 1)"
__green="$(tput setaf 2)"
__yellow="$(tput setaf 3)"
__blue="$(tput setaf 4)"
__magenta="$(tput setaf 5)"
__cyan="$(tput setaf 6)"
__white="$(tput setaf 7)"
# style
__default="$(tput sgr0)"
__bold="$(tput bold)"
__underline="$(tput smul)"
function has_colors() {
COLOR=${COLOR:-auto}
if [[ $COLOR = 'never' ]]; then
return 1
elif [[ $COLOR = 'always' ]]; then
return 0
else
# check if stoud is terminal and terminal supports colors
[[ -t 1 ]] && \
[[ -n $__colors ]] && \
[[ $__colors -ge 8 ]]
fi
}
function __style() {
read -r input
if has_colors; then
echo -e "$1" "$input" "$__default"
else
echo -e "$input"
fi
}
function black() {
__style "$__black"
}
function red() {
__style "$__red"
}
function green() {
__style "$__green"
}
function yellow() {
__style "$__yellow"
}
function blue() {
__style "$__blue"
}
function magenta() {
__style "$__magenta"
}
function cyan() {
__style "$__cyan"
}
function white() {
__style "$__white"
}
function bold() {
__style "$__bold"
}
function underline() {
__style "$__underline"
}
Setting COLOR=always outputs with escape codes all the time. On the other hand COLOR=auto preforms some checks to make sure current stdout is the terminal and terminal supports colors.
The problem is using multiple styling options does not seem to be working.It always applies the last styling option. For example:
echo "Yellow bold" | yellow | bold outputs bold text, but not yellow.
On the other hand:
echo "Bold yellow" | bold | yellow outputs yellow text, but not bold.
Funny thing is; setting COLOR=always seems to be working just fine. So it looks like the test I perform to see if stdout is terminal [[ -t 1 ]] is causing this. I am not sure if it is because there is some kind of delay with that test. But when I remove [[ -t 1 ]] bit, it works.
Any idea how I can achieve this ? Not expert with Bash or how shell works for that matter. Quite confused here.

Looking at this with a clear head, I found the problem in my approach.
[[ -t 1 ]] tests if stdout is terminal, when I pipe two function like echo "Hello" | yellow | bold , [[ -t 1 ]] is false when going through function yellow denoting output is not a terminal.
Which is because output of the function( yellow) is piped into the second function (bold). That's why it does not output escape code for yellow and just outputs the input.
So If I keep piping to another function like echo "Hello" | yellow | bold | underline it will only underline the output.
This seemed to be a nice and easy way of outputting with colour, but now I have to change my approach unless there is a way to know if currently running function is being piped but not redirected?
EDIT
This post shows there is a way to detect if command is being redirected or being piped.
Still in the end this approach doesn't seem quite feasible because if I do not disable colour when piped it will output colour codes when piped to another command that is not another output styling function; That maybe an edge case, but still solution is not %100 fault proof
EDIT Solution:
Changed the approach. Instead of piping formats one after another using next format options as parameters to first one like below does the trick
echo "Hello" | yellow bold underline
The final code is follows:
#shellcheck shell=bash
# set debug
# set -xv
# number of colors supported
__colors=$(tput colors 2> /dev/null)
# foreground colors
__black="$(tput setaf 0)"
__red="$(tput setaf 1)"
__green="$(tput setaf 2)"
__yellow="$(tput setaf 3)"
__blue="$(tput setaf 4)"
__magenta="$(tput setaf 5)"
__cyan="$(tput setaf 6)"
__white="$(tput setaf 7)"
# background colors
__bg_black="$(tput setab 0)"
__bg_red="$(tput setab 1)"
__bg_green="$(tput setab 2)"
__bg_yellow="$(tput setab 3)"
__bg_blue="$(tput setab 4)"
__bg_magenta="$(tput setab 5)"
__bg_cyan="$(tput setab 6)"
__bg_white="$(tput setab 7)"
# style
__reset="$(tput sgr0)"
__bold="$(tput bold)"
__underline="$(tput smul)"
function has_colors() {
COLOR=${COLOR:-auto}
if [[ $COLOR = 'never' ]]; then
return 1
elif [[ $COLOR = 'always' ]]; then
return 0
else
[[ -t 1 ]] && [[ -n $__colors ]] && [[ $__colors -ge 8 ]]
fi
}
function __format() {
local format="$1"
local next="${2:-}" # next formatting function e.g. underline
if has_colors; then
echo -en "$format"
if [[ -n $next ]]; then
shift 2
tee | "$next" "$#"
else
tee
echo -en "$__reset"
fi
else
tee #print output
fi
}
function black() { __format "$__black" "$#"; }
function red() { __format "$__red" "$#"; }
function green() { __format "$__green" "$#";}
function yellow() { __format "$__yellow" "$#"; }
function blue() { __format "$__blue" "$#"; }
function magenta() { __format "$__magenta" "$#";}
function cyan() { __format "$__cyan" "$#";}
function white() { __format "$__white" "$#";}
function bg_black() { __format "$__bg_black" "$#"; }
function bg_red() { __format "$__bg_red" "$#"; }
function bg_green() { __format "$__bg_green" "$#";}
function bg_yellow() { __format "$__bg_yellow" "$#"; }
function bg_blue() { __format "$__bg_blue" "$#"; }
function bg_magenta() { __format "$__bg_magenta" "$#";}
function bg_cyan() { __format "$__bg_cyan" "$#";}
function bg_white() { __format "$__bg_white" "$#";}
function bold() { __format "$__bold" "$#";}
function underline() { __format "$__underline" "$#"; }

Related

How to return value in shell script function? [duplicate]

This question already has answers here:
Returning value from called function in a shell script
(5 answers)
Closed 6 years ago.
This is my function
validateFile()
{
echo "$1" | grep '.zip$' > /dev/null
if [ $? -eq 0 ]
then
return 1
else
return 0
fi
}
printf "\n Enter Source File1 Path: "
read file1
res=$(validateFile $file1);
echo "$res"
But nothing store in **"res"** store nothig
validateFile function verify that file is zip file or not if yes then return 2 else 0.And returned value stored in res variable. But issue is no value store in res variable.
What you want is maybe
validateFile()
{
echo "$1" | grep '.zip$' > /dev/null
if [ $? -eq 0 ]
then
# return appropriate value
return 1
else
return 0
fi
}
printf "\n Enter Source File1 Path: "
read file1
# call function
validateFile $file1
# use return code
res=$?
echo "$res"
Although shell script provides a return statement, the only thing you can return with, is the function's own exit status (a value between 0 and 255, 0 meaning "success").
Best way to return a value is to echo the same, something like below:
validateFile()
{
echo "$1" | grep '.zip$' > /dev/null
if [ $? -eq 0 ]
then
echo 1
else
echo 0
fi
}
printf "\n Enter Source File1 Path: "
read file1
res=$(validateFile $file1);
echo "$res"
$(...) invokes a subshell and captures its standard output. So you can do one of these two things:
foo() {
fooresult=1
}
foo
echo $fooresult
or this:
foo() {
echo 1
}
fooresult=$(foo)
echo $fooresult
validateFile()
{
echo "$1" | grep '.zip$' > /dev/null
if [ $? -eq 0 ]
then
echo 1
else
echo 0
fi
}
printf "\n Enter Source File1 Path: "
read file1
res=$(validateFile $file1);
echo "$res"

Validate input in bash script

I have a code to find the area of a rectangle by giving the width and height.
echo -n "Enter width: "
read width
echo -n "Enter height:"
read height
echo "Area of rectangle $(echo "$height*$width" | bc) sqcm"
How can I make it so that only a number can be entered and, otherwise, an error display?
Since you are reading input twice, I would use a function to check it. This way you do not repeate code.
This checks whether input contains just digits and at least one. Otherwise, it keeps asking for the input:
myread () {
while : # infinite loop
do
read value
[[ $value =~ ^[0-9]+$ ]] && echo "$value" && return #return value if good input
done
}
echo -n "Enter width: "
width=$(myread) #call to the funcion and store in $width
echo -n "Enter height: "
height=$(myread) #call to the funcion and store in $height
echo "Area of rectangle $(echo "$height*$width" | bc) sqcm"
You could perhaps use grep to check but really bash (and shell in general) is a bad choice of language if you want these kinds of checks.
You can do some thing like thi
if [[ -n "$width" ]] ; then
nodigits="$(echo $width| sed 's/[[:digit:]]//g')"
if [[ ! -z $nodigits ]] ; then
print "Invalid number format! Only digits, no commas, spaces, etc."
fi
fi
Something like:
echo $width | grep -E -q '^[0-9]+$' || echo "numeral expected!"

BASH: different colour for text entered by user

I've been tidying up my BASH prompt.
Currently it looks like this:
Here is the code:
# NOTE: OSX uses .bashprofile http://superuser.com/questions/244964/mac-os-x-bashrc-not-working
# http://mywiki.wooledge.org/BashFAQ/037
bold=$( tput bold || tput md )
black=$( tput setaf 0 )
red=$( tput setaf 1 )
green=$( tput setaf 2 )
blue=$( tput setaf 4 )
white=$( tput setaf 7 || tput AF 7 )
RESET=$( tput sgr0 )
# https://github.com/sickill/stderred
export DYLD_INSERT_LIBRARIES="/usr/lib/libstderred.dylib${DYLD_INSERT_LIBRARIES:+:$DYLD_INSERT_LIBRARIES}"
export STDERRED_ESC_CODE="$bold$red"
pre_prompt ()
{
if [ $? = 0 ]; then
echo "$green โœ”";
else
echo "$red โœ˜";
fi
printf "$RESET\n"
printf "$bold"
printf "%s#%s ~ %s:\n" "$USER" "$HOSTNAME" "$PWD"
printf "$RESET"
}
# execs before prompt
export PROMPT_COMMAND=pre_prompt
# \[ ... \] --> http://mywiki.wooledge.org/BashFAQ/053
export PS1="\[$bold$blue\] โค \[$RESET$bold\]"
export PS2="-2-> "
export PS3="-3-> "
export PS4="-4-> "
Note I'm using a super little piece of code by sickill to have STDERR get printed in red.
The only thing that I would still like to improve is to hilight text entered by the user.
Is there any way to do this?
After your $RESET, add $bold plus whatever color code you want to use to highlight the commandline entry.

How to combine Case with shift and add results in a list in bash

When I write parameters they should be checked in a case and then added in a list if u type -c in front the parameter should be in uppercase and if u type -4 the parameter should appear 4 times
#!/bin/bash
function forloop {
for each in naam
do
echo $naam zegt je dat...
echo ""
echo
let "x+=1"
done
}
function helper {
echo "use names only."
echo "if u type -c before a word then it wil be shown in capital"
echo "if u type -(number) before a word the word will be shown (number)times "
}
naam=null
x=0
while [ $x -le 4 ]
do
case $1 in
-c) echo "${2 ^^}";shift ;;
help) helper ;shift;naam=helper;;
-4) $1x-1
exit ;shift;;
esac
naam=" $1"
shift
forloop
done
exit
#!/bin/bash
if [ -z "$1" ]
then
echo "use names only."
echo "if you type -c before a word, then it will be shown in capitals"
echo "if you type -NUMBER before a word, the word will be shown NUMBER times"
exit
fi
while [ $1 ]
do
case $1 in
-c) shift; <<<$1 tr '[:lower:]' '[:upper:]';;
-[0-9]*)for ((count=$1; count++; )) do echo $2; done; shift;;
*) echo $1
esac
shift
done

In bash, how to store a return value in a variable?

I know some very basic commands in Linux and am trying to write some scripts. I have written a function which evaluates the sum of last 2-digits in a 5-digit number. The function should concatenate this resultant sum in between the last 2-digits and return it. The reason I want to return this value is because I will be using this value in the other function.
Ex: if I have 12345, then my function will calculate 4+5 and return 495.
#!/bin/bash
set -x
echo "enter: "
read input
function password_formula
{
length=${#input}
last_two=${input:length-2:length}
first=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $2}'`
second=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $1}'`
let sum=$first+$second
sum_len=${#sum}
echo $second
echo $sum
if [ $sum -gt 9 ]
then
sum=${sum:1}
fi
value=$second$sum$first
return $value
}
result=$(password_formula)
echo $result
I am trying to echo and see the result but I am getting the output as shown below.
-bash-3.2$ ./file2.sh
+++ password_formula
+++ echo 'enter: '
+++ read input
12385
+++ length=8
+++ last_two=85
++++ echo 85
++++ sed -e 's/\(.\)/\1 /g'
++++ awk '{print $2}'
+++ first=5
++++ echo 85
++++ sed -e 's/\(.\)/\1 /g'
++++ awk '{print $1}'
+++ second=8
+++ let sum=5+8
+++ sum_len=2
+++ echo 5
+++ echo 8
+++ echo 13
+++ '[' 13 -gt 9 ']'
+++ sum=3
+++ value=835
+++ return 835
++ result='enter:
5
8
13'
++ echo enter: 5 8 13
enter: 5 8 13
I also tried to print the result as:
password_formula
RESULT=$?
echo $RESULT
But that is giving some unknown value:
++ RESULT=67
++ echo 67
67
How can I properly store the correct value and print (to double check) on the screen?
Simplest answer:
the return code from a function can be only a value in the range from 0 to 255 .
To store this value in a variable you have to do like in this example:
#!/bin/bash
function returnfunction {
# example value between 0-255 to be returned
return 23
}
# note that the value has to be stored immediately after the function call :
returnfunction
myreturnvalue=$?
echo "myreturnvalue is "$myreturnvalue
The return value (aka exit code) is a value in the range 0 to 255 inclusive. It's used to indicate success or failure, not to return information. Any value outside this range will be wrapped.
To return information, like your number, use
echo "$value"
To print additional information that you don't want captured, use
echo "my irrelevant info" >&2
Finally, to capture it, use what you did:
result=$(password_formula)
In other words:
echo "enter: "
read input
password_formula()
{
length=${#input}
last_two=${input:length-2:length}
first=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $2}'`
second=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $1}'`
let sum=$first+$second
sum_len=${#sum}
echo $second >&2
echo $sum >&2
if [ $sum -gt 9 ]
then
sum=${sum:1}
fi
value=$second$sum$first
echo $value
}
result=$(password_formula)
echo "The value is $result"
It is easy you need to echo the value you need to return and then capture it like below
demofunc(){
local variable="hellow"
echo $variable
}
val=$(demofunc)
echo $val
Use the special bash variable "$?" like so:
function_output=$(my_function)
function_return_value=$?
The answer above suggests changing the function to echo data rather than return it so that it can be captured.
For a function or program that you can't modify where the return value needs to be saved to a variable (like test/[, which returns a 0/1 success value), echo $? within the command substitution:
# Test if we're remote.
isRemote="$(test -z "$REMOTE_ADDR"; echo $?)"
# Or:
isRemote="$([ -z "$REMOTE_ADDR" ]; echo $?)"
# Additionally you may want to reverse the 0 (success) / 1 (error) values
# for your own sanity, using arithmetic expansion:
remoteAddrIsEmpty="$([ -z "$REMOTE_ADDR" ]; echo $((1-$?)))"
E.g.
$ echo $REMOTE_ADDR
$ test -z "$REMOTE_ADDR"; echo $?
0
$ REMOTE_ADDR=127.0.0.1
$ test -z "$REMOTE_ADDR"; echo $?
1
$ retval="$(test -z "$REMOTE_ADDR"; echo $?)"; echo $retval
1
$ unset REMOTE_ADDR
$ retval="$(test -z "$REMOTE_ADDR"; echo $?)"; echo $retval
0
For a program which prints data but also has a return value to be saved, the return value would be captured separately from the output:
# Two different files, 1 and 2.
$ cat 1
1
$ cat 2
2
$ diffs="$(cmp 1 2)"
$ haveDiffs=$?
$ echo "Have differences? [$haveDiffs] Diffs: [$diffs]"
Have differences? [1] Diffs: [1 2 differ: char 1, line 1]
$ diffs="$(cmp 1 1)"
$ haveDiffs=$?
$ echo "Have differences? [$haveDiffs] Diffs: [$diffs]"
Have differences? [0] Diffs: []
# Or again, if you just want a success variable, reverse with arithmetic expansion:
$ cmp -s 1 2; filesAreIdentical=$((1-$?))
$ echo $filesAreIdentical
0
It's due to the echo statements. You could switch your echos to prints and return with an echo. Below works
#!/bin/bash
set -x
echo "enter: "
read input
function password_formula
{
length=${#input}
last_two=${input:length-2:length}
first=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $2}'`
second=`echo $last_two| sed -e 's/\(.\)/\1 /g'|awk '{print $1}'`
let sum=$first+$second
sum_len=${#sum}
print $second
print $sum
if [ $sum -gt 9 ]
then
sum=${sum:1}
fi
value=$second$sum$first
echo $value
}
result=$(password_formula)
echo $result
Something like this could be used, and still maintaining meanings of return (to return control signals) and echo (to return information) and logging statements (to print debug/info messages).
v_verbose=1
v_verbose_f="" # verbose file name
FLAG_BGPID=""
e_verbose() {
if [[ $v_verbose -ge 0 ]]; then
v_verbose_f=$(tempfile)
tail -f $v_verbose_f &
FLAG_BGPID="$!"
fi
}
d_verbose() {
if [[ x"$FLAG_BGPID" != "x" ]]; then
kill $FLAG_BGPID > /dev/null
FLAG_BGPID=""
rm -f $v_verbose_f > /dev/null
fi
}
init() {
e_verbose
trap cleanup SIGINT SIGQUIT SIGKILL SIGSTOP SIGTERM SIGHUP SIGTSTP
}
cleanup() {
d_verbose
}
init
fun1() {
echo "got $1" >> $v_verbose_f
echo "got $2" >> $v_verbose_f
echo "$(( $1 + $2 ))"
return 0
}
a=$(fun1 10 20)
if [[ $? -eq 0 ]]; then
echo ">>sum: $a"
else
echo "error: $?"
fi
cleanup
In here, I'm redirecting debug messages to separate file, that is watched by tail, and if there is any changes then printing the change, trap is used to make sure that background process always ends.
This behavior can also be achieved using redirection to /dev/stderr, But difference can be seen at the time of piping output of one command to input of other command.
Ok the main answers to this are problematic if we have errexit set, e.g.
#!/bin/bash
set -o errexit
my_fun() {
# returns 0 if the first arguments is "a"
# 1 otherwise
[ "${1}" = "a" ]
}
my_fun "a"
echo "a=$?"
my_fun "b"
echo "b=$?"
In this case bash just exit when the result is not 0, e.g. this only prints the a line.
./test_output.sh
a=0
As already said well here probably the most correct answer is something like this:
# like this
my_val=0 ; my_fun "a" || my_val=$?
echo "a=${my_val}"
# or this
my_fun "b" && my_val=0 || my_val=$?
echo "b=${my_val}"
This print all the values correctly without error
a=0
b=1
I don't know if the "echo" implementation is the most correct, as I still is not clear to me if
$() creates a subshell or not.
I have a function for example that opens up a
file descriptor in a function and return the number and it seems that bash closes the fd after exiting the function.
(if someone can help me here :-)

Resources