Bash에서 문자열을 배열로 분할


640

Bash 스크립트에서 줄을 조각으로 나누고 배열에 저장하고 싶습니다.

라인 :

Paris, France, Europe

다음과 같이 배열로 만들고 싶습니다.

array[0] = Paris
array[1] = France
array[2] = Europe

간단한 코드를 사용하고 싶습니다. 명령 속도는 중요하지 않습니다. 어떻게하니?


22
이것은 Google이 1 위를 차지했지만 불행히도 , 쉼표 와 같은 단일 문자가 아닌 (쉼표 공백) 구분에 대해 질문하기 때문에 대답에 논쟁이 있습니다. 후자에만 관심이 있으시면 여기에 답변을 더 쉽게 찾을 수 있습니다. stackoverflow.com/questions/918886/…
antak

문자열을 병합하고 배열로 신경 쓰지 않으려 cut는 경우 유용한 bash 명령도 염두에 두어야합니다. 구분 기호를 정의 할 수 있습니다. en.wikibooks.org/wiki/Cut 고정 너비 레코드 구조에서 데이터를 추출 할 수도 있습니다. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

답변:


1088
IFS=', ' read -r -a array <<< "$string"

문자에서 유의 $IFS이 경우 필드로 분리 될 수 있도록 분리되어 개별적으로 처리 하거나 쉼표 또는 공백이 아닌 두 문자의 시퀀스. 흥미롭게도 공백이 특수하게 처리되므로 입력에 쉼표 공백이 표시되면 빈 필드가 만들어지지 않습니다.

개별 요소에 액세스하려면

echo "${array[0]}"

요소를 반복하려면 다음을 수행하십시오.

for element in "${array[@]}"
do
    echo "$element"
done

인덱스와 값을 모두 얻으려면

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

마지막 예제는 Bash 배열이 희박하므로 유용합니다. 즉, 요소를 삭제하거나 요소를 추가하면 인덱스가 연속되지 않습니다.

unset "array[1]"
array[42]=Earth

배열의 요소 수를 얻으려면

echo "${#array[@]}"

위에서 언급했듯이 배열은 희박 할 수 있으므로 마지막 요소를 얻기 위해 길이를 사용해서는 안됩니다. Bash 4.2 이상에서 수행 할 수있는 방법은 다음과 같습니다.

echo "${array[-1]}"

Bash의 모든 버전에서 (2.05b 이후)

echo "${array[@]: -1:1}"

더 큰 음수 오프셋은 배열 끝에서 더 멀리 선택합니다. 이전 양식에서 빼기 기호 앞에 공백을 기록하십시오. 필수입니다.


15
를 사용 IFS=', '하면 공백을 별도로 제거 할 필요가 없습니다. 테스트 :IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0 : 감사합니다. 내가 무슨 생각을했는지 모르겠습니다. 그건 declare -p array그렇고, 테스트 출력 에 사용 하고 싶습니다 .
추후 공지가있을 때까지 일시 중지되었습니다.

1
이것은 따옴표를 존중하지 않는 것 같습니다. 예를 들어 France, Europe, "Congo, The Democratic Republic of the"콩고 후에 분할됩니다.
이스라엘 Dov

2
@YisraelDov : Bash는 자체적으로 CSV를 처리 할 방법이 없습니다. 따옴표 안의 쉼표와 그 밖의 따옴표 사이의 차이점을 알 수 없습니다. 고급 언어의 라이브러리 (예 : Python 의 csv 모듈) 와 같은 CSV를 이해하는 도구를 사용해야합니다 .
추후 공지가있을 때까지 일시 중지되었습니다.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")메모로 분할됩니다 . 따라서 IFS=', '문자열 구분 기호가 아닌 개별 문자 세트 이므로 공백이없는 필드에서만 작동합니다 .
dawg

332

이 질문에 대한 모든 대답은 어떤 식 으로든 잘못되었습니다.


오답 # 1

IFS=', ' read -r -a array <<< "$string"

1 : 의 오용입니다 $IFS. 의 값은 $IFS변수입니다 하지 A와 촬영 단일 가변 길이 오히려 그것이로한다 문자열 세퍼레이터 세트단일 문자의 각 필드는 해당 문자열의 분리, read입력 라인으로부터 벗어난 분열이 종료 될 수 있는 세트에서 문자 ( 이 예에서는 쉼표 또는 공백).

실제로, 실제 고수들에게는 전체 의미 $IFS가 약간 더 관련되어 있습니다. 로부터 bash는 설명서 :

쉘은 IFS의 각 문자를 분리 문자로 취급 하고 이러한 문자를 필드 종결 자로 사용하여 다른 확장 결과를 단어로 나눕니다. 경우 IFS가 설정되지 않은 경우, 또는 그 값이 정확히 <스페이스> <탭> <개행 문자> , 기본의 다음 순서 <공간> , <탭><줄 바꿈> 시작과 이전 확장의 결과의 끝 무시 되고 시작 또는 끝에없는 IFS 문자 시퀀스는 단어를 구분하는 역할을합니다. IFS 에 기본값 이외의 값이 있으면 공백 문자 <space> , <tab><공백 문자가 IFS ( ISF 공백 문자) 값에있는 한 단어의 시작과 끝에서 무시됩니다 . 의 모든 문자 IFS 아닌 IFS 인접한과 함께 공백을 IFS , 필드을 구분 공백 문자. 일련의 IFS 공백 문자도 분리 문자로 처리됩니다. IFS 의 값 이 널이면 단어 분할이 발생하지 않습니다.

기본적으로 null이 아닌 값이 아닌 값의 $IFS경우 필드는 (1) "IFS 공백 문자"집합 (즉, <space> 중 하나 이상)에서 하나 이상의 문자 시퀀스로 구분할 수 있습니다 . <tab><newline> ( 줄 바꿈 (LF)을 의미하는 "줄 바꿈" )은 $IFS(2) 어디에나 존재 $IFS합니다. 입력 라인에.

OP의 경우, 이전 단락에서 설명한 두 번째 분리 모드가 입력 문자열에 대해 원하는 것일 수도 있지만, 내가 설명한 첫 번째 분리 모드가 전혀 정확하지 않다고 확신 할 수 있습니다. 예를 들어, 입력 문자열이 'Los Angeles, United States, North America'무엇입니까?

IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")

2 : 당신은의 값이있는 경우 (예 : 없음 다음 공간 또는 다른 짐과 함께, 그 자체로 쉼표와 같은) 단일 문자 구분이 솔루션을 사용하더라도 $string다음, 변수가 발생 어떤 LFS를 포함 할 read것 첫 번째 LF가 발생하면 처리를 중지하십시오. read내장에만 호출 당 한 줄을 처리합니다. here-string 메커니즘 을 사용하여이 예제에서 수행하는 것처럼 입력 파이프 라인으로 보내거나 read명령문으로 만 경로 재지 정하는 경우에도 마찬가지 이므로 처리되지 않은 입력은 유실됩니다. 내장 기능 을 강화하는 코드 에는 포함 된 명령 구조 내의 데이터 흐름에 대한 지식이 없습니다.read

이것이 문제를 일으킬 가능성은 없지만, 가능하면 피해야하는 미묘한 위험이라고 주장 할 수 있습니다. read내장은 실제로 두 가지 수준의 입력 분할 을 수행하기 때문에 발생 합니다. OP는 한 수준의 분할 만 원 read하므로이 내장 사용은 적합하지 않으므로 피해야합니다.

3 : 이 솔루션의 명백한 잠재적 문제는 read빈 필드를 유지하지만 항상 비어있는 경우 후행 필드를 삭제 한다는 것입니다. 데모는 다음과 같습니다.

string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")

아마도 OP는 이것에 신경 쓰지 않을지 모르지만 여전히 알아야 할 한계입니다. 솔루션의 견고성과 일반성을 줄입니다.

이 문제는 read나중에 설명 할 것처럼 입력 문자열에 더미 후행 구분 기호를 입력 문자열에 추가하여 해결할 수 있습니다 .


오답 # 2

string="1:2:3:4:5"
set -f                     # avoid globbing (expansion of *).
array=(${string//:/ })

비슷한 생각 :

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

(참고 : 응답자가 생략 한 것으로 보이는 명령 대체에 누락 된 괄호를 추가했습니다.)

비슷한 생각 :

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

이 솔루션은 배열 할당에서 단어 분할을 사용하여 문자열을 필드로 분할합니다. 재미있게도 read, 일반적인 단어 분리는 $IFS특수 변수를 사용하지만,이 경우 기본값 <space> <tab> <newline> 및 하나 이상의 IFS 순서로 설정되어 있음을 암시합니다. 문자 (이제 모두 공백 문자 임)는 필드 구분 기호로 간주됩니다.

이것은 read단어 분리 자체가 단 하나의 분리 레벨을 구성하기 때문에에 의해 커밋 된 두 레벨의 분리 문제를 해결합니다 . 그러나 이전과 마찬가지로 여기서 문제는 입력 문자열의 개별 필드에 이미 $IFS문자 가 포함되어 있으므로 단어 분할 작업 중에 잘못 분할 될 수 있다는 것입니다. 이것은 이러한 응답자가 제공하는 샘플 입력 문자열 중 어느 경우에도 발생하지 않지만 (얼마나 편리합니다 ...) 물론이 관용구를 사용한 코드베이스가 다음과 같은 위험을 초래한다는 사실을 변경하지는 않습니다. 이 가정이 어느 시점에서 선을 넘어 서면 폭파합니다. 다시 한번, 'Los Angeles, United States, North America'(또는 'Los Angeles:United States:North America') 에 대한 나의 반례를 고려하십시오 .

또한, 단어 분할은 일반적으로 뒤 따른다 파일명 확장 ( 일명 패스 팽창 일명 , 완료되면, 문자를 포함하는 잠재적 손상 단어 것이다 로빙) *, ?또는 [다음에 ](그리고 있다면, extglob설정, 괄호 단편 앞에는 ?, *, +, @, 또는 !) 파일 시스템 객체와 일치시키고 이에 따라 단어 ( "글로브")를 확장합니다. 이 세 명의 응답자 중 첫 번째 응답자는 set -fglobbing을 비활성화하기 위해 미리 실행 하여이 문제를 영리하게 극복했습니다. 기술적으로 이것은 작동합니다 (아마도 추가해야하지만set +f 나중에 후속 코드에 대한 글 로빙을 다시 활성화해야합니다.)하지만 로컬 코드에서 기본 문자열 대 배열 구문 분석 작업을 해킹하기 위해 전역 셸 설정을 엉망으로 만드는 것은 바람직하지 않습니다.

이 답변의 또 다른 문제는 모든 빈 필드가 손실된다는 것입니다. 응용 프로그램에 따라 문제가 될 수도 있고 아닐 수도 있습니다.

참고 :이 솔루션을 사용하려는 경우 명령 대체 (쉘을 포크)를 호출하고 파이프 라인을 시작하는 데 어려움을 겪는 대신 ${string//:/ }"패턴 대체"형식의 매개 변수 확장 을 사용하는 것이 좋습니다. 매개 변수 확장은 순전히 쉘 내부 조작이므로 외부 실행 파일 ( tr또는 sed) 실행 ( trsed솔루션의 경우 입력 변수는 명령 대체 내에서 큰 따옴표로 묶어야합니다. 그렇지 않으면 단어 분할이 echo명령에 영향을 미치고 필드 값을 엉망으로 만들 수 있습니다. 또한 $(...)명령 대체 형식이 이전보다 선호됩니다`...` 명령 대체의 중첩을 단순화하고 텍스트 편집기로 구문 강조를 개선 할 수 있기 때문에 형식)


오답 # 3

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

이 답변은 # 2 와 거의 동일 합니다. 차이점은 응답자가 필드가 두 개의 문자로 구분되고 하나는 기본값으로 표시되고 $IFS다른 하나는 그렇지 않은 것으로 가정한다는 것입니다 . 그는 패턴 대체 확장을 사용하여 비 IFS 표현 문자를 제거한 다음 단어 분할을 사용하여 존속하는 IFS 표현 분리 문자 문자에서 필드를 분할함으로써 다소 구체적인 경우를 해결했습니다.

이것은 매우 일반적인 해결책이 아닙니다. 또한 쉼표는 실제로 "기본"구분 문자이며, 필드 분리를위한 공백 문자에 따라이 문자를 스트리핑 한 다음 잘못했다고 주장 할 수 있습니다. 다시 한번, 내 반례를 고려하십시오 : 'Los Angeles, United States, North America'.

또한 파일 이름 확장으로 확장 단어가 손상 될 수 있지만, set -f및로 할당에 대한 globbing을 일시적으로 사용 중지하면이를 방지 할 수 있습니다 set +f.

또한 모든 빈 필드가 손실되므로 응용 프로그램에 따라 문제가 될 수도 있고 아닐 수도 있습니다.


오답 # 4

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

이것은 단어 분할을 사용하여 작업을 수행한다는 점에서 # 2# 3 과 유사합니다 . 코드 $IFS는 입력 문자열에 존재하는 단일 문자 필드 구분 기호 만 포함하도록 명시 적으로 설정 됩니다. OP의 쉼표 공백 구분 기호와 같은 다중 문자 필드 구분 기호에는이 기능을 사용할 수 없다는 점을 반복해야합니다. 그러나이 예제에서 사용 된 LF와 같은 단일 문자 구분 기호의 경우 실제로는 완벽에 가깝습니다. 이전에 틀린 답으로 보았 듯이 필드가 실수로 중간에 나눌 수 없으며 필요에 따라 하나의 분리 수준 만 있습니다.

하나의 문제는 다시 한 번이에 중요한 문을 포장에 의해 해결 될 수 있지만 파일 이름 확장은, 이전의 손상 영향을받는 단어 설명 것 같은 것입니다 set -fset +f.

또 다른 잠재적 인 문제는 LF가 앞에서 정의한 "IFS 공백 문자"로 규정되기 때문에 # 2# 3에서 와 같이 모든 빈 필드가 손실된다는 것 입니다. 구분자가 "IFS 공백 문자"가 아닌 경우에는 문제가되지 않으며 응용 프로그램에 따라 문제가되지 않을 수도 있지만 솔루션의 일반성을 저해합니다.

그래서, 당신은 하나의 문자 구분 기호를 가지고 가정, 요약하고,이 중 비 "공백 문자 IFS"또는 당신은 빈 필드에 대한 상관 없어, 당신은에 중요한 문을 포장 set -f하고 set +f,이 솔루션 작품 그렇지 않으면 그렇지 않습니다.

(또한 정보를 위해 bash의 변수에 LF를 할당하는 것은 $'...'구문 과 같이 더 쉽게 수행 할 수 있습니다 IFS=$'\n';.


오답 # 5

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

비슷한 생각 :

IFS=', ' eval 'array=($string)'

이 솔루션은 효과적으로 # 1 ( $IFS쉼표 공간으로 설정 됨)과 # 2-4 (단어 분리를 사용하여 문자열을 필드로 분할 함 ) 사이의 교차 입니다. 이로 인해 위의 모든 잘못된 답을 겪는 대부분의 문제로 인해 모든 세계에서 최악의 문제가 발생합니다.

또한 두 번째 변형에 대해서는 eval인수가 작은 따옴표로 묶인 문자열 리터럴이므로 정적으로 알려져 있기 때문에 호출이 완전히 필요하지 않은 것처럼 보일 수 있습니다. 그러나 실제로 eval이런 식 으로 사용하면 명백한 이점 이 있습니다. 일반적으로 변수 할당 만으로 구성되는 간단한 명령을 실행하면 그 뒤에 실제 명령 단어가 없으면 쉘 환경에서 할당이 적용됩니다.

IFS=', '; ## changes $IFS in the shell environment

간단한 명령에 여러 변수 할당이 포함 된 경우에도 마찬가지입니다 . 명령 단어가없는 한 모든 변수 지정은 쉘 환경에 영향을줍니다.

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

그러나 변수 할당이 명령 이름에 첨부되면 (이를 "접두사 할당"이라고 부르는 경우) 쉘 환경에 영향을 미치지 않으며 대신 내장 명령인지 여부에 관계없이 실행 된 명령의 환경에만 영향을줍니다. 또는 외부 :

IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it

bash 매뉴얼의 관련 인용문 :

명령 이름이 없으면 변수 지정이 현재 쉘 환경에 영향을줍니다. 그렇지 않으면 변수가 실행 된 명령의 환경에 추가되고 현재 쉘 환경에 영향을 미치지 않습니다.

이 변수 할당 기능을 활용하여 $IFS일시적으로 만 변경할 수 $OIFS있으므로 첫 번째 변형에서 변수 로 수행되는 것과 같은 전체 저장 및 복원 bit 비트를 피할 수 있습니다 . 그러나 여기서 직면 한 문제는 실행해야하는 명령 자체가 단순한 변수 할당이므로 $IFS할당을 임시 로 만드는 명령 단어가 포함되지 않는다는 것 입니다. 당신은 자신에게 생각할 수도 있습니다. 왜 할당을 일시적으로 : builtin만들기 위해 no-op 명령 단어를 문장에 추가하지 $IFS않겠습니까? $array할당을 일시적으로 만들 수 있기 때문에 작동하지 않습니다 .

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

그래서 우리는 효과적으로 곤경에 처해 있습니다. 그러나 eval코드를 실행하면 일반 정적 소스 코드 인 것처럼 쉘 환경에서 실행되므로 접두사 할당 은 쉘 환경에서 적용 $array되도록 eval인수 내에서 할당을 실행할 수 있습니다. 명령 $IFS앞에 접두사가 eval붙으면 eval명령 보다 오래 지속되지 않습니다 . 이것은이 솔루션의 두 번째 변형에서 사용되는 트릭입니다.

IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does

보시다시피, 실제로는 꽤 영리한 트릭이며, (적어도 할당 효과와 관련하여) 다소 분명하지 않은 방식으로 필요한 것을 정확하게 달성합니다. eval; 의 참여에도 불구하고 실제로이 트릭에 반대하지는 않습니다 . 보안 위협으로부터 보호하기 위해 인수 문자열을 작은 따옴표로 묶으십시오.

그러나 다시 말하지만 문제의 "모든 세계에서 가장 나쁜"응집 때문에 여전히 OP의 요구 사항에 대한 잘못된 답변입니다.


오답 # 6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)

음 ... 뭐? OP에는 배열로 구문 분석해야하는 문자열 변수가 있습니다. 이 "답변"은 배열 리터럴에 붙여 넣은 입력 문자열의 완전 내용으로 시작합니다. 나는 그것이 한 가지 방법이라고 생각합니다.

응답자가 $IFS변수가 모든 컨텍스트의 모든 bash 구문 분석에 영향을 미친 다고 가정했을 수도 있습니다 . bash 매뉴얼에서 :

IFS     확장 후 단어 분리 및 read 내장 명령을 사용하여 행을 단어로 분할하는 데 사용되는 내부 필드 구분 기호입니다 . 기본값은 <space> <tab> <newline> 입니다.

따라서 $IFS특수 변수는 실제로 두 가지 컨텍스트에서만 사용됩니다. (1) 확장 후 수행되는 단어 분리 ( bash 소스 코드를 구문 분석 할 때가 아님 ) 및 (2) 입력 행을 read내장 단어로 단어로 분할하는 경우 .

좀 더 명확하게 해보도록하겠습니다. 파싱실행을 구분하는 것이 좋을 것이라고 생각합니다 . Bash는 먼저 소스 코드를 구문 분석 해야합니다 . 이는 명백하게 구문 분석 이벤트이며, 나중에 코드가 확장 될 때 코드가 실행 됩니다. 확장은 실제로 실행 이벤트입니다. 또한 $IFS방금 인용 한 변수에 대한 설명과 관련하여 문제가 있습니다 . 오히려 단어 분할이 수행한다는보다 확장 후 , 나는 그 단어 분할이 수행되는 말을 하는 동안 아마도 더 정확하게, 단어 분할이며, 확장, 또는 의 일부확장 과정. "단어 분리"라는 문구는이 확장 단계만을 의미합니다. 불행히도 문서는 "split"과 "words"라는 단어를 많이 던지는 것처럼 보이지만 bash 소스 코드의 구문 분석을 참조하는 데 사용해서는 안됩니다. bash 매뉴얼 의 linux.die.net 버전 에서 발췌 한 내용은 다음과 같습니다 .

확장은 단어로 분할 된 후 명령 행에서 수행됩니다. 괄호 확장 , 틸드 확장 , 매개 변수 및 변수 확장 , 명령 대체 , 산술 확장 , 단어 분할경로 이름 확장의 7 가지 확장이 수행 됩니다.

확장 순서는 다음과 같습니다. 괄호 확장; 물결표 확장, 매개 변수 및 변수 확장, 산술 확장 및 명령 대체 (왼쪽에서 오른쪽으로 수행); 단어 분할; 경로명 확장.

확장 섹션의 첫 문장에서 "단어"대신 "토큰"이라는 단어를 선택하기 때문에 GNU 버전 의 매뉴얼이 약간 더 나을 것이라고 주장 할 수 있습니다.

확장은 토큰으로 분할 된 후 명령 행에서 수행됩니다.

중요한 점은 $IFSbash가 소스 코드를 구문 분석하는 방식을 변경하지 않는다는 것입니다. bash 소스 코드 구문 분석은 실제로 명령 시퀀스, 명령 목록, 파이프 라인, 매개 변수 확장, 산술 대체 및 명령 대체와 같은 셸 문법의 다양한 요소를 인식하는 매우 복잡한 프로세스입니다. 대부분의 경우 bash 파싱 프로세스는 변수 할당과 같은 사용자 수준 작업으로 변경할 수 없습니다 (실제로이 규칙에는 약간의 예외가 있습니다. 예를 들어 다양한 셸 설정 참조)compatxx파싱 ​​동작의 특정 측면을 즉시 변경할 수 있습니다). 이 복잡한 구문 분석 프로세스에서 발생하는 업스트림 "단어"/ "토큰"은 확장 된 (확장?) 텍스트의 단어를 다운 스트림으로 분할하는 위의 발췌 부분에서 분류 된 "확장"의 일반적인 프로세스에 따라 확장됩니다. 단어는 단순히 그 과정의 한 단계입니다. 단어 분리는 이전 확장 단계에서 뱉어 낸 텍스트 만 만집니다. 소스 바이트 스트림에서 바로 구문 분석 된 리터럴 텍스트에는 영향을 미치지 않습니다.


오답 # 7

string='first line
        second line
        third line'

while read -r line; do lines+=("$line"); done <<<"$string"

이것은 최고의 솔루션 중 하나입니다. 을 다시 사용 read합니다. read우리가 하나만 필요할 때 두 가지 수준의 분리를 수행하기 때문에 부적절하다고 말하지 않았습니까 ? 여기서의 요점은 호출 read당 하나의 필드 만 분리하여 효과적으로 한 수준의 분할 만 수행하는 방식으로 호출 할 수 있다는 것입니다. 루프에서 반복적으로 호출해야하는 비용이 필요합니다. 손이 약간 얇지 만 작동합니다.

그러나 문제가 있습니다. 첫 번째 :에 하나 이상의 NAME 인수를 제공 read하면 입력 문자열에서 분리 된 각 필드의 선행 및 후행 공백이 자동으로 무시됩니다. 이 $IFS게시물의 앞부분에서 설명한 것처럼 기본값으로 설정되어 있는지 여부에 관계없이 발생합니다 . 이제 OP는 특정 사용 사례에 대해 이것을 신경 쓰지 않을 수 있으며 실제로 구문 분석 동작의 바람직한 기능 일 수 있습니다. 그러나 문자열을 필드로 구문 분석하려는 모든 사람이 이것을 원하는 것은 아닙니다. 그러나 해결책이 있습니다. 다소 명확하지 않은 사용법은 NAME 인수를 read0으로 전달하는 것 입니다. 이 경우 입력 스트림에서 얻은 전체 입력 행을이라는 변수에 저장 하고 보너스는 그렇지 않습니다.read$REPLY값에서 선행 및 후행 공백을 제거합니다. 이것은 read쉘 프로그래밍 경력에서 자주 사용하는 매우 강력한 사용법입니다 . 행동의 차이에 대한 데모는 다음과 같습니다.

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

이 솔루션의 두 번째 문제는 실제로 OP의 쉼표 공간과 같은 사용자 정의 필드 구분 기호의 경우를 다루지 않는다는 것입니다. 이전과 같이 다중 문자 구분 기호는 지원되지 않으므로이 솔루션의 불행한 한계입니다. -d옵션에 구분 기호를 지정하여 최소한 쉼표로 분할하려고 시도 할 수 있지만 어떻게되는지 살펴보십시오.

string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")

예측할 수없는 주변 공백이 필드 값으로 끌어 당겨 졌으므로 트리밍 작업을 통해 이후에 수정해야합니다 (이는 while 루프에서 직접 수행 할 수도 있음). 그러나 또 다른 명백한 오류가 있습니다 : 유럽이 없습니다! 무슨 일이야? 대답은 read최종 필드에서 최종 필드 종결자가 발생하지 않고 파일 끝 (이 경우 문자열 끝이라고 부름)에 도달하면 실패 리턴 코드 를 리턴하는 것입니다. 이로 인해 while 루프가 조기에 중단되고 최종 필드가 손실됩니다.

기술적으로 이와 동일한 오류가 이전 예제에도 영향을 미쳤습니다. 차이점은 필드 구분 기호가 LF로 설정되었다는 것입니다.이 -d옵션 은 옵션을 지정하지 않을 때의 기본값 이며, <<<( "here-string") 메커니즘은 LF를 다음과 같이 공급하기 직전에 문자열에 자동으로 LF를 추가합니다. 명령에 입력하십시오. 따라서 이러한 경우 우연히 추가 더미 터미네이터를 입력에 추가하여 실수로 최종 필드가 떨어지는 문제를 해결했습니다. 이 솔루션을 "더미 터미네이터"솔루션이라고합니다. here-string에서 인스턴스화 할 때 입력 문자열과 직접 연결하여 사용자 정의 구분 기호에 대해 더미 종결 자 솔루션을 수동으로 적용 할 수 있습니다.

a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

문제가 해결되었습니다. 또 다른 해결책은 (1) read리턴 된 실패와 (2) $REPLY가 모두 비어있는 경우 while 루프를 중단하는 것 입니다. 즉, read파일 끝을 누르기 전에 문자를 읽을 수 없습니다. 데모:

a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

이 접근법은 또한 <<<리디렉션 연산자에 의해 here-string에 자동으로 추가되는 비밀 LF를 보여줍니다 . 물론 앞에서 설명한 것처럼 명시적인 트리밍 작업을 통해 별도로 분리 할 수도 있지만, 수동 더미 터미네이터 방식은이를 직접 해결하므로 그대로 사용할 수 있습니다. 수동 더미 터미네이터 솔루션은 실제로이 두 가지 문제 (드롭-파이널 필드 문제 및 추가 된 LF 문제)를 한 번에 해결한다는 점에서 매우 편리합니다.

따라서 전반적으로 이것은 매우 강력한 솔루션입니다. 남아있는 약점은 다중 문자 구분 기호에 대한 지원이 부족하다는 것입니다. 나중에 다루겠습니다.


오답 # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(이것은 실제로 # 7 과 동일한 게시물에서 온 것으로 , 답변자는 같은 게시물에 두 가지 솔루션을 제공했습니다.)

readarray동의어 인 내장 mapfile이 이상적입니다. 한 번에 바이트 스트림을 배열 변수로 구문 분석하는 기본 제공 명령입니다. 루프, 조건부, 대체 또는 다른 것을 망칠 필요가 없습니다. 그리고 입력 문자열에서 공백을 제거하지 않습니다. ( -O제공되지 않은 경우 ) 대상 배열을 할당하기 전에 편리하게 지 웁니다. 그러나 여전히 완벽하지는 않으므로 "답변이 틀렸다"는 비판이 있습니다.

먼저, 이것을 방해 read하지 readarray않으려면 필드 구문 분석을 수행 할 때 의 동작과 마찬가지로 후행 필드가 비어 있으면 삭제합니다. 다시 말하지만 이것은 아마도 OP에 대한 우려는 아니지만 일부 유스 케이스에 대한 것일 수 있습니다. 잠시 후에 다시 올게요.

둘째, 이전과 마찬가지로 다중 문자 구분 기호를 지원하지 않습니다. 이 문제도 잠시 후에 수정하겠습니다.

셋째, 작성된 솔루션은 OP의 입력 문자열을 구문 분석하지 않으며 실제로 구문 분석하는 그대로 사용할 수 없습니다. 이 순간도 확장하겠습니다.

위의 이유로, 나는 여전히 이것이 OP의 질문에 대한 "오답"이라고 생각합니다. 아래에는 올바른 답변이라고 생각되는 내용이 나와 있습니다.


정답

다음 은 옵션을 지정하여 # 8을 작동 시키는 순진한 시도입니다 -d.

string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')

우리는 그 결과가 # 7read 에서 논의 된 루핑 솔루션 의 이중 조건 접근법에서 얻은 결과와 동일하다는 것을 알 수 있습니다 . 우리는 수동 더미 터미네이터 트릭으로 이것을 거의 해결할 수 있습니다 .

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

여기서의 문제 readarray<<<리디렉션 연산자가 LF를 입력 문자열에 추가 했으므로 후행 필드가 비어 있지 않기 때문에 후행 필드가 유지 된다는 것입니다 (그렇지 않으면 삭제됨). 우리는 사실 최종 배열 요소를 명시 적으로 설정 해제하여이를 처리 할 수 ​​있습니다.

readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")

실제로 관련되어있는 유일한 두 가지 문제는 (1) 다듬어야하는 외부 공백 및 (2) 다중 문자 구분 기호에 대한 지원 부족입니다.

물론 공백은 나중에 다듬을 수 있습니다 (예 : Bash 변수에서 공백을 자르는 방법? 참조 ). 그러나 다중 문자 구분 기호를 해킹 할 수 있다면 두 가지 문제를 한 번에 해결할 수 있습니다.

불행히도 다중 문자 구분 기호를 작동시키는 직접적인 방법 은 없습니다 . 내가 생각한 가장 좋은 해결책은 입력 문자열을 사전 처리하여 다중 문자 구분 기호를 입력 문자의 내용과 충돌하지 않는 단일 문자 구분 기호로 대체하는 것입니다. 이 보장이있는 유일한 문자는 NUL 바이트 입니다. 이것은 bash에서 (zsh는 아니지만 우연히) 변수에 NUL 바이트를 포함 할 수 없기 때문입니다. 이 사전 처리 단계는 프로세스 대체에서 인라인으로 수행 될 수 있습니다. 다음은 awk를 사용하여 수행하는 방법입니다 .

readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

마지막으로! 이 솔루션은 중간에 잘못된 필드를 분할하지 않고, 조기에 잘리지 않으며, 빈 필드를 삭제하지 않으며, 파일 이름 확장에서 자체를 손상시키지 않으며, 앞뒤 공백을 자동으로 제거하지 않으며, 끝에 LF를 남기지 않습니다. 루프가 필요하지 않으며 단일 문자 분리 문자에 대해서는 정착하지 않습니다.


트리밍 솔루션

마지막으로,의 모호한 -C callback옵션을 사용하여 상당히 복잡한 트리밍 솔루션을 시연하고 싶었습니다 readarray. 불행히도, Stack Overflow의 draconian 30,000 자 제한에 대해 공간이 부족하여 설명 할 수 없습니다. 나는 그것을 독자들을위한 연습으로 남겨 둘 것이다.

function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")

8
Bash 4.4 -dreadarray처음으로 나타나는 옵션이 있음을 이해하는 것이 도움이 될 수도 있습니다 (이해할 여지는 없었지만) .
fbicknel

2
훌륭한 답변 (+1). 당신이 당신의 awk를 변경 awk '{ gsub(/,[ ]+|$/,"\0"); print }'하고 결승의 연결 ", " 을 제거하면 최종 기록을 제거하기 위해 체조를 수행 할 필요가 없습니다. 따라서 : readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")Bash에서 지원합니다 readarray. 귀하의 방법은 Bash 4.4 이상입니다. -dinreadarray
dawg

3
@datUser 죄송합니다. bash 버전이 너무 오래되어 있어야합니다 readarray. 이 경우에 구축 된 두 번째로 좋은 솔루션을 사용할 수 있습니다 read. 나는 이것을 언급하고있다 : a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";( awk다 문자 구분 기호 지원이 필요한 경우 대체로). 문제가 발생하면 알려주십시오. 나는이 솔루션이 20 년 전에 출시 된 버전 2로 돌아가서 상당히 오래된 bash 버전에서 작동해야한다고 확신합니다.
bgoldst

1
와, 정말 훌륭한 답변입니다! Hee Hee, 내 응답 : bash 스크립트를 버리고 파이썬을 시작했습니다!
artfulrobot

1
OSX의 @datUser bash는 여전히 3.2 (2007 년 출시)에서 멈췄습니다. 저는 Homebrew에서 찾은 bash를 사용하여 OS X에서 4.X bash 버전을 얻었습니다
JDS

222

IFS를 설정하지 않은 방법은 다음과 같습니다.

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

아이디어는 문자열 교체를 사용하고 있습니다.

${string//substring/replacement}

$ substring의 모든 일치 항목을 공백으로 바꾼 다음 대체 된 문자열을 사용하여 배열을 초기화합니다.

(element1 element2 ... elementN)

참고 :이 답변은 split + glob 연산자를 사용 합니다. 따라서 (와 같은 *) 일부 문자의 확장을 방지 하려면이 스크립트에 대한 글 로빙을 일시 중지하는 것이 좋습니다.


1
이 접근법을 사용했습니다 ... 분할 긴 문자열을 발견 할 때까지. 1 분 이상 동안 100 % CPU (그런 다음 죽였습니다). 이 방법을 사용하면 IFS의 일부 문자가 아닌 문자열로 분할 할 수 있기 때문에 유감입니다.
Werner Lehmann

어딘가에 문제가있는 것처럼 1 분 이상 100 % CPU 시간이 들립니다. 그 문자열의 길이는 얼마입니까, MB 또는 GB 크기입니까? 나는 일반적으로 작은 문자열 분할이 필요하다면 Bash 내에 머물기를 원하지만 큰 파일이라면 Perl과 같은 것을 실행하려고합니다.

12
경고 :이 방법으로 문제가 발생했습니다. *라는 요소가 있다면 cwd의 모든 요소도 얻을 수 있습니다. 따라서 string = "1 : 2 : 3 : 4 : *"는 구현에 따라 예기치 않은 위험한 결과를 제공합니다. (IFS = ','read -a array <<< "$ string")와 동일한 오류가 발생하지 않았으며 사용하기에 안전합니다.
Dieter Gribnitz

4
인용은 ${string//:/ }쉘 확장 방지
앤드류 화이트

1
OSX에서 다음을 사용해야했습니다. array=(${string//:/ })
Mark Thomson

95
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

3 장 인쇄


8
실제로이 방법을 선호합니다. 단순한.
shrimpwagon

4
나는 이것을 복사하여 붙여 넣었고 에코와는 작동하지 않지만 for 루프에서 사용할 때 작동했습니다.
Ben

2
명시된대로 작동하지 않습니다. @ Jmoney38 또는 shrimpwagon 이것을 터미널에 붙여 넣고 원하는 출력을 얻을 수 있으면 결과를 여기에 붙여 넣으십시오.
abalter

2
@abalter와 함께 작동합니다 a=($(echo $t | tr ',' "\n")). 와 동일한 결과 a=($(echo $t | tr ',' ' ')).
leaf

@procrastinator 난 그냥 그것을 시도 VERSION="16.04.2 LTS (Xenial Xerus)"A의 bash쉘, 마지막은 echo단지 빈 줄을 인쇄합니다. 어떤 Linux 버전과 어떤 셸을 사용하고 있습니까? 불행히도, 주석에 터미널 세션을 표시 할 수 없습니다.
abalter

29

때로는 구분 기호가 캐리지 리턴 인 경우 허용 된 답변에 설명 된 방법이 효과가 없었습니다.
그런 경우에 나는 이런 식으로 해결했다 :

string='first line
second line
third line'

oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

2
+1 이것은 완전히 나를 위해 일했습니다. 줄 바꿈으로 나눈 여러 개의 문자열을 배열에 넣어야 read -a arr <<< "$strings"했지만 작동하지 않았습니다 IFS=$'\n'.
Stefan van den Akker


이것은 원래 질문에 대한 답이 아닙니다.
Mike

29

허용되는 답변은 한 줄의 값에 적용됩니다.
변수에 여러 줄이있는 경우 :

string='first line
        second line
        third line'

모든 줄을 얻으려면 매우 다른 명령이 필요합니다.

while read -r line; do lines+=("$line"); done <<<"$string"

또는 훨씬 간단한 bash readarray :

readarray -t lines <<<"$string"

printf 기능을 활용하면 모든 행을 인쇄하는 것이 매우 쉽습니다.

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

2
아니 모든 솔루션은 모든 상황에 대한 작동하지만, readarray 당신의 언급은 ... 오분 내 마지막 두 시간 대체 당신은 내 투표 있어요
84 화가

7

이것은 Jmoney38접근 방식 과 비슷 하지만 sed를 사용합니다.

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

인쇄 1


1
내 경우에는 1 2 3 4를 인쇄합니다
minigeek

6

문자열을 배열로 나누는 열쇠는의 다중 문자 분리 문자입니다 ", ". IFSIFS는 문자열이 아닌 문자 세트이므로 다중 문자 구분 기호에 사용 하는 솔루션 은 본질적으로 잘못되었습니다.

지정 IFS=", "하면 문자열이 EITHER ","OR " "또는 두 문자 분리 문자의 정확한 표현이 아닌 이들의 조합에서 끊어집니다 ", ".

프로세스 대체와 함께 awk또는 sed문자열을 사용 하여 분할 할 수 있습니다 .

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Bash에서 직접 정규식을 사용하는 것이 더 효율적입니다.

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

두 번째 형태에서는 하위 쉘이 없으며 본질적으로 더 빠릅니다.


bgoldst에 의해 편집 : 여기 내 readarray솔루션을 dawg 의 정규식 솔루션 과 비교하는 벤치 마크가 있으며 그 read솔루션에 대한 솔루션 도 포함 했습니다 (참고 : 솔루션과의 조화를 위해 정규식 솔루션을 약간 수정했습니다). 게시하다):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

매우 멋진 솔루션! 정규식 일치에 루프를 사용하는 것을 생각하지 않았습니다 $BASH_REMATCH. 그것은 작동하고 실제로 서브 쉘이 생성되는 것을 피합니다. 나에게서 +1 그러나 비판으로, 정규 표현식 자체는 약간 이상적이지 않습니다. 정확하지 않은 승수에 대한 지원 부족을 해결하기 위해 구분 기호 토큰 (특히 쉼표)의 일부를 복제해야한다는 점에서 ERE의 (또한 둘러보기) (bash에 내장 된 "확장 된"정규식 맛). 이것은 덜 일반적이고 강력합니다.
bgoldst

둘째, 벤치마킹을 수행했으며 작은 문자열에 대한 다른 솔루션보다 성능이 우수하지만 반복되는 문자열 재구성으로 인해 기하 급수적으로 악화되어 매우 큰 문자열에 치명적입니다. 귀하의 답변에 대한 편집 내용을 참조하십시오.
bgoldst

@bgoldst : 정말 멋진 벤치 마크입니다! 정규식을 방어하기 위해 10 또는 10 만 개의 필드 (정규식이 분할 \n되는 필드)에 대해 해당 필드를 구성하는 일정한 형식의 레코드 (예 : 구분 된 텍스트 행) 가있을 수 있으므로 치명적인 속도 저하가 발생하지 않을 수 있습니다. 필드가 100,000 개인 문자열이있는 경우 Bash가 이상적이지 않을 수 있습니다. ;-) 벤치 마크에 감사드립니다. 나는 한두 가지를 배웠습니다.
dawg

4

순수한 bash 다중 문자 분리 문자 솔루션.

다른 사람들 이이 스레드에서 지적했듯이 OP의 질문은 배열로 구문 분석 할 쉼표로 구분 된 문자열의 예를 제공했지만 쉼표 구분 기호, 단일 문자 구분 기호 또는 다중 문자에만 관심이 있는지는 나타내지 않았습니다. 구분자.

Google 은이 답변을 검색 결과의 상단 또는 근처에 순위를 매기는 경향이 있기 때문에 독자에게 여러 문자 구분 기호에 대한 강력한 답변을 제공하고 싶었습니다. 하나 이상의 응답에서 언급되기 때문입니다.

다중 문자 구분 기호 문제에 대한 해결책을 찾고 있다면 Mallikarjun M 의 게시물, 특히 매개 변수 확장을 사용 하여이 우아한 순수 BASH 솔루션을 제공 하는 gniourf_gniourf 의 응답을 검토하는 것이 좋습니다 .

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

링크 인용 코멘트 / 참조 포스트

인용 된 질문에 연결 : bash에서 여러 문자 구분 기호로 문자열을 나누는 방법은 무엇입니까?


1
비슷하지만 개선 된 접근 방식에 대한 내 의견 을 참조하십시오 .
xebeche 2016 년

3

이것은 OSX에서 저에게 효과적입니다.

string="1 2 3 4 5"
declare -a array=($string)

문자열에 다른 구분 기호가있는 경우 첫 번째 공백을 공백으로 바꾸십시오.

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

간단한 :-)


Bash와 Zsh에서 모두 작동합니다.
Elijah W. Gagne

2

IFS를 수정하지 않고 다른 방법으로 수행 할 수 있습니다.

read -r -a myarray <<< "${string//, /$IFS}"

원하는 구분 기호와 일치하도록 IFS를 변경하지 않고 원하는 구분 기호를 모두 via의 ", "내용으로 바꿀 수 있습니다 .$IFS"${string//, /$IFS}"

어쩌면 이것은 매우 큰 줄의 경우 느려질까요?

이것은 Dennis Williamson의 답변을 기반으로합니다.


2

word1, word2, ...와 같은 입력을 구문 분석하려고 할 때이 게시물을 보았습니다.

위의 어느 것도 나를 도우 지 못했습니다. awk를 사용하여 해결했습니다. 누군가를 돕는다면 :

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

이 시도

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

간단 해. 원하는 경우 선언을 추가하고 쉼표를 제거 할 수도 있습니다.

IFS=' ';declare -a array=(Paris France Europe)

IFS는 위의 명령을 취소하기 위해 추가되었지만 새로운 bash 인스턴스에서 작동하지 않습니다.


1

tr 명령을 사용하여 문자열을 배열 객체로 분할 할 수 있습니다. MacOS와 Linux 모두에서 작동합니다

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

다른 옵션은 IFS 명령을 사용합니다.

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

이것을 사용하십시오 :

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

3
나쁨 : 단어 분리 및 경로 이름 확장이 적용됩니다. 잘못된 답변을 제공하기 위해 좋은 답변으로 오래된 질문을 되 살리지 마십시오.
gniourf_gniourf

2
잘못된 답변 일 수 있지만 여전히 유효한 답변입니다. 신고자 / 검토 자 : 이 답변과 같은 잘못된 답변은 공감하지 말고 삭제하지 마십시오!
Scott Weldon

2
@gniourf_gniourf 왜 나쁜 답변인지 설명해 주시겠습니까? 나는 그것이 실패 할 때 정말로 이해하지 못한다.
George Sovetov

3
@GeorgeSovetov : 내가 말했듯이 단어 분리 및 경로 이름 확장이 적용됩니다. 보다 일반적으로, (슬프게도 매우 흔한) 반 패턴 과 같이 문자열을 배열로array=( $string ) 분할 : 단어 분할 발생 : string='Prague, Czech Republic, Europe'; : 경로 이름 확장이 발생 string='foo[abcd],bar[efgh]'하면 예를 들면, 파일 이름이있는 경우 실패 할 것이다, food또는 barf디렉토리있다. 그러한 구문의 유일한 유효한 사용법은 stringglob입니다.
gniourf_gniourf

0

업데이트 : 평가 문제로 인해이 작업을 수행하지 마십시오.

의식이 약간 적음 :

IFS=', ' eval 'array=($string)'

예 :

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar

4
평가는 악하다! 이러지 마
caesarsol

1
ff 아니요.이 문제를 처리 할 수있을만큼 큰 스크립트를 작성하는 경우 잘못하고 있습니다. 응용 프로그램 코드에서 eval은 사악합니다. 쉘 스크립팅에서는 일반적이며 필요하며 중요하지 않습니다.
user1009908

2
을 넣어 $나는 많은 스크립트를 작성하고 내가 하나를 사용할 수 없었 ... 당신의 변수에 당신은 볼eval
caesarsol

2
맞습니다. 입력이 깨끗하다고 ​​알려진 경우에만 사용할 수 있습니다. 강력한 솔루션이 아닙니다.
user1009908

내가 eval을 사용해야했던 유일한 시간은 자체 코드 / 모듈을 자체 생성하는 응용 프로그램이었습니다 ... 그리고 이것은 결코 사용자 입력 형식이 없었습니다 ...
Angry 84

0

내 해킹이야!

문자열을 문자열로 나누는 것은 bash를 사용하여 지루한 일입니다. 우리는 몇 가지 경우에만 작동하는 접근 방식이 제한되어 있거나 ( ";", "/", "."등으로 분리됨) 출력에 다양한 부작용이 있습니다.

아래의 접근 방식에는 많은 기동이 필요했지만 대부분의 요구에 적합하다고 생각합니다!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/1285/how-do-i-list-all-databases-and-tables-using-psql]


"

f_split "$STRING_TO_SPLIT" "bin/psql -c"

# --------------------------------------
# OUTPUT AND TEST
# ----------------

ARR_LENGTH=${#F_SPLIT_R[*]}
for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do
    echo " > -----------------------------------------"
    echo "${F_SPLIT_R[$i]}"
    echo " < -----------------------------------------"
done

if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then
    echo " > -----------------------------------------"
    echo "The strings are the same!"
    echo " < -----------------------------------------"
fi

0

여러 줄로 된 요소의 경우

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

다른 방법은 다음과 같습니다.

string="Paris, France, Europe"
IFS=', ' arr=(${string})

이제 요소는 "arr"배열에 저장됩니다. 요소를 반복하려면 다음을 수행하십시오.

for i in ${arr[@]}; do echo $i; done

1
나는이 아이디어를 내 대답으로 다룬다 . 오답 # 5를 참조하십시오 ( eval트릭에 대한 나의 토론에 특히 관심이있을 수 있습니다 ). 솔루션 $IFS은 사실 쉼표 공간 값으로 설정되어 있습니다.
bgoldst

-1

이를 해결하는 방법은 매우 다양하므로 먼저 솔루션에서보고 싶은 것을 정의 해 봅시다.

  1. Bash는 readarray이러한 목적 으로 내장 기능 을 제공합니다 . 사용합시다.
  2. 변경 IFS, 반복, 사용 eval또는 추가 요소 추가 후 제거 와 같은 추악하고 불필요한 트릭을 피하십시오 .
  3. 비슷한 문제에 쉽게 적용 할 수있는 간단하고 읽기 쉬운 방법을 찾으십시오.

readarray명령은 구분 기호로 줄 바꿈으로 사용하는 것이 가장 쉬운 방법입니다. 다른 구분 기호를 사용하면 배열에 추가 요소를 추가 할 수 있습니다. 가장 깨끗한 방법은 입력을 readarray전달하기 전에 먼저 잘 작동하는 형식으로 입력을 조정 하는 것입니다.

이 예의 입력 에는 다중 문자 분리 문자 가 없습니다 . 약간의 상식을 적용하면 각 요소를 잘라야하는 쉼표로 구분 된 입력으로 이해하는 것이 가장 좋습니다. 내 해결책은 입력을 쉼표로 여러 줄로 나누고 각 요소를 자르고 모두 전달하는 것 readarray입니다.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

다른 접근법은 다음과 같습니다.

str="a, b, c, d"  # assuming there is a space after ',' as in Q
arr=(${str//,/})  # delete all occurrences of ','

이 'arr'뒤에는 4 개의 문자열이있는 배열이 있습니다. 이것은 IFS 또는 읽기 또는 다른 특별한 것들을 다루지 않아도되므로 훨씬 간단하고 직접적입니다.


다른 답변과 동일 (약간 일반적으로) 반 패턴 : 단어 분할 및 파일 이름 확장이 적용됩니다.
gniourf_gniourf
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.