8

I have a .txt file that contains a text like this

A1/B1/C1
A2/B2/C2 
A3/B3/C3

I want a script that reads the .txt file for each line then create a directory based on the first word (A1, A2, A3)

I have created script like this:

file="test.txt"
while IFS='' read -r line
do
    name="line"
    mkdir -p $line
done <"$file"

While I run it, it creates directory A1 then it also create sub-directories B1 and C1. the same happens for another line (A2* and A3*)

What should I do to create only A1, A2, A3 directories?

I don't want to make the name like A1/B1/C1 with '/' character in it. I just want to take the word before '/' character and make it directory name. Just "A1" "A2" "A3".

αғsнιη
  • 35,660
maulana
  • 103

6 Answers6

14

You can just cut the 1st slash-delimited field of each line and give the list to mkdir:

mkdir $(<dirlist.txt cut -d/ -f1)

Example run

$ cat dirlist.txt 
A1/A2/A3
B1/B2/B3
C1/C2/C3
$ ls
dirlist.txt
$ mkdir $(<dirlist.txt cut -d/ -f1)
$ ls
A1  B1  C1  dirlist.txt

You may run into ARG_MAX problems if your list holds a huge number of directory names, in this case use GNU parallel Install parallel or xargs as follows:

parallel mkdir :::: <(<dirlist.txt cut -d/ -f1)
xargs -a<(<dirlist.txt cut -d/ -f1) mkdir

While parallel has it covered, the xargs approach won’t work if the directory names contain spaces – you can either use \0 as the line delimiter or simply instruct xargs to only split the input on newline characters (as proposed by Martin Bonner) instead:

xargs -0a<(<dirlist.txt tr \\{n,0} | cut -d/ -f1 -z) mkdir # \\{n,0} equals \\n \\0
xargs -d\\n -a<(<dirlist.txt cut -d/ -f1) mkdir

In case any of the fields contains a newline character one would need to identify the “true” line endings and replace only those newline characters with e.g. \0. That would be a case for awk, but I feel it’s too far fetched here.

dessert
  • 39,982
  • Why xargs -a<(....) rather than <dirlist.txt cut -d/ -f1 | xargs? – Martin Bonner supports Monica May 24 '18 at 16:25
  • 1
    @MartinBonner Thanks for clarifying, that’s indeed simpler than my tr approach – just realized that parallel has it covered by default. – dessert May 24 '18 at 20:48
  • @MartinBonner Why xargs -a<(....) rather than a pipe – because I like it this way, as simple as that. :) – dessert May 24 '18 at 20:51
  • @AndreaCorbellini Oh, now I understand. <(...) helps with spaces as well, so it’s definitely the better choice – I don’t think anybody was using this file descriptor anyway. – dessert May 24 '18 at 20:55
  • Did you intentionally obfuscate tr "\n" "\0" to that tr \\{n,0}? – ilkkachu May 25 '18 at 09:43
  • @ilkkachu How is that obfuscating? I think it’s actually more clear, it shows the two comma-separated next to each other – brace expansion is quite common and easy to understand, isn’t this even a C shell feature? – dessert May 25 '18 at 10:12
  • @dessert, sure, and tr "\n" "\0" or tr \\n \\0 shows two arguments next to each other, space separated. In exactly the usual way to write command line arguments. Brace expansion may be common, yes, but it still brings up questions from time to time so it's not like it's immediately obvious to everyone in every case. And it only saves one character here. In the same vein, you could have written cut -{d/,f1,z}. It would also show the flags comma-separated next to each other. – ilkkachu May 25 '18 at 10:32
  • @ilkkachu That’s a nice idea, but arguments are not options. It’s a matter of taste in the end, one could easily call -d/ obfuscated as well and advertise -d "/" or -d '/' instead, or frown upon run-together-options and always type ls -a -l. But if you feel this way, let me add an explanatory comment… I like to throw in unorthodox things like that in my answers just to show what’s possible. I always answer comments asking for explanations, normally extending my answer. – dessert May 25 '18 at 11:37
12

You need to set your IFS='/' for read and then assign each first field into separate variable first and the rest of the fields into variable rest and just work on first field's value (Or you can read -ar array to a single array and use "${array[0]}" for the first field's value. ):

while IFS='/' read -r first rest;
do
    echo mkdir -- "$first" 
done < test.txt

###Or in single line for those who like it:

<test.txt xargs -d'\n' -n1 sh -c 'echo mkdir -- "$'{1%%/*}'"' _

###Or create all directories in one-go:

<test.txt xargs -d'\n' bash -c 'echo mkdir -- "$'{@%%/*}'"' _

The ANSI-C Quoting $'...' is used to deal with directory names containing special characters.

Note that the _ (can be any character or string) at the end will be argv[0] to the bash -c '...' and the $@ will contains rest of the parameters starting from 1; without that in second command the first parameter to the mkdir will be lost.

In ${1%%/*} using shell (POSIX sh/bash/Korn/zsh) parameter substitution expansion, removes longest possible match of a slash followed by anything till the end of the parameter passing into it which is a line that read by xargs;

P.s:

  • Remove echo in front of the mkdir to create those directories.
  • Replace -d'\n' with -0 if your list is separated with NUL characters instead of a \newline (supposed there is/are embedded newline in your directory name).
αғsнιη
  • 35,660
6

Contents of test.txt:

A 12"x4" dir/B b/C c
A1/B1/C1
A2/B2/C2
A3/B3/C3

Script to create A[123] folders:

file="test.txt"
while read -r line ; do
   mkdir "${line%%/*}"
done < "$file"

Output of ls:

A 12"x4" dir
A1
A2
A3
xiota
  • 4,849
6

For simple case as shown in the question's input example, just use cut and pass output to mkdir via xargs

cut -f1 -d '/' file.txt | xargs -L1 mkdir 

For handling cases where directory name may contain spaces, we could add -d '\n' to the list of options:

$ cat input.txt 
A 1/B 1/C 1
A 2/B 2/C 2
A 3/B 2/C 2
$ cut -f1 -d '/' input.txt | xargs -d '\n' mkdir 
$ ls
A 1  A 2  A 3  input.txt

For more complex variations, such as A 12"x4" dir/B b/C c as suggested by @OleTange in the comments, one may turn to awk to create null-separated list instead of newline-separated list.

awk -F'/' '{printf  "%s\0",$1}' input.txt |  xargs -0 mkdir

@dessert in the comments wondered whether printf can be used instead of cut , and technically speaking it can be used, for instance via limiting the printed string to the width of 3 characters only:

xargs -d '\n' printf "%.3s\n"  < input.txt | xargs -L1 mkdir 

Not the cleanest way, but it proves printf can be used. Of course, this gets problematic if directory name becomes longer than 3 characters.

Sergiy Kolodyazhnyy
  • 105,154
  • 20
  • 279
  • 497
2

using Perl:

perl -ne 'mkdir for /^(\w+)/' list.txt

Or

perl -ne 'mkdir for /^([^\/]+)/' list.txt

if we want to accept spaces on dir-names

αғsнιη
  • 35,660
  • 1
    perl -ne 'mkdir for /^([^\/]+)/' list.txt to cover spaces in dir names. I finally need to learn Perl – thank you! – dessert May 24 '18 at 21:03
0

GNU Parallel may be overkill for the task, but if you are going to do other stuff for each line, then it may be useful:

cat myfile.txt | parallel --colsep / mkdir {1}
parallel -a myfile.txt --colsep / mkdir {1}

It deals correctly with input like:

A 12"x4" dir/B b/C c
Ole Tange
  • 1,710