如何根据字母顺序等在脚本/命令行中选择当前目录中文件的子范围?

如何根据字母顺序等在脚本/命令行中选择当前目录中文件的子范围?

在文件管理器中,您通常可以选择一个文件,然后按住Shift并选择另一个文件。中间的每个文件都将被选择。

我想做bash/zsh相当于这个。

即:我想给出 2 个文件名,并在中间包含每个文件名(按字母顺序排列 -ls输出它们的方式)。

我知道? {} *和其他通配符选项,但是我希望能够在名称高度混乱的文件上使用它。

例如:给定文件

$ ls
aoeitoae.txt
oaeistn.txt
oaie.txt
paeoai.txt
sotaoe.txt

我想发出这样的命令rm aoeitoae.txt-oaie.txt,然后得到:

$ ls
paeoai.txt
sotaoe.txt

我怎样才能实现这个目标?

答案1

这个答案主要针对 zsh。大部分工作都不能在 bash 中轻松完成。

许多常见的情况都可以用通配符。尤其:

  • 一切都带有固定前缀:foo-*
  • 一切都带有固定前缀后跟一个范围内的另一个字母:(foo[b-m]*包括foobar, food, foomz1,但不包括fooarfoon
  • 一定范围内的数字: IMG-<7-42>.*(包括IMG-8.PNGIMG-0042.JPG但不包括IMG-77.JPG)

全局限定符,有一个简单的方法可以识别范围,但需要计数foo*([1,3])匹配 列出的前 3 个文件foo*,无论它们是什么(如果少于 3 个,则匹配所有文件)。这发生在完成任何排序之后,例如foo*(om[1,3])匹配名称以foo.

你可以让 zsh 帮你算出数字。分两步进行:首先将所有匹配项放入一个数组中,然后使用下标标志 iand Ie如果您想阻止任何通配符匹配):是从元素到元素(包含在内$a[$a[(i)foo],$a[(I)bar]])的数组部分,如果或不存在,则为空。$afoobarfoobar

a=(*.txt(oL))
# List the files that appear between small.txt and large.txt in a listing by size.
# Note that files that have the same size as one of the bounds may or may not be included.
echo $a[$a[(I)small.txt],$a[(I)large.txt]]

所以这是一个实现的函数完全符合问题的要求(除了精确的语法,这是无法完成的):

# Usage: select_range FROM TO WILDCARD_PATTERN
# Sets the array $s to the files matching PATTERN from FROM to TO inclusive.
function select_range {
  if (($# < 2)); then
    echo >&2 "select_range: missing range arguments"
    return 120
  fi
  local from=$1 to=$2
  shift 2
  from=$@[(ie)$from]
  if ((from == 0)); then
    echo >&2 "select_range: not matched: $from"
  fi
  to=$@[(Ie)$to]
  if ((to == 0)); then
    echo >&2 "select_range: not matched: $from"
  fi
  s=($@[$from,$to])
}

用法:select_range aoeitoae.txt oaie.txt * && rm $s

globe限定符允许您编写任意代码来过滤结果,但它已经开始变得有点笨拙。在复杂的情况下,引用可能会很棘手;为了简单起见,请使用'作为分隔符(需要用反斜杠引用)并将过滤器代码放在单引号中,这意味着模式如下所示:foo-*(e\''code goes here'\')。 (如果引用变得太复杂,请编写一个函数并使用限定符+。)过滤之后aoeitoae.txt和之前的文件oaie.txt 按字典顺序: *(e\''! [[ $REPLY < aoeitoae.txt || $REPLY > oaie.txt ]]'\')

请注意,过滤器中进行的比较不一定使用与通配符扩展相同的顺序。例如,在感谢限定符之前foo-*(n)列出,但在字符串比较中,并没有foo-9foo-10n[[ foo-9 > foo-10 ]]有条件的类似于对>整数子串进行数字比较的运算符。如果你想做一个字符串与按数字排序的整数部分的比较,您可以使用n 参数扩展标志对于数组排序并检查它是否将匹配的名称保留在中间:*(ne\''a=(b11r $REPLY f10o); [[ $a[2] == "${${(@n)a}[2]}" ]]'\'))包括b101r, b11s, d1, f02o, ...,但不包括b9r, f011, ...

如果您按日期匹配文件,则可以使用-nt条件(请注意,文件不比其自身更新):*(ome\''! [[ $REPLY -ot from || $REPLY -nt to ]]'\')仅包含之间修改的文件的修改时间from和 的修改时间to(含)。

答案2

使用 bash 函数:

sfiles ()
(
    # run this in a subshell, so we don't have to care if nullglob/dotglob were enabled or not
    [ $# -eq 0 ] && exit
    
    local nullsep=0
    if [ "$1" = "-0" ]; then
        nullsep=1; shift
    fi
    local first=$1
    shift $(($# -1))
    local last=$1
    local files=( )

    shopt -s nullglob dotglob
    for i in *; do
        # first argument found or array not empty?
        if [ "$i" = "$first" ] || [ "${#files[@]}" -ne 0 ]; then
            files+=( "$i" )
        fi
        # last argument found? break loop
        [ "$i" = "$last" ] && break
    done

    if [ "${#files[@]}" -gt 0 ]; then
        [ "$nullsep" -eq 1 ] && 
            printf '%s\0' "${files[@]}" ||
            printf '%s\n' "${files[@]@Q}"
    fi
)

它输出第一个参数和最后一个参数(包括)之间的所有文件。

例子:

$ ls -A
 btjhyyxrlv.txt    otewagahzp.txt       .xxx
 crlcsbzizl.txt    ssffszhdmp.txt      'zdjtgahx q.txt'
 hgiagchkgt.txt   'tt'$'\t''aa.txt'    'zmwik zhur.txt'
 jusupbivit.txt    umikyfucgu.txt      'z otmleqlq.txt'
' kcyigyurc.txt'  ' upvpntdfv.txt'      .zzz
 kfthnpgrxm.txt   'uu'$'\t\t''aa.txt'
 lgzsmquxwj.txt    wlwexgzohs.txt
$ sfiles c* k*
'crlcsbzizl.txt'
'hgiagchkgt.txt'
'jusupbivit.txt'
' kcyigyurc.txt'
'kfthnpgrxm.txt'
$ sfiles .xxx .zzz
'.xxx'
'zdjtgahx q.txt'
'zmwik zhur.txt'
'z otmleqlq.txt'
'.zzz'
$ LC_ALL=C sfiles .xxx .zzz
'.xxx'
'.zzz'

顺序错误,这个什么也不返回:

$ sfiles .zzz .xxx

使用以下命令删除选定的文件xargs

$ sfiles .xxx .zzz | xargs rm

对于带有制表符或换行符的文件名,添加选项-0作为第一个参数,以进行空分隔输出,无需 bash 引用。

$ sfiles -0 tt* uu* | xargs -0 ls
'tt'$'\t''aa.txt'  ' upvpntdfv.txt'
 umikyfucgu.txt    'uu'$'\t\t''aa.txt'

答案3

您可以编写自己的 shell 函数来为您执行此操作:获取所有文件的列表,并删除第一个文件名之前的所有文件以及第二个文件名之后的所有文件。粗略地说,在 zsh 中,因为我认为这在 bash 中会更烦人:

#!/usr/bin/zsh
filerange() {
  # variable "reply" is an array
  typeset -ag reply
  reply=("$1")
  # don't get upset if nothing matches
  setopt nullglob

  # get a complete list of candidate files
  #allfiles=*(^/^on) 
  #         ^--------- *   glob
  #          ^----^--- (…) glob modifiers within parentheses
  #           ^------- ^   invert match
  #            ^------ /   match directories (inverted: match all *but* dirs)
  #             ^----- ^   invert sort order (uninvert the above inversion)
  #              ^---- o   order by:
  #               ^--- n    character values of name
  for fname in *(on^/); do
    [[ ( "$2" > "${fname}" && "${fname}" > "$1" ) ]] \
    && reply+=("${fname}")
  done
  reply+=("$2")
}

这为您提供了一个漂亮且“安全”的数组,$reply您可以在其中很好地使用例如for

# prepare a test dir
mkdir /tmp/testdir
cd /tmp/testdir
sudo mknod blockdev b 1 1
sudo mknod chardev c 1 1
mkdir dir
mkfifo fifo
touch file
touch "file with spaces"
touch "file with\nnewlines"
touch "last file"
touch "ok one more file"
ln -s blockdev symlink

# This will look funny, because there's a file name with a new line in there
echo *

# Let's try this:
filerange chardev "last file"
# We're expecting to get all files from (incl) chardev to (incl) last file,
# but not `dir`, as that's a directory
for f in ${reply}; do;
  echo "entry: ${f}"
done

当然,如果您只想打印这些文件(例如, forparallelxargs),请编写一个采用可选项-0作为 Freddy's 的函数很好的答案插图是一个不错的选择!该函数非常简单:它可以在'ing循环中调用filerange并处理$replyfrom 。printffor

答案4

这不是一个完整的答案,但我想提一下,它awk有一个很好的可以使用的范围运算符:

$ ls | awk '/^aoeitoae\.txt$/,/^oaie\.txt$/ { print }'
aoeitoae.txt
oaeistn.txt
oaie.txt

awk代码打印输入行,从与第一个正则表达式匹配的第一个输入行开始,到与第二个正则表达式匹配的第一个输入行结束。

请注意,匹配使用正则表达式,并且在本示例中没有引用任何内容,因此这可能会导致文件名中出现空格或其他奇怪字符而导致意外结果。

或许您想尝试使用基于文本的文件管理器,例如午夜指挥官

相关内容