I attempted to execute this command:
#!/bin/sh
cmd="ls *.txt | sed s/.txt//g"
for i in `$cmd`
do
echo $i
echo "Hello world"
done
I.e. get things with .txt extension in the current working directory and remove the .txt part from their files and print it out. However, I am getting this error and it appears that ls is interpretting "|" and "sed" and "s/.txt//g" as literal file names:
ls: s/.txt//g: No such file or directory
ls: sed: No such file or directory
ls: |: No such file or directory
If I pass the command as, for i in ls *.txt | sed s/.txt//g
, this is not a problem. Can I get some suggestions?
CodePudding user response:
Several points:
- If you want this to be a bash script, not a sh script, you need to change the shebang to invoke bash.
- The immediate reason evaluating
$cmd
does not behave identically to running a command withcmd
's contents is described in BashFAQ #50. - Also see BashFAQ #48 describing why
eval
is prone to causing security risks. - The shell has its own built-in string substitution behavior; there's no reason to use
sed
. See the bash-hackers' wiki page on parameter expansion describing how${var%suffix}
expands to the contents of$var
withsuffix
removed. - In
ls *.txt
, it's notls
that evaluates the*.txt
glob: The shell itself does that, before starting thels
executable. As such, there's no point to runningls
at all: It's the shell's built-in functionality that generates the list of filenames, so you might as well just use that list.
#!/usr/bin/env bash
for i in *.txt; do i=${i%.txt}
echo "$i"
echo "Hello world"
done
CodePudding user response:
The basic issue is the sequence of expansion (see EXPANSION in man bash):
The order of expansions is: brace expansion, tilde expansion, parameter, variable and arithmetic expansion and command substitution (done in a left-to-right fashion), word splitting, and pathname expansion.
In other words, the command substitution already assumed that the original line was split into individual commands, and it will not recognize '|', ';', and other bash keywords.
If the command is constant, the simple solution is to inline it:
for i in `ls *.txt | sed s/.txt//g`
From the OP, looks like the command has to be variable. In this case, one can use 'eval' to force re-parsing of the variable into the pipeline:
for i in `eval $cmd`
Make sure to read all the warning about using "eval", and constructing commands on the fly.
Minor comment: In sed
the command 's/.txt//g' assumes that .txt
is regular expression. Therefore, it will change btxt.txt
to .txt
. Make sure to escape the '.' so that it will be treated as literal.
cmd="ls *.txt | sed -e 's/\.txt//g'