BASH: Assign special symbol to a variable - bash

As a system Administrator I have to work on different servers with different keyboard layouts. So every time I face huge problem with finding keys like '&' ,'|'.
Is there any way I can assign these symbols to a variable and call the variable when ever I need a symbol ?
For example: Assume
echo "|" > filename
pipe = $(cat filename)
ps -ef somethinghere($(pipe)) grep java
should give me running java process. I tried everything I could but failed.
Please help.

The following function will do the job:
# read a number of arguments on the left-hand side; those actual arguments; then the RHS
pipe() {
local nargs
local -a firstCmd
nargs=$1; shift
firstCmd=( )
for ((i=0; i<nargs; i++)); do
firstCmd+=( "$1" ); shift
done
"${firstCmd[#]}" | "$#"
}
# basic use case
pipe 3 ps -ef somethinghere grep java
# or, for a pipeline with more than two components:
pipe 3 ps -ef somethinghere pipe 2 grep java tee log.txt
What's better is that unlike a solution using eval, it'll work even with more complicated values:
pipe 3 ps -ef 'something with spaces here' grep java
One could also write a version of this function that uses a sigil value:
pipe() {
local sigil
local -a firstCmd
sigil=$1; shift
firstCmd=( )
while (( $# )); do
if [[ $1 = "$sigil" ]]; then
shift
"${firstCmd[#]}" | pipe "$sigil" "$#"
return
else
firstCmd+=( "$1" )
shift
fi
done
"${firstCmd[#]}"
}
In this case, you could even do:
sigil=$(uuidgen) # generate a random, per-session value
pipe "$sigil" ps -ef 'something with spaces here' "$sigil" grep java "$sigil" tee log.txt

If you really want to do that, here is a way:
pipe="|"
eval $(echo ps -ef $pipe grep java)
with cat:
eval $(echo ps -ef $(cat pipe.txt) grep java)
Note that using eval is discouraged and that this command will become problematic as soon as you need complex commands involving quotes, escape sequences, filenames and/or arguments with spaces, etc.
In my opinion, it would be better for you to familiarise yourself with how to change keyboard layouts on different linux systems (see loadkeys for example).

Building on the same logic as the first solution proposed by CharlesDuffy, this should be equivalent :
pipe()
{
"${#:2:$1}" | "${#:$(($1+2))}"
}
Rather than using iteration to build an array with the first command and shift arguments until the remaining ones contain only the second command, this solution uses expansions.
"${#:2:$1}" expands $1 arguments, starting at position 2
"${#:$(($1+2))}" expands all arguments starting at position $1 + 2.
In both cases, the double quotes ensure arguments expand as one word per argument (no word splitting being performed).
If you find this too cryptic, feel free to avoid it, as readability (to the intended coder(s) who would some day have to maintain the code) is likely to trump any advantage.

Related

Store a command in a variable; implement without `eval`

This is almost the exact same question as in this post, except that I do not want to use eval.
Quick question short, I want to execute the command echo aaa | grep a by first storing it in a string variable Command='echo aaa | grep a', and then running it without using eval.
In the post above, the selected answer used eval. That works for me too. What concerns me a lot is that there are plenty of warnings about eval below, followed by some attempts to circumvent it. However, none of them are able to solve my problem (essentially the OP's). I have commented below their attempts, but since it has been there for a long time, I suppose it is better to post the question again with the restriction of not using eval.
Concrete Example
What I want is a shell script that runs my command when I am happy:
#!/bin/bash
# This script run-this-if.sh runs the commands when I am happy
# Warning: the following script does not work (on nose)
if [ "$1" == "I-am-happy" ]; then
"$2"
fi
$ run-if.sh I-am-happy [insert-any-command]
Your sample usage can't ever work with an assignment, because assignments are scoped to the current process and its children. Because there's no reason to try to support assignments, things get suddenly far easier:
#!/bin/sh
if [ "$1" = "I-am-happy" ]; then
shift; "$#"
fi
This then can later use all the usual techniques to run shell pipelines, such as:
run-if-happy "$happiness" \
sh -c 'echo "$1" | grep "$2"' _ "$untrustedStringOne" "$untrustedStringTwo"
Note that we're passing the execve() syscall an argv with six elements:
sh (the shell to run; change to bash etc if preferred)
-c (telling the shell that the following argument is the code for it to run)
echo "$1" | grep "$2" (the code for sh to parse)
_ (a constant which becomes $0)
...whatever the shell variable untrustedStringOne contains... (which becomes $1)
...whatever the shell variable untrustedStringTwo contains... (which becomes $2)
Note here that echo "$1" | grep "$2" is a constant string -- in single-quotes, with no parameter expansions or command substitutions -- and that untrusted values are passed into the slots that fill in $1 and $2, out-of-band from the code being evaluated; this is essential to have any kind of increase in security over what eval would give you.

bash: Dump a parent call tree

I'm looking to instrument a recursive Makefile, and I want to see the complete calling tree that got me to where I'm at (which includes all parameters, etc). I don't care about the other processes on the system.
What I'm looking for is essentially pstree -ha, where it only outputs the highlighted parts (plus the current process). Notice that pstree -ha <PID> does not work, as it does not show the parent's parents for some reason (it does not go all the way up to init). I found another SO answer ps -f -g$BINOSPID, but it shows siblings, which I don't want.
To spell out what I'm looking for: I want is this:
~> sh
sh-4.1$ bash
~> pstree -?? $$
init
`- sshd
`- bash
`- sh
`- bash
`- pstree -?? 1402
Also, as a side question, pstree -ha automatically truncates the parameter list if it's to long. Is there a way to avoid that?
Implemented in native bash, depending on an operating system having procfs:
#!/usr/bin/env bash
pid=${1:-${BASHPID:-$$}}
while (( pid )); do
mutated=0
cmdline=( )
while IFS= read -r -d '' piece || { [[ $piece ]] && mutated=1; }; do
cmdline+=( "$piece" )
done <"/proc/$pid/cmdline"
printf '%s\t' "$pid"
if (( mutated )); then
printf '%s ' "${cmdline[#]}"
else
printf '%q ' "${cmdline[#]}"
fi
printf '\n'
stat_data=$(<"/proc/$pid/stat") || break
read _ ppid _ <<<"${stat_data##*')'}" || break
[[ $ppid = "$pid" ]] && break
pid=$ppid
done
The mutated flag is set if cmdline doesn't end with a NUL delimiter. This implies that the command line was modified by the program that was run, and no longer contains its original value. OpenSSH in particular tends to do this. In that case, rather than treating the command line as a NUL-separated list of literal arguments (and escaping those literals to generate output that could be copied-and-pasted to generate the same command when invoked), we treat it as something that has been formatted for readability by humans.
We're parsing "${stat_data##*')'}" out of paranoia, to prevent an application which contains literal spaces or ) characters in its executable name from throwing off the interpretation of /proc/$pid/stat.

How to recall a string in shell script

I made a script like this:
#! /usr/bin/bash
a=`ls ../wrfprd/wrfout_d0${i}* | cut -c22-25`
b=`ls ../wrfprd/wrfout_d0${i}* | cut -c27-28`
c=`ls ../wrfprd/wrfout_d0${i}* | cut -c30-31`
d=`ls ../wrfprd/wrfout_d0${i}* | cut -c33-34`
f=$a$b$c$d
echo $f
sed "s/.* startdate=.*/export startdate=${f}/g" ./post_process > post_process2
echo command works and gives 2008042118 that is what I want but in file post_process2 is like this export startdate= and can not recall variable f. I want to produce a line like export startdate=2008042118
First -- don't use ls here -- it's both expensive in terms of performance (compared to globbing, which is performed internal to the shell without starting any external programs), and doesn't guarantee useful output for the full range of possible filenames, making its use in this context inherently bug-prone. A better way to retrieve pieces from a filename, assuming a ksh-derived shell such as bash or zsh, would look like this:
#!/bin/bash
# this is an array, but we're only going to use the first element
file=( "../wrfprd/wrfout_d0${i}"* )
[[ -e $file ]] || { echo "No file found" >&2; exit 1; }
f=${file:22:4}${file:27:2}${file:30:2}${file:33:2}
Second, don't use sed to modify code -- doing so requires that your runtime user have permission to modify its own code, and moreover invites injection vulnerabilities. Just write your content out to a data file:
printf '%s\n' "$f" >startdate.txt
...and, in your second script, to read in the value from that file:
# if the shebang is #!/bin/bash
startdate=$(<startdate.txt)
# if the shebang is #!/bin/sh
startdate=$(cat startdate.txt)

Read file for value, loop until value = $foo?

I'm writing a shell script that greps for $foo then counts the number of occurrences then runs a command. Each time that command is run, there is one less instance of $foo in that file. Uncertain on how to continuously read that file and reduce the value in the variable I set.
$count= `grep -o $foo /some/file |wc -w`
until [ $count -eq 0 ]
do
some_command_that_deletes_foo_in_file
done
However I realize that $count is set once at runtime and is not updated. What I want is $count to be updated to the current count in /some/file while the script is looping through /some/file until there is 0 instances of the phrase I'm grepping for. Uncertain to what the best approach is.
Unless you have additional code that you haven't showed us that depends on $count, you don't actually need to count occurrences; you just need to know whether the string appears in the file. For that, you can write:
while grep -q -- "$foo" /some/file ; do
some_command_that_deletes_foo_in_file
done
(using the fact that grep returns success when it finds the value, and failure when it does not, and using the -q flag to suppress its console output).
You could add the grep command inside the loop:
count=$(grep -o "$foo" /some/file |wc -w)
until (( count == 0 ))
do
some_command_that_deletes_foo_in_file
count=$(grep -o "$foo" /some/file |wc -w)
done
You simply want to delete the string "$foo"? Use sed:
sed "/$foo/d" /some/file > /some/other/file
The sed command is an editor. The /$foo/ is taking a regular expression (whatever the value of $foo), finding it in the file. The d tells it to delete the line.
sed doesn't usually do an in place edit. You usually have to write to another file and then to a move. However, some sed commands may have such a parameter. You can check your manage.
Second Try
I think it must take some action or perform some processing or something, and one of its effects is that one of the $foos is gone. (But I could be wrong.) – ruakh yesterday
This is what I get answering these questions at night.
You can take advantage of the fact that grep returns true (i.e. exit value = 0) if it can find your regular expression and false (i.e. exit value not equal to 0) otherwise:
while grep -o -q "$foo" "/some/file"
do
some_command_that_deletes_foo_in_file
done
The -q tells grep not to output anything. Instead, the exit value of grep will be true if the string is found and false if it isn't.
Read your manpage on grep. Some grep commands don't have the -q parameter. In that case, you'l need to redirect both STDOUT and STDERR to /dev/null.
Another tact may be to do your count of the number of lines, and then use that as a counter:
count=$(grep -o "$foo" "/some/file" | wc -w) # $(...) is the preferred syntax over `...`
for loop in {1..$count}
do
some_command_that_deletes_foo_in_file
done
The advantage is that you only grep through the file once (which maybe a long operation). However, your count maybe incorrect if $foo is on more than one line.
A few notes:
The $(...) is preferred over backticks
When you set a variable, you don't use the dollar sign in front of that variable. Note I have count= and not $count=.
Watch spaces. count= $(...) is wrong because there's a space after the equals sign.

Passing multiple arguments to a UNIX shell script

I have the following (bash) shell script, that I would ideally use to kill multiple processes by name.
#!/bin/bash
kill `ps -A | grep $* | awk '{ print $1 }'`
However, while this script works is one argument is passed:
end chrome
(the name of the script is end)
it does not work if more than one argument is passed:
$end chrome firefox
grep: firefox: No such file or directory
What is going on here?
I thought the $* passes multiple arguments to the shell script in sequence. I'm not mistyping anything in my input - and the programs I want to kill (chrome and firefox) are open.
Any help is appreciated.
Remember what grep does with multiple arguments - the first is the word to search for, and the remainder are the files to scan.
Also remember that $*, "$*", and $# all lose track of white space in arguments, whereas the magical "$#" notation does not.
So, to deal with your case, you're going to need to modify the way you invoke grep. You either need to use grep -F (aka fgrep) with options for each argument, or you need to use grep -E (aka egrep) with alternation. In part, it depends on whether you might have to deal with arguments that themselves contain pipe symbols.
It is surprisingly tricky to do this reliably with a single invocation of grep; you might well be best off tolerating the overhead of running the pipeline multiple times:
for process in "$#"
do
kill $(ps -A | grep -w "$process" | awk '{print $1}')
done
If the overhead of running ps multiple times like that is too painful (it hurts me to write it - but I've not measured the cost), then you probably do something like:
case $# in
(0) echo "Usage: $(basename $0 .sh) procname [...]" >&2; exit 1;;
(1) kill $(ps -A | grep -w "$1" | awk '{print $1}');;
(*) tmp=${TMPDIR:-/tmp}/end.$$
trap "rm -f $tmp.?; exit 1" 0 1 2 3 13 15
ps -A > $tmp.1
for process in "$#"
do
grep "$process" $tmp.1
done |
awk '{print $1}' |
sort -u |
xargs kill
rm -f $tmp.1
trap 0
;;
esac
The use of plain xargs is OK because it is dealing with a list of process IDs, and process IDs do not contain spaces or newlines. This keeps the simple code for the simple case; the complex case uses a temporary file to hold the output of ps and then scans it once per process name in the command line. The sort -u ensures that if some process happens to match all your keywords (for example, grep -E '(firefox|chrome)' would match both), only one signal is sent.
The trap lines etc ensure that the temporary file is cleaned up unless someone is excessively brutal to the command (the signals caught are HUP, INT, QUIT, PIPE and TERM, aka 1, 2, 3, 13 and 15; the zero catches the shell exiting for any reason). Any time a script creates a temporary file, you should have similar trapping around the use of the file so that it will be cleaned up if the process is terminated.
If you're feeling cautious and you have GNU Grep, you might add the -w option so that the names provided on the command line only match whole words.
All the above will work with almost any shell in the Bourne/Korn/POSIX/Bash family (you'd need to use backticks with strict Bourne shell in place of $(...), and the leading parenthesis on the conditions in the case are also not allowed with Bourne shell). However, you can use an array to get things handled right.
n=0
unset args # Force args to be an empty array (it could be an env var on entry)
for i in "$#"
do
args[$((n++))]="-e"
args[$((n++))]="$i"
done
kill $(ps -A | fgrep "${args[#]}" | awk '{print $1}')
This carefully preserves spacing in the arguments and uses exact matches for the process names. It avoids temporary files. The code shown doesn't validate for zero arguments; that would have to be done beforehand. Or you could add a line args[0]='/collywobbles/' or something similar to provide a default - non-existent - command to search for.
To answer your question, what's going on is that $* expands to a parameter list, and so the second and later words look like files to grep(1).
To process them in sequence, you have to do something like:
for i in $*; do
echo $i
done
Usually, "$#" (with the quotes) is used in place of $* in cases like this.
See man sh, and check out killall(1), pkill(1), and pgrep(1) as well.
Look into pkill(1) instead, or killall(1) as #khachik comments.
$* should be rarely used. I would generally recommend "$#". Shell argument parsing is relatively complex and easy to get wrong. Usually the way you get it wrong is to end up having things evaluated that shouldn't be.
For example, if you typed this:
end '`rm foo`'
you would discover that if you had a file named 'foo' you don't anymore.
Here is a script that will do what you are asking to have done. It fails if any of the arguments contain '\n' or '\0' characters:
#!/bin/sh
kill $(ps -A | fgrep -e "$(for arg in "$#"; do echo "$arg"; done)" | awk '{ print $1; }')
I vastly prefer $(...) syntax for doing what backtick does. It's much clearer, and it's also less ambiguous when you nest things.

Resources