Home > OS >  ls command with pipes not working when passed into a while loop
ls command with pipes not working when passed into a while loop

Time:10-18

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 with cmd'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 with suffix removed.
  • In ls *.txt, it's not ls that evaluates the *.txt glob: The shell itself does that, before starting the ls executable. As such, there's no point to running ls 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'
  • Related