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
$cmddoes not behave identically to running a command withcmd's contents is described in BashFAQ #50. - Also see BashFAQ #48 describing why
evalis 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$varwithsuffixremoved. - In
ls *.txt, it's notlsthat evaluates the*.txtglob: The shell itself does that, before starting thelsexecutable. As such, there's no point to runninglsat 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'
