252

For example, right now I'm using the following to change a couple of files whose Unix paths I wrote to a file:

cat file.txt | while read in; do chmod 755 "$in"; done

Is there a more elegant, safer way?

barlop
  • 12,887
  • 8
  • 80
  • 109
hawk
  • 2,655
  • 2
  • 16
  • 11

9 Answers9

247

Read a file line by line and execute commands: 4+ answers

Because the main usage of (and/or ) is to run other commands, there is not only 1 answer!!

    0. Shell command line expansion
    1. xargs dedicated tool
    2. while read with some remarks
    3. while read -u using dedicated fd, for interactive processing (sample)
    5. running with inline generated script

Regarding the OP request: running chmod on all targets listed in file, xargs is the indicated tool. But for some other applications, small amount of files, etc...

0. Read entire file as command line argument.

If

  • your file is not too big (tested on my host with 128Mb file, with more than 10'000'000 lines) and
  • all files are well named (without spaces or other special chars like quotes),

you could use shell command line expansion. Simply:

chmod 755 $(<file.txt)

This command is the simplier one.

1. xargs is the right tool

For

  • bigger amount of files, or almost any number of lines in your input file...
  • files holding names that could contain spaces or special characters

For many binutils tools, like chown, chmod, rm, cp -t ...

xargs chmod 755 <file.txt

Could be used after a pipe on found files by find:

find /some/path -type f -uid 1234 -print | xargs chmod 755

If you have special chars and/or a lot of lines in file.txt.

xargs -0 chmod 755 < <(tr \\n \\0 <file.txt)

find /some/path -type f -uid 1234 -print0 | xargs -0 chmod 755

If your command need to be run exactly 1 time for each entry:

xargs -0 -n 1 chmod 755 < <(tr \\n \\0 <file.txt)

This is not needed for this sample, as chmod accepts multiple files as arguments, but this matches the title of question.

For some special cases, you could even define the location of the file argument in commands generated by xargs:

xargs -0 -I '{}' -n 1 myWrapper -arg1 -file='{}' wrapCmd < <(tr \\n \\0 <file.txt)

Test with seq 1 5 as input

Try this:

xargs -n 1 -I{} echo Blah {} blabla {}.. < <(seq 1 5)
Blah 1 blabla 1..
Blah 2 blabla 2..
Blah 3 blabla 3..
Blah 4 blabla 4..
Blah 5 blabla 5..

where your command is executed once per line.

IMPORTANT PREAMBLE BEFORE CHAPTER 2.

Doing loop under is generally a bad idea! There is a lot of warning about doing loop under shell!

Before doing loop, think parallelisation and dedicated tools!!

You could use for interact with and administrate dedicated tools. Some samples:

2. while read and variants.

For this, make sure to end the file with a newline character.

As OP suggests,

cat file.txt |
while read in; do
    chmod 755 "$in"
done

will work, but there are 2 issues:

  • cat | is a useless fork, and

  • | while ... ;done will become a subshell whose environment will disappear after ;done.

So this could be better written:

while read in; do
    chmod 755 "$in"
done < file.txt

But

  • You may be warned about $IFS and read flags:

help read

read: read [-r] ... [-d delim] ... [name ...]
    ...
    Reads a single line from the standard input... The line is split
    into fields as with word splitting, and the first word is assigned
    to the first NAME, the second word to the second NAME, and so on...
    Only the characters found in $IFS are recognized as word delimiters.
    ...
    Options:
      ...
      -d delim   continue until the first character of DELIM is read, 
                 rather than newline
      ...
      -r do not allow backslashes to escape any characters
    ...
    Exit Status:
    The return code is zero, unless end-of-file is encountered...

In some cases, you may need to use

while IFS= read -r in;do
    chmod 755 "$in"
done <file.txt

for avoiding problems with strange filenames. And maybe if you encounter problems with UTF-8:

while LANG=C IFS= read -r in ; do
    chmod 755 "$in"
done <file.txt

While you use a redirection from standard inputfor reading file.txt`, your script cannot read other input interactively (you cannot use standard input for other input anymore).

3. while read, using dedicated fd.

Syntax: while read ...;done <file.txt will redirect standard input to come from file.txt. That means you won't be able to deal with processes until they finish.

This will let you use more than one input simultaneously, you could merge two files (like here: scriptReplay.sh), or maybe:

You plan to create an interactive tool, you have to avoid use of standard input and use some alternative file descriptor.

Constant file descriptors are:

  • 0 for standard input
  • 1 for standard output
  • 2 for standard error.

3.1 first

You could see them by:

ls -l /dev/fd/

or

ls -l /proc/$$/fd/

From there, you have to choose unused numbers between 0 and 63 (more, in fact, depending on sysctl superuser tool) as your file descriptor.

For this demo, I will use file descriptor 7:

while read <&7 filename; do
    ans=
    while [ -z "$ans" ]; do
        read -p "Process file '$filename' (y/n)? " foo
        [ "$foo" ] && [ -z "${foo#[yn]}" ] && ans=$foo || echo '??'
    done
    if [ "$ans" = "y" ]; then
        echo Yes
        echo "Processing '$filename'."
    else
        echo No
    fi
done 7<file.txt

If you want to read your input file in more differents steps, you have to use:

exec 7<file.txt      # Without spaces between `7` and `<`!
# ls -l /dev/fd/

read <&7 headLine
while read <&7 filename; do
    case "$filename" in
        *'----' ) break ;;  # break loop when line end with four dashes.
    esac
    ....
done

read <&7 lastLine

exec 7<&-            # This will close file descriptor 7.
# ls -l /dev/fd/

3.2 Same under

Under , you could let him choose any free fd for you and store into a variable:
exec {varname}</path/to/input:

while read -ru ${fle} filename;do
    ans=
    while [ -z "$ans" ]; do
        read -rp "Process file '$filename' (y/n)? " -sn 1 foo
        [ "$foo" ] && [ -z "${foo/[yn]}" ] && ans=$foo || echo '??'
    done
    if [ "$ans" = "y" ]; then
        echo Yes
        echo "Processing '$filename'."
    else
        echo No
    fi
done {fle}<file.txt

Or

exec {fle}<file.txt
# ls -l /dev/fd/
read -ru ${fle} headline

while read -ru ${fle} filename;do
    [[ -n "$filename" ]] && [[ -z ${filename//*----} ]] && break
    ....
done

read -ru ${fle} lastLine

exec {fle}<&-
# ls -l /dev/fd/

5. filtering input file for creating commands

sed <file.txt 's/.*/chmod 755 "&"/' | sh

This won't optimise forks, but this could be usefull for more complex (or conditional) operation:

sed <file.txt 's/.*/if [ -e "&" ];then chmod 755 "&";fi/' | sh

sed 's/.*/[ -f "&" ] \&\& echo "Processing: \\"&\\"" \&\& chmod 755 "&"/' \
    file.txt | sh
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • 3
    As `xargs` was initialy build for answering this kind of need, some features, like *building command as long as possible in the current environment* for invoking `chmod` in this case as less as possible, reducing *forks* ensure efficience. `while ;do..done <$file` implie running 1 fork for 1 file. `xargs` could run 1 fork for thousand files... in a reliable manner. – F. Hauri - Give Up GitHub Dec 19 '12 at 01:20
  • 1
    why does the third command not work in a makefile? i'm getting "syntax error near unexpected token `<'", but executing straight from the command line works. – Woodrow Barlow Sep 28 '15 at 01:01
  • 2
    This seem linked to Makefile specific syntax. You could try to reverse the command line: `cat file.txt | tr \\n \\0 | xargs -0 -n1 chmod 755` – F. Hauri - Give Up GitHub Sep 28 '15 at 05:27
  • @F.Hauri for some reason, `tr \\n \\0 – phil294 Aug 19 '17 at 10:07
  • October 2019, new edit, add ***interactive*** file processor sample. – F. Hauri - Give Up GitHub Oct 06 '19 at 08:35
  • If `xargs` only seems to process the first line of your input file, the idea of *creating as long a command line as possible* may be inappropriate. Try using `xargs -l` (lowercase 'L') to see if it fixes your problem, as it did for me. – FKEinternet Nov 12 '19 at 20:20
  • I couldn't remember `xargs` these past two days, so since I'm processing the line via `sed` (i needed to cut out a specific part of it anyway), I just put the command line in the sed output and piped it all to bash again. `somecmd | sed -re 's#.*#chmod 755 \0#' | bash`. This doesn't stop on error like xargs. – coladict Jul 28 '21 at 06:35
  • @coladict .1 you could use `xargs -n1` to ensure all xargs step is done for 1 element. .2 you could avoid error by perfecting "`somecmd`*, but mostly 3. Use `| sh` instead of `| bash`, because if your last command doesn't require bashisms, `dash` could be a lot quicker. – F. Hauri - Give Up GitHub Jul 28 '21 at 06:45
  • I tried to clean up the formatting, but I'm not sure I understand why you have struck out some parts of the code. It looks like you are retracting or not recommending that code, but I'm guessing that's not what you are trying to communicate. Perhaps you could take it out or replace it, and reword or perhaps just remove the explanation of why the formatting is different, which I could not understand. – tripleee Apr 09 '22 at 12:36
  • @tripleee I agree, this is not clear... Using `exec 7 – F. Hauri - Give Up GitHub Apr 09 '22 at 16:58
  • 1
    @tripleee Answer edited... – F. Hauri - Give Up GitHub Apr 12 '22 at 14:03
  • @coladict Rhght! In some case `sed | sh` could permit more complex process. – F. Hauri - Give Up GitHub Jun 22 '22 at 10:28
196

Yes.

while read in; do chmod 755 "$in"; done < file.txt

This way you can avoid a cat process.

cat is almost always bad for a purpose such as this. You can read more about Useless Use of Cat.

tripleee
  • 175,061
  • 34
  • 275
  • 318
P.P
  • 117,907
  • 20
  • 175
  • 238
  • Avoid *one* `cat` is a good idea, but in this case, **the** indicated command is `xargs` – F. Hauri - Give Up GitHub Dec 18 '12 at 21:05
  • That link doesn't seem to be relevant, perhaps the content of the web page has changed? The rest of the answer is awesome though :) – starbeamrainbowlabs Apr 21 '15 at 19:06
  • @starbeamrainbowlabs Yes. It seems page has been moved. I have re-linked and should be ok now. Thanks :) – P.P Apr 21 '15 at 19:12
  • 1
    Thanks! This was helpful, especially when you need to do something else than calling `chmod` (i.e. really run one command for each line in the file). – Per Lundberg Sep 03 '15 at 07:24
  • **be careful with backslashes!** from http://unix.stackexchange.com/a/7561/28160 - "`read -r` reads a single line from standard input (`read` without `-r` interprets backslashes, you don't want that)." – That Brazilian Guy Jun 10 '16 at 16:28
  • 1
    while this might be more intuitive, a shell loop to process text is [**dramatically slow** and bad practice](https://unix.stackexchange.com/questions/169716/why-is-using-a-shell-loop-to-process-text-considered-bad-practice). I just measured echoing a sample file: In comparison to the accepted answer, this is 18 TIMES SLOWER. – phil294 Aug 19 '17 at 10:15
23

if you have a nice selector (for example all .txt files in a dir) you could do:

for i in *.txt; do chmod 755 "$i"; done

bash for loop

or a variant of yours:

while read line; do chmod 755 "$line"; done < file.txt
d.raev
  • 9,216
  • 8
  • 58
  • 79
  • 2
    What doesn't work is that if there's spaces in the line, input is split by spaces, not by line. – Michael Fox Feb 26 '16 at 16:45
  • @Michael Fox : Lines with spaces can be supported by changing the separator. To change it to newlines, set the 'IFS' environment variable before the script/command. Ex: export IFS='$\n' – codesniffer Sep 14 '18 at 22:58
  • Typo in my last comment. Should be: export IFS=$'\n' – codesniffer Sep 21 '18 at 05:35
  • @codesniffer You don't need the `export` here. Its purpose is to make the variable visible to subprocesses (so, useful if you want to change the separator in subshells started from the current one, but not really relevant or useful here). – tripleee Apr 09 '22 at 12:26
19

If you want to run your command in parallel for each line you can use GNU Parallel

parallel -a <your file> <program>

Each line of your file will be passed to program as an argument. By default parallel runs as many threads as your CPUs count. But you can specify it with -j

janisz
  • 6,292
  • 4
  • 37
  • 70
16

If you know you don't have any whitespace in the input:

xargs chmod 755 < file.txt

If there might be whitespace in the paths, and if you have GNU xargs:

tr '\n' '\0' < file.txt | xargs -0 chmod 755
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • I know about xargs, but (sadly) it seems less of a reliable solution than bash built-in features like while and read. Also, I don't have GNU xargs, but I am using OS X and xargs also has a -0 option here. Thanks for the answer. – hawk Dec 19 '12 at 00:54
  • 1
    @hawk No: `xargs` is robust. This tool is very old and his code is strongly *revisited*. His goal was initialy to build lines in respect of shell limitations (64kchar/line or something so). Now this tool could work with very big files and may reduce a lot the number of fork to final command. See [my answer](http://stackoverflow.com/a/13941223/1765658) and/or `man xargs`. – F. Hauri - Give Up GitHub Dec 10 '13 at 07:40
  • @hawk Less of a reliable solution in which way? If it works in Linux, Mac/BSD and Windows (yes, MSYSGIT's bundles GNU xargs), then it's as reliable as it gets. – Camilo Martin Feb 24 '15 at 03:30
  • 1
    For those coming still finding this from search results... you can install GNU xargs on macOS by using Homebrew (`brew install findutils`), and then invoke GNU xargs with `gxargs` instead, e.g. `gxargs chmod 755 < file.txt` – Jase Mar 24 '19 at 02:01
  • `xargs` itself is robust, but you have to understand how it handles (or fails to handle) quotes etc in your input. The workaround with `xargs -0` is completely predictable and robust, but regrettably specific to GNU `xargs`. – tripleee Apr 09 '22 at 12:31
6

Now days (In GNU Linux) xargs still the answer for this, but ... you can now use the -a option to read directly input from a file:

xargs -a file.txt -n 1 -I {} chmod 775 {}

Raydel Miranda
  • 13,825
  • 3
  • 38
  • 60
  • 1
    This is the only answer that worked for me, thanks. Can't believe someone wrote like 500 lines just for a useless answer – Mdev Nov 19 '21 at 01:38
  • Just a heads up for anyone who comes across this, the version of xargs that comes with macOS doesn't take the -a argument – OneHoopyFrood Mar 14 '22 at 20:50
  • `xargs -a` in a GNU extension, which means it typically works on Linux out of the box, but not so much anywhere else unless you separately installed the GNU versions of many common utilities. The standard solution to read file names from standard input continues to work portably across GNU and other versions of `xargs`. – tripleee Apr 09 '22 at 12:23
5

You can also use AWK which can give you more flexibility to handle the file

awk '{ print "chmod 755 "$0"" | "/bin/sh"}' file.txt

if your file has a field separator like:

field1,field2,field3

To get only the first field you do

awk -F, '{ print "chmod 755 "$1"" | "/bin/sh"}' file.txt

You can check more details on GNU Documentation https://www.gnu.org/software/gawk/manual/html_node/Very-Simple.html#Very-Simple

brunocrt
  • 720
  • 9
  • 11
3

I see that you tagged bash, but Perl would also be a good way to do this:

perl -p -e '`chmod 755 $_`' file.txt

You could also apply a regex to make sure you're getting the right files, e.g. to only process .txt files:

perl -p -e 'if(/\.txt$/) `chmod 755 $_`' file.txt

To "preview" what's happening, just replace the backticks with double quotes and prepend print:

perl -p -e 'if(/\.txt$/) print "chmod 755 $_"' file.txt
1.618
  • 1,765
  • 16
  • 26
0

The logic applies to many other objectives. And how to read .sh_history of each user from /home/ filesystem? What if there are thousand of them?

#!/bin/ksh
last |head -10|awk '{print $1}'|
 while IFS= read -r line
 do
su - "$line" -c 'tail .sh_history'
 done

Here is the script https://github.com/imvieira/SysAdmin_DevOps_Scripts/blob/master/get_and_run.sh

BladeMight
  • 2,670
  • 2
  • 21
  • 35