Redirection Wizardry
or How to make two parallel pipes
Hi all,
Goal: understand the following script and why it is usefull:
{
print "Standard"
print "Error" >&2
print "Main" >&3
} > >(awk '{print "BUF1: "$0}') \
2> >(awk '{print "BUF2: "$0}') \
3> >(awk '{print "BUF3: "$0}')
Problem
I write a script with:
echo "first step" launch a complex sub command echo "second step" launch a complex sub command ...
It’s output should be:
first step hello, I'm a big long complex command which output this And I write many things. Here is an error message. second step therefore It is difficult to see where I end
My main logs are difficult to discern and therefore should be unnoticed
. Most of time the sub command is not mine, and therefore, it is not easy to manage their appearance.
First solution: redirect command output
Prefix each output of the complex sub command
echo "first step"
complexSubcommand | awk '{print "\tcomplexSubcommand: "$0}'
echo "second step"
complexSubcommand2 | awk '{print "\tcomplexSubcommand2: "$0}'
OK, it is far better now:
first step
complexSubcommand: hello, I'm a big long complex
complexSubcommand: command which output this
complexSubcommand: And I write many things.
Here is an error message.
second step
complexSubcommand2: therefore
complexSubcommand2: It is difficult to see where I end
Unfortunately the standard error is not redirected.
Second solution: redirect everything
One can do:
echo "first step"
complexSubcommand 2>&1 | awk '{print "\tcomplexSubcommand:"$0}'
echo "second step"
complexSubcommand2 2>&1 | awk '{print "\tcomplexSubcommand2:"$0}'
output is now:
first step
complexSubcommand: hello, I'm a big long complex
complexSubcommand: command which output this
complexSubcommand: And I write many things.
complexSubcommand: Here is an error message.
second step
complexSubcommand2: therefore
complexSubcommand2: It is difficult to see where I end
This is better, but error is lost in the flow of standard output. This is why it would be good to prefix the standard output by some string and standard error by another more visible string.
But it is not possible to implement that only with pipe or even redirection between standard output and error.
Complete solution: using named pipe
Then, a final solution is to use named pipe.
One named pipe for standard output, one for standard error and one for main messages.
Here is an example:
#!/usr/bin/env zsh
Creation of the fifos: /tmp/y/fifo1, fifo2, fifo3
fifo="/tmp/y/fifo"
mkdir -p $(dirname $fifo)
for n in 1 2 3; do
if [[ ! -p "fifo$n" ]]; then
mkfifo $fifo$n
fi
done
# will print what is written in the fifo
# prefix all string written in fifoX by BUF X:
for n in 1 2 3; do
awk "{print \"BUF $n:\"\$0}" < $fifo$n &
done
# here begins the program
{
print "Standard"
print "Error" >&2
print "Main" > ${fifo}3
# close the standard output and error
2>&- >&-
# redirect all standard output to fifo1
# and all standard error to fifo2
} >${fifo}1 2>${fifo}2
# once the program ended
# delete the fifos
for n in 1 2 3; do
\rm $fifo$n
done
Here is the result:
BUF 1:Standard BUF 2:Erreur BUF 3:Main
Final solution: use zsh syntax
Finally I recently found a way to do exactly the same with a much clearer and compact syntax. Thanks zsh:
{
print "Standard"
print "Error" >&2
print "Main" >&3
} > >(awk '{print "BUF1: "$0}') \
2> >(awk '{print "BUF2: "$0}') \
3> >(awk '{print "BUF3: "$0}')
Here is the result:
BUF 1:Standard BUF 2:Erreur BUF 3:Main
Add include directive to awk
With gawk, there exists the @include directive :
@include "fic"
But, when you work for a company which use a prehistoric version of awk, you could use my script (it is more a hack than a real program) :
#!/usr/bin/env zsh
if (($#<1)); then
{
echo "usage : $(basename $0) script.awk arg1 ... argN"
echo "provide @include \"path/to/awk/file\" directive"
echo "usefull for libraries"
} >&2
exit 1
fi
tmpScriptAwk="/tmp/$(basename $1)"
# see my blog entry about this function
function matche
{
echo "$1" | egrep "$2" > /dev/null
}
# go into the same directory as the script (to find the good path)
cd $(dirname $1)
# read the script
IFS='
'
for ligne in $(cat $(basename $1) | sed 's/\\/\\\\/g'); do
# transform @include "path/to/file" by the content of path/to/file.awk
if matche "$ligne" "^@include \".*\"" ; then
nomFic=$(echo $ligne | perl -p -e 's#\@include "(.*)"#$1#' )
# verify if the file to include is readable
if [ ! -r "$nomFic" ]; then
# add the .awk prefix (as it is not necessary)
nomFic="$nomFic.awk"
if [ ! -r "$nomFic" ]; then
echo "$nomFic n'est pas accessible en lecture" >&2
exit 1
fi
fi
# write the content of the included file
cat $nomFic
else
echo $ligne
fi
done > $tmpScriptAwk
shift
# run the script with the argument
awk -f $tmpScriptAwk "$@"
Hope this help,
global search and replace on place with perl
This is a tip on how to modify many files (when sed don’t work):
perl -pi -e 's/toto/tata/g' file1 file2 ... fileN
If you want to make a test assign a suffix at all files (beware to use suffix no existing file already has):
perl -pi.orig -e 's/toto/tata/g' file1 file2 ... fileN
or to make a preview on the standard output:
perl -p -e 's/toto/tata/g' file1 file2 ... fileN
In short to make perl work like sed or awk simply use perl -pe instead of just perl. And use perl -pi -e if you want to replace files in place.
Bash or Zsh pointer
Here is a way to implement pointer like behavior in shell script:
toto="tata" ptr="toto" echo $( eval echo \$$ptr )
tata
complex remote command with ssh
How to launch remote command ? Use ssh:
> ssh user@host "ls /home"
OK it works!
PROBLEM
With complex commands:
> remoteUser="toto" ; > ssh user@host "for fic in /home/$remoteUser/???/*(/); do echo $fic | egrep -v "common$" ; done
It fails miserably! Because all special characters are interpreted within the command. And even if you protect all special characters, the remote shell should not be zsh and the command will not be interpreted correctly.
SOLUTION
remoteUser="toto"
ssh cmsrct <<END
zsh -s <<END2
for fic in /home/$remoteUser/???/*(/); do
echo \\\$fic | egrep -v "common\\\$"
done
END2
END
All special character are no more interpreted except variables (such as $fic). This is why you need to put three backslashes before the $. The previous code will execute the following command on the remote host:
for fic in /home/toto/???/*(/); do
echo $fic | egrep -v "common$"
done
Bazaar (bzr) with existing projects
Bazaar is a distributed concurent versions system (DCVS). That means, you can focus on the development of one feature at a time. You “freeze” the main project, working in a totally isolated environment. Others should continue on the main project while you work on only one feature.
When you have finished you can pull your change into the main project.
Bazaar gives you many others benefits :
- security (backups),
- history (who as done what)
- focusing (work on one feature at a time)
- multi-user
- work on parallel versions of the same program (remove a bug on all versions)…
Unfortunately, most of time, project have an older versionning system not as elegant as Bazaar. But you can really easily handle this.
- Download sources using the versionning system used by the other (subversion for example)
- into the root of the new arborescence
(said its name is main) do a:bzr initbzr ingnore **/.svnbzr add- and then
bzr commit.
Now you have a complete new bazaar branch containing the code of theproject
- Now you want to add a new feature :
bzr branch main new-feature-branch- hack, hack, hack…
svn updatebzr branch ../new-feature-branch- In case of conflicts resolve them and do not forget to put the conflict files into your
new-feature-branch svn commit
How to write a counter in zsh
Here is a code to be able to write a counter in zsh. The advantage are it work alwas on the same line. The trick is to use backspace characters.
for example:
host> echo "123\b4" 124
What happened ?
- write 1
- write 2
- write 3
- backspace (delete 3 and position the cursor on the 3)
- write 4
Now real usage:
n=1
echo
while true; do
printf "\b\b\b\b\b%5s" $((n++))
sleep 1
done
A faster way should be to use the ‘\r’ which erase the beginning of the line.
n=1
echo
while true; do
printf "\r%5s" $((n++))
sleep 1
done
Don’t get burned! Easy password managment
We all have to use many passwords. Unfortunately, most of time, we use only a few (if not one) password everywhere. Most of time it is really dangerous. Think of it:
I’m using an email account which belongs to my employer. If the password I use for my job is the same as my personnal gmail password. If he wants, he can try the same password to view all my personal mail or track my monster account…
This is a good reason enough to manage many different passwords. But how to do it? In general, best answers are:
- Use a program containing all password
- Write it on paper (use steganography with phone numbers for example)
- Use a checksum of the concatenation of your password and the domain name of the site you want to visit.
The first method is the best when you work only on one computer, on one place. Unfortunately, if you use many computer, it is a pain to synchronize between each of them.The second method is the basic method, not as bad as it should appear. But it is also not a really secure way to do that ; think about physical destruction by error or by accident.The third method is the one I adopted. On most computer it is easy to have a sha1 or md5 checksum at hand. You just have to do it that way :
echo -n "myUniqueStrongPassword_DomainName" | openssl sha1
or use services such as :
This process gives you a string to use as password, from which it is almost impossible to recover your password. Now you only have to remember one strong password and to calculate the checksum each time you need the password.
Many people use this method, but in my humble opinion, this is not enough shared.
bash (or zsh) regular expression matching
I wanted to be able to write conditionnal on string matching in bash. One easy way to do that is :
if echo "$fic" | egrep "regexp" > /dev/null; then
echo "it matches !"
else
echo "it doesn't matches"
fi
But I found it a litle too long. This why I have written the small script match :
echo $1 | egrep $2 > /dev/null
in order to be able to write the more readable (in my point of vue) command :
if match "$fic" "regexp"; then
echo "it matches !"
else
echo "it doesn't matches"
fi