Dropping any arguments from a string representing a command line in bash - bash

I need to write a bash function which, given a string that represents a command line, returns just the first token in the command line (i.e. the program being called, which may have spaces in its name), dropping any arguments. I want to do this without using sed or awk or anything but bash builtins and variable manipulation.
e.g.:
drop_args "ls" # prints "ls"
drop_args "ls -al" # prints "ls"
drop_args "spaces\ in\ name --bad-idea" # prints "spaces\ in\ name"
What I've tried is:
drop_args () { echo $1; }
and then I call drop_args ls -al, i.e. without quoting the string, and that works nicely for all cases that I can see except for drop_args spaces\ in\ name.
I'm not too terribly concerned if I can't do this in a way that correctly handles the spaces case with the restrictions I have stipulated, but I need to at least reliably detect that situation and display an appropriate error message, I guess.
This is related to my earlier question about dereferencing shell aliases; it's sort of a subproblem of what I am ultimately trying to accomplish there.

drop_args()
{
read cmd args <<< "$1"
echo "$cmd"
}
This loses the backslashes, but they were never really meant to be "there" in the first place.

Related

how to explicitly expand ~ and other variables inside function?

I am trying to write a bash function that takes command as the first variable and output file as the second variable:
my_func() {
$1 ${SOME_OTHER_PARAMS} |
tee $2
}
when I run my_func "python print_hello_to_a_file ~/out.txt" "second_file.txt", it seems to output "hello" to "~/out.txt", not "out.txt" in my home directory, meaning that "~" was not expanded correctly.
I am wondering if it is possible to correctly expand "~" inside this function?
Possible? Yes, but probably not a good idea.
The basic problem: When parsing the command line, Tilde Expansion happens before Parameter Expansion. This means you can't put a tilde inside a variable and have it be replaced by a path to your home directory in the simplest case.
Minimal demo:
[user#host]$ myvar="~"
[user#host]$ echo $myvar
~
[user#host]$ echo ~
/home/user
One possible solution is to use eval to force a second round of parsing before executing the command.
[user#host]$ eval echo $myvar
/home/user
But eval is VERY DANGEROUS and you should not use it without exhausting all other possibilities. Forcing a second parsing of the command line can result in unexpected, confusing, and potentially even unsafe results if you are not extremely familiar with the parsing rules and take sufficient steps to sanitize your inputs before running them through eval.
The more standard solution is to build up your command inside a bash array.
my_func() {
tee_output="${1}"
shift
# expand the inputs all together
# SOME_OTHER_PARAMS should be an array as well
"${#}" "${SOME_OTHER_PARAMS[#]}" | tee "${tee_output}"
}
# build the command up as an array with each word as its own element
# Tilde expansion will occur here and the result will be stored for later
my_command=( "python" "print_hello_to_a_file" ~/"out.txt" )
# expand the array and pass to my_func
# Important that the tee_location goes first here, as we
# shift it off to capture the remaining arguments as a complete command
my_func "${tee_loc}" "${my_command[#]}"
But my_func still only supports simple commands with this approach - no loops or if/case statements, no file redirections, etc. This might be okay if your goal is just to decorate a variety of commands with extra parameters and tee their output somewhere.

Concatenating a string inside a while loop in bash

I am trying to write a program in bash which takes as input argument a file name and then takes every line of that file and appends it to a string. At the end, I want to print this string. This is what I wrote (consider that the program name is concat.sh):
#!/bin/bash
FILE=$1
STR=""
cat $FILE | while read USER; do
STR="${STR}${USER}"
done
echo "$STR"
And I have the file stuff.txt, which has the following:
a
b
c
And after I run ./concat.sh stuff.txt I get the empty string. I tried multiple variations of that concatenation, with space, without space (like above), with newline, etc. Still doesn't work. If I try to simply print each line, it works. So if I simply add the line echo "$USER" inside the loop I get the correct output, i.e., a, b, c (each on a different line). But that string concatenation still doesn't work. It doesn't work even if I stop using that $USER variable in the concatenation. So if I do something like STR="${STR} abc" the characters abc are surprisingly not concatenated to STR. But if I take this outside of the while loop it actually works (so if I comment the while loop and simply do STR="${STR} abc" I will get abc in the string STR). I am a newbie in bash and this looks like really weird behaviour to me and have no idea what is going on/how to fix it.
Just do not use cat - ie. do not use pipe. And do not use USER.
while read var; do
str="${str}${var}"
done < "$file"
Do not use upper case variables in your scripts. USER is variable set by bash to the name of current user.
Check scripts with http://shellcheck.net
Read https://mywiki.wooledge.org/BashFAQ/024
Read https://mywiki.wooledge.org/BashFAQ/001
Do not uselessly use cat.
In bash you can also use str+="$var".
Quote variable expansions.

What's the use of \$$ in bash?

I found this as a suggestion of how to store the output of "eval" into a variable called line. So, what's the use of \$$?
command = "some command"
line = $(eval \$$command)
The \$ prevents the shell from trying to treat the $ as the beginning of a parameter expansion. However, the code as a whole doesn't do anything useful. After fixing the whitespace issues and adding a real command to the example, your code looks like
command="ls -l"
line=$(eval \$$command)
command is simply a string ls -l. To evaluate the next line, the shell first evaluates the command substitution. The first step is to expand the parameter command, yielding line=$(eval \$ls -l). Quote removal gets rid of the backslash, so eval receives the arguments $ls and -l. Since ls presumably is not a variable, $ls is expanded to the empty string, and eval is left simply with -l to execute. There being no such command, you get an error.
You might think, then, that the correct form is simply
line=$(eval $command)
or slightly better
line=$(eval "$command")
That will work for simple cases, but not in general. This has been hashed over many times in many questions; see Bash FAQ 50, "I'm trying to put a command in a variable, but the complex cases always fail!" for the details.
To answer the literal question, though, \$$ is useful for outputing the string $$, instead of expanding it to the current process ID:
# The exact output will vary
$ echo $$
86542
# Literal quotes
$ echo \$\$
$$
# Escaping either quote is sufficient
$ echo \$$ $\$
$$ $$

Ksh ls -1 not working as expected

For the command ls, the option -1 is supposed to do run ls yielding an output with only one column (yielding one file per line). But if I put it in a script, it just shows every file jammed on one line, separated with spaces.
Script:
#!/bin/ksh
text=`ls -1`
echo $text
Folder contents:
test
|--bob
|--coolDir
|--file
|--notThisDirectoryAgain
|--script.sh
|--spaces are cool
|--thatFile
Script Output:
bob coolDir file notThisDirectoryAgain script.sh spaces are cool thatFile
Output if I run ls -1 in the terminal (not in a script)
bob
coolDir
file
notThisDirectoryAgain
script.sh
spaces are cool
thatFile
it just shows every file jammed on one line, separated with spaces.
You have to consider what it is.
When you do
text=`ls -1`
that runs the program ls and presents the output as if you typed it in. So the shell gets presented with:
ls=file1
file2
file3
etc.
The shell splits command-line tokens on whitespace, which by default includes a space, a tab, and a newline. So each filename is seen as a separate token by the shell.
These tokens are then passed into echo as separate parameters. The echo command is unaware that the newlines were ever there.
As I'm sure you know, all echo does is to write each parameter to stdout, with a space between each one.
This is why the suggestion given by #user5228826 works. Change IFS if you don't want a newline to separate tokens.
However, all you really had to do is to enclose the variable in quotes, so that it didn't get split:
echo "$text"
By the way, using `backticks` is deprecated and poor practice because it can be difficult to read, particularly when nested. If you run ksh -n on your script it will report this to you (assuming you are not using an ancient version). Use:
text=$(ls -1)
Having said all that, this is a terrible way to get a list of files. UNIX shells do globbing, this is an unnecessary use of the ls external program. Try:
text=(*) # Get all the files in current directory into an array
oldIFS="$IFS" # Save the current value of IFS
IFS=$'\n' # Set IFS to a newline
echo "${text[*]}" # Join the elements of the array by newlines, and display
IFS="$oldIFS" # Reset IFS to previous value
That's because you're capturing ls output into a variable. Bash does the same.

bash: passing paths with spaces as parameters?

I have a bash script that recieves a set of files from the user. These files are sometimes under directories with spaces in their names. Unfortunately unlike this question all the filenames are passed via the command line interface. Let's assume the paths are correctly quoted as they are passed in by the user, so spaces (save for quoted spaces) are delimiters between paths. How would I forward these parameters to a subroutine within my bash script in a way that preserves the quoted spaces?
#! /bin/bash
for fname in "$#"; do
process-one-file-at-a-time "$fname"
done
Note the excessive use of quotes. It's all necessary.
Passing all the arguments to another program is even simpler:
process-all-together "$#"
The tricky case is when you want to split the arguments in half. That requires a lot more code in a simple POSIX shell. But maybe the Bash has some special features.
You want "$#", which has the special syntax of expanding $# but preserving the white-space quoting of the caller (it does not create a single giant string with all the arguments in it). So someone can call your script like:
bash-script.sh AFile "Another File With Spaces"
Then in your script you can do things like:
for f in "$#"; do
echo "$f";
done
and get two lines of output (not 5).
Read the paragraph about the Special Parameter "#" here: http://www.gnu.org/s/bash/manual/bash.html#Special-Parameters
Bravo #Roland . Thans a lot for your solution
It has really worked!
I wrote a simple script function that opens a given path with nautilus.
And I've just nested a function with this "helper"-for-loop into the main function:
fmp () {
fmp2() {
nautilus "$#";
};
for fname in "$#";
do
fmp2 "$fname";
done;
}
Now I'm able to make all my scripts work handling with paths just by turning them into nested functions wrapped by a function with this helper-for-loop.
"$var"
For example,
$ var='foo bar'
$ perl -E'say "<<$_>>" for #ARGV' $var
<<foo>>
<<bar>>
$ perl -E'say "<<$_>>" for #ARGV' "$var"
<<foo bar>>

Resources